Compare commits
10 Commits
f23fa1662e
...
690ecc2053
| Author | SHA1 | Date | |
|---|---|---|---|
| 690ecc2053 | |||
| 1dbaa66206 | |||
| 41103bbc5a | |||
| f751295449 | |||
| 2444a01fc7 | |||
| 957fdb7dc8 | |||
| ca6196234e | |||
| 2e7aeda684 | |||
| 0cecb1eddf | |||
| dcd845d337 |
@@ -37,16 +37,26 @@
|
||||
}
|
||||
</MudText>
|
||||
</MudStack>
|
||||
<div class="dashboard-manometer" aria-label="Export activity manometer">
|
||||
<div class="manometer-arc">
|
||||
<span class="tick tick-0"></span>
|
||||
<span class="tick tick-1"></span>
|
||||
<span class="tick tick-2"></span>
|
||||
<span class="tick tick-3"></span>
|
||||
<span class="tick tick-4"></span>
|
||||
<span class="needle"></span>
|
||||
<span class="hub"></span>
|
||||
</div>
|
||||
<div class="dashboard-manometer">
|
||||
<svg class="manometer-svg" viewBox="0 0 210 118" role="img" aria-label="Export activity manometer">
|
||||
<path class="manometer-outer" d="M25 98 A80 80 0 0 1 185 98" />
|
||||
<path class="manometer-inner" d="M47 98 A58 58 0 0 1 163 98" />
|
||||
<line class="manometer-tick" x1="38" y1="98" x2="56" y2="98" />
|
||||
<line class="manometer-tick" x1="61" y1="54" x2="74" y2="67" />
|
||||
<line class="manometer-tick" x1="105" y1="34" x2="105" y2="52" />
|
||||
<line class="manometer-tick" x1="149" y1="54" x2="136" y2="67" />
|
||||
<line class="manometer-tick" x1="172" y1="98" x2="154" y2="98" />
|
||||
<text class="manometer-label" x="43" y="89">0</text>
|
||||
<text class="manometer-label" x="67" y="53">25</text>
|
||||
<text class="manometer-label" x="105" y="28">50</text>
|
||||
<text class="manometer-label" x="143" y="53">75</text>
|
||||
<text class="manometer-label" x="167" y="89">100</text>
|
||||
<text class="manometer-caption" x="105" y="113">EXPORT</text>
|
||||
<g class="manometer-needle">
|
||||
<line class="needle-line" x1="105" y1="98" x2="105" y2="38" />
|
||||
</g>
|
||||
<circle class="manometer-hub" cx="105" cy="98" r="11" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</MudPaper>
|
||||
@@ -211,87 +221,80 @@
|
||||
justify-self: end;
|
||||
width: 210px;
|
||||
height: 118px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border: 1px solid #111;
|
||||
border-radius: 6px;
|
||||
padding: 12px 14px 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.manometer-arc {
|
||||
position: relative;
|
||||
width: 170px;
|
||||
height: 86px;
|
||||
border: 8px solid #111;
|
||||
border-bottom: 0;
|
||||
border-radius: 170px 170px 0 0;
|
||||
background: #fff;
|
||||
overflow: visible;
|
||||
.manometer-svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.manometer-arc::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
bottom: -1px;
|
||||
height: 70px;
|
||||
border: 2px solid #111;
|
||||
border-bottom: 0;
|
||||
border-radius: 140px 140px 0 0;
|
||||
.manometer-outer,
|
||||
.manometer-inner {
|
||||
fill: none;
|
||||
stroke: #111;
|
||||
stroke-linecap: square;
|
||||
}
|
||||
|
||||
.tick {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: #111;
|
||||
transform-origin: 50% 78px;
|
||||
.manometer-outer {
|
||||
stroke-width: 8;
|
||||
}
|
||||
|
||||
.tick-0 { transform: translateX(-50%) rotate(-70deg); }
|
||||
.tick-1 { transform: translateX(-50%) rotate(-35deg); }
|
||||
.tick-2 { transform: translateX(-50%) rotate(0deg); }
|
||||
.tick-3 { transform: translateX(-50%) rotate(35deg); }
|
||||
.tick-4 { transform: translateX(-50%) rotate(70deg); }
|
||||
.manometer-inner {
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.needle {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
height: 72px;
|
||||
background: #111;
|
||||
border-radius: 4px;
|
||||
transform-origin: 50% 100%;
|
||||
.manometer-tick,
|
||||
.needle-line {
|
||||
stroke: #111;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: square;
|
||||
}
|
||||
|
||||
.manometer-needle {
|
||||
transform-box: view-box;
|
||||
transform-origin: 105px 98px;
|
||||
animation: manometer-sweep 5.8s infinite cubic-bezier(.45, 0, .25, 1);
|
||||
}
|
||||
|
||||
.hub {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -8px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #111;
|
||||
transform: translateX(-50%);
|
||||
.manometer-hub {
|
||||
fill: #111;
|
||||
}
|
||||
|
||||
.manometer-label,
|
||||
.manometer-caption {
|
||||
fill: #111;
|
||||
font-family: Arial, sans-serif;
|
||||
text-anchor: middle;
|
||||
dominant-baseline: middle;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.manometer-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.manometer-caption {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
@@keyframes manometer-sweep {
|
||||
0% { transform: translateX(-50%) rotate(-52deg); }
|
||||
11% { transform: translateX(-50%) rotate(18deg); }
|
||||
19% { transform: translateX(-50%) rotate(-8deg); }
|
||||
33% { transform: translateX(-50%) rotate(63deg); }
|
||||
48% { transform: translateX(-50%) rotate(4deg); }
|
||||
61% { transform: translateX(-50%) rotate(38deg); }
|
||||
74% { transform: translateX(-50%) rotate(-41deg); }
|
||||
88% { transform: translateX(-50%) rotate(55deg); }
|
||||
100% { transform: translateX(-50%) rotate(-52deg); }
|
||||
0% { transform: rotate(-52deg); }
|
||||
11% { transform: rotate(18deg); }
|
||||
19% { transform: rotate(-8deg); }
|
||||
33% { transform: rotate(63deg); }
|
||||
48% { transform: rotate(4deg); }
|
||||
61% { transform: rotate(38deg); }
|
||||
74% { transform: rotate(-41deg); }
|
||||
88% { transform: rotate(55deg); }
|
||||
100% { transform: rotate(-52deg); }
|
||||
}
|
||||
|
||||
@@media (max-width: 900px) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<MudStack Row AlignItems="AlignItems.Center" Class="mb-3">
|
||||
<div>
|
||||
<MudText Typo="Typo.h6">@T("Net Sales Actuals 2025 Referenz", "Net sales actuals 2025 reference")</MudText>
|
||||
<MudText Typo="Typo.caption">@T("Verbindliche Finance-Sicht aus CentralSalesRecords", "Authoritative finance view from CentralSalesRecords")</MudText>
|
||||
<MudText Typo="Typo.caption">@T("Verbindliche Finance-Sicht aus der aktuellen zentralen Datenquelle", "Authoritative finance view from the current central data source")</MudText>
|
||||
</div>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="@(_hideRowsWithoutActual ? Variant.Filled : Variant.Outlined)"
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
@if (_financeResult is not null)
|
||||
{
|
||||
<MudTabs Elevation="1" Rounded="false" PanelClass="pt-4" @bind-ActivePanelIndex="_activeOverviewTabIndex">
|
||||
<MudTabs Class="management-side-nav-tabs" Elevation="0" Rounded="false" PanelClass="pt-0" @bind-ActivePanelIndex="_activeOverviewTabIndex">
|
||||
<MudTabPanel Text="@T("Schnelluebersicht", "Quick overview")" Icon="@Icons.Material.Filled.Speed">
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
@@ -84,9 +84,7 @@
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudTabs Elevation="0" Rounded="false" PanelClass="pt-4">
|
||||
<MudTabPanel Text="@T("Freigabe", "Approval")" Icon="@Icons.Material.Filled.FactCheck">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">@T("Finance-Freigabe je Land", "Finance approval by country")</MudText>
|
||||
<MudTable Items="_financeResult.CountryRows" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
@@ -109,9 +107,7 @@
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="@T("Datenstand", "Data status")" Icon="@Icons.Material.Filled.Storage">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">@T("Letzter Datenstand je Standort", "Latest data status by site")</MudText>
|
||||
<MudTable Items="_financeResult.DataStatusRows" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
@@ -139,8 +135,6 @@
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="@T("Sparten", "Divisions")" Icon="@Icons.Material.Filled.AccountTree">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">@T("Sparten-Abdeckung nach Land", "Division coverage by country")</MudText>
|
||||
<MudTable Items="_financeResult.ProductFinanceCountryRows" Dense Hover Striped>
|
||||
@@ -162,12 +156,10 @@
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</MudTabPanel>
|
||||
|
||||
<MudTabPanel Text="@T("Experten", "Experts")" Icon="@Icons.Material.Filled.Tune">
|
||||
<MudTabs Elevation="0" Rounded="false" PanelClass="pt-4" @bind-ActivePanelIndex="_activeFinanceTabIndex">
|
||||
<MudTabs Class="management-side-nav-tabs" Elevation="0" Rounded="false" PanelClass="pt-0" @bind-ActivePanelIndex="_activeFinanceTabIndex">
|
||||
<MudTabPanel Text="@T("Finance Summary", "Finance summary")" Icon="@Icons.Material.Filled.Dashboard">
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
@@ -409,7 +401,7 @@
|
||||
</MudPaper>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="@T("Spartenanalyse", "Division analysis")" Icon="@Icons.Material.Filled.AccountTree">
|
||||
<MudTabs Elevation="0" Rounded="false" PanelClass="pt-4" @bind-ActivePanelIndex="_activeDivisionTabIndex">
|
||||
<MudTabs Class="management-side-nav-tabs" Elevation="0" Rounded="false" PanelClass="pt-0" @bind-ActivePanelIndex="_activeDivisionTabIndex">
|
||||
<MudTabPanel Text="@T("Finanzanalyse", "Finance analysis")" Icon="@Icons.Material.Filled.PieChart">
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
@@ -815,7 +807,7 @@
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">@T("Zentrale Roh-Auswertung", "Central raw analysis")</MudText>
|
||||
<MudAlert Severity="Severity.Info" Dense Variant="Variant.Outlined" Class="mb-3">
|
||||
@T("Diese Sicht arbeitet direkt auf `CentralSalesRecords`. Summenfeld und Anzeige-Waehrung koennen gewaehlt werden; fachliche Filter wie Intercompany, Budget und Spartenlogik sind weiterhin nicht enthalten.", "This view works directly on `CentralSalesRecords`. Value field and display currency can be selected; business filters such as intercompany, budget and divisional logic are still not included.")
|
||||
@T("Diese Sicht arbeitet auf der aktuell konfigurierten zentralen Datenquelle (DB oder Audit-CSV). Summenfeld und Anzeige-Waehrung koennen gewaehlt werden; fachliche Filter wie Intercompany, Budget und Spartenlogik sind weiterhin nicht enthalten.", "This view works on the currently configured central data source (DB or audit CSV). Value field and display currency can be selected; business filters such as intercompany, budget and divisional logic are still not included.")
|
||||
</MudAlert>
|
||||
<MudAlert Severity="Severity.Warning" Dense Variant="Variant.Outlined" Class="mb-3">
|
||||
@T("Diese Analyse ist eine Plausibilitaets- und Rohdatensicht. Fuer den verbindlichen Finance-Abgleich bitte `Soll/Ist Vergleich` oder im Endexcel die `Finance | ...`-Spalten verwenden.",
|
||||
@@ -1108,6 +1100,12 @@
|
||||
</MudTabs>
|
||||
}
|
||||
|
||||
<style>
|
||||
.management-side-nav-tabs .mud-tabs-toolbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
[SupplyParameterFromQuery(Name = "section")]
|
||||
@@ -1446,6 +1444,28 @@
|
||||
if (_financeResult is null)
|
||||
return [];
|
||||
|
||||
if (IsFinance3dReferenceYearIndicator(_finance3dIndicator))
|
||||
{
|
||||
return _financeResult.CountryRows
|
||||
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group =>
|
||||
{
|
||||
var rows = group.ToList();
|
||||
var first = rows[0];
|
||||
return new
|
||||
{
|
||||
country = first.CountryKey,
|
||||
year = first.Year,
|
||||
currency = BuildDisplayCurrencyLabel(rows.Select(row => row.Currency).Where(value => value != "-")),
|
||||
value = ResolveFinance3dCountryValue(rows)
|
||||
};
|
||||
})
|
||||
.OrderBy(row => row.country, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(row => row.year)
|
||||
.Cast<object>()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var countryRowsByKey = _financeResult.CountryRows
|
||||
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
@@ -1453,9 +1473,7 @@
|
||||
group => group.ToList(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var sourceRows = IsFinance3dReferenceYearIndicator(_finance3dIndicator)
|
||||
? _financeResult.Rows
|
||||
: (_financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows);
|
||||
var sourceRows = _financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows;
|
||||
|
||||
return sourceRows
|
||||
.OrderBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||
@@ -1481,6 +1499,22 @@
|
||||
if (_financeResult is null)
|
||||
return 0m;
|
||||
|
||||
if (IsFinance3dReferenceYearIndicator(_finance3dIndicator))
|
||||
{
|
||||
var referenceValues = _financeResult.CountryRows
|
||||
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => ResolveFinance3dCountryValue(group.ToList()))
|
||||
.ToList();
|
||||
|
||||
if (IsFinance3dPercentIndicator(_finance3dIndicator))
|
||||
{
|
||||
var nonZeroValues = referenceValues.Where(value => value != 0m).ToList();
|
||||
return nonZeroValues.Count == 0 ? 0m : nonZeroValues.Average();
|
||||
}
|
||||
|
||||
return referenceValues.Sum();
|
||||
}
|
||||
|
||||
var countryRowsByKey = _financeResult.CountryRows
|
||||
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
@@ -1488,9 +1522,7 @@
|
||||
group => group.ToList(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var sourceRows = IsFinance3dReferenceYearIndicator(_finance3dIndicator)
|
||||
? _financeResult.Rows
|
||||
: (_financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows);
|
||||
var sourceRows = _financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows;
|
||||
|
||||
var values = sourceRows
|
||||
.Select(row =>
|
||||
@@ -1529,6 +1561,15 @@
|
||||
_ => Math.Abs(row.NetSalesActual)
|
||||
};
|
||||
|
||||
private decimal ResolveFinance3dCountryValue(IReadOnlyCollection<ManagementFinanceCountryStatusRow> rows)
|
||||
=> _finance3dIndicator switch
|
||||
{
|
||||
Finance3dIndicators.ReferenceValue => Math.Abs(rows.Select(row => row.ReferenceValue).FirstOrDefault(value => value.HasValue) ?? 0m),
|
||||
Finance3dIndicators.Deviation => Math.Abs(rows.Where(row => row.Difference.HasValue).Sum(row => row.Difference!.Value)),
|
||||
Finance3dIndicators.DeviationPercent => Math.Abs(AverageNullablePercent(rows.Select(row => row.DifferencePercent))),
|
||||
_ => 0m
|
||||
};
|
||||
|
||||
private static bool IsFinance3dReferenceYearIndicator(string indicator)
|
||||
=> indicator is Finance3dIndicators.ReferenceValue or Finance3dIndicators.Deviation or Finance3dIndicators.DeviationPercent;
|
||||
|
||||
@@ -1662,6 +1703,17 @@
|
||||
? value.ToString("N2")
|
||||
: $"{value:N2} {currency}";
|
||||
|
||||
private static string BuildDisplayCurrencyLabel(IEnumerable<string> currencies)
|
||||
{
|
||||
var distinct = currencies
|
||||
.Where(currency => !string.IsNullOrWhiteSpace(currency) && currency != "-")
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(currency => currency, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
return distinct.Count == 0 ? "-" : string.Join("/", distinct);
|
||||
}
|
||||
|
||||
private static string FormatNullableValue(decimal? value, string currency)
|
||||
=> value.HasValue ? FormatValue(value.Value, currency) : "-";
|
||||
|
||||
@@ -1714,6 +1766,8 @@
|
||||
{
|
||||
if (!row.ReferenceValue.HasValue)
|
||||
return T("Kein Sollwert gepflegt.", "No reference value maintained.");
|
||||
if (row.TotalRows == 0)
|
||||
return T("Sollwert gepflegt, aber kein Ist im aktuellen Filter.", "Reference maintained, but no actuals in the current filter.");
|
||||
if (row.Status == "OK")
|
||||
return T("Freigabefaehig.", "Ready for approval.");
|
||||
if (row.Difference.HasValue)
|
||||
|
||||
@@ -37,13 +37,13 @@
|
||||
</MudText>
|
||||
<div class="purchasing-hero-actions">
|
||||
<MudChip T="string" Size="Size.Small" Color="@(_liveState.EkkoLoaded ? Color.Success : Color.Warning)" Variant="Variant.Outlined">
|
||||
EKKO @(_liveState.EkkoLoaded ? "live" : "pending")
|
||||
EKKO @(_liveState.EkkoLoaded ? T("live", "live") : T("wartet", "pending"))
|
||||
</MudChip>
|
||||
<MudChip T="string" Size="Size.Small" Color="@(_liveState.EkpoLoaded ? Color.Success : Color.Warning)" Variant="Variant.Outlined">
|
||||
EKPO @(_liveState.EkpoLoaded ? "live" : "pending")
|
||||
EKPO @(_liveState.EkpoLoaded ? T("live", "live") : T("wartet", "pending"))
|
||||
</MudChip>
|
||||
<MudChip T="string" Size="Size.Small" Color="@(_liveState.EketLoaded ? Color.Success : Color.Warning)" Variant="Variant.Outlined">
|
||||
EKET @(_liveState.EketLoaded ? "live" : "pending")
|
||||
EKET @(_liveState.EketLoaded ? T("live", "live") : T("wartet", "pending"))
|
||||
</MudChip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -371,7 +371,7 @@
|
||||
"Full load builds the correct base. Delta then updates only changed purchase documents.")
|
||||
</MudText>
|
||||
</div>
|
||||
<MudChip T="string" Color="@ResolveRefreshStatusColor()" Variant="Variant.Outlined">@_refreshStatus.Status</MudChip>
|
||||
<MudChip T="string" Color="@ResolveRefreshStatusColor()" Variant="Variant.Outlined">@TranslateRefreshStatus(_refreshStatus.Status)</MudChip>
|
||||
</MudStack>
|
||||
<MudGrid Spacing="2" Class="mb-3">
|
||||
<MudItem xs="12" sm="6" lg="3">
|
||||
@@ -399,7 +399,7 @@
|
||||
<MudPaper Class="pa-3 purchasing-section-kpi" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Class="purchasing-muted">@T("Letzter Stand", "Latest state")</MudText>
|
||||
<MudText Typo="Typo.h6">@FormatRefreshDate(_refreshStatus.CompletedAtUtc)</MudText>
|
||||
<MudText Typo="Typo.caption">@_refreshStatus.Mode</MudText>
|
||||
<MudText Typo="Typo.caption">@TranslateRefreshMode(_refreshStatus.Mode)</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
@@ -415,7 +415,7 @@
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="mb-2" />
|
||||
}
|
||||
<MudText Typo="Typo.body2" Class="purchasing-muted">@_refreshStatus.Message</MudText>
|
||||
<MudText Typo="Typo.body2" Class="purchasing-muted">@TranslatePurchasingMessage(_refreshStatus.Message)</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
@@ -1534,7 +1534,66 @@
|
||||
private string PurchasingStatusText
|
||||
=> _liveLoading
|
||||
? T("SAP-Einkaufsdaten werden geladen...", "Loading SAP purchasing data...")
|
||||
: $"{_liveState.Message} {FormatLatestOrderDate()}";
|
||||
: $"{TranslatePurchasingMessage(_liveState.Message)} {FormatLatestOrderDate()}";
|
||||
|
||||
private string TranslateRefreshStatus(string status)
|
||||
=> status switch
|
||||
{
|
||||
"Success" => T("Erfolgreich", "Success"),
|
||||
"Running" => T("Laeuft", "Running"),
|
||||
"Error" => T("Fehler", "Error"),
|
||||
"Empty" => T("Noch leer", "Empty"),
|
||||
_ => string.IsNullOrWhiteSpace(status) ? "-" : status
|
||||
};
|
||||
|
||||
private string TranslateRefreshMode(string mode)
|
||||
=> mode switch
|
||||
{
|
||||
"Full" => T("Full Load", "Full load"),
|
||||
"Delta" => T("Delta", "Delta"),
|
||||
_ => string.IsNullOrWhiteSpace(mode) ? "-" : mode
|
||||
};
|
||||
|
||||
private string TranslatePurchasingMessage(string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return string.Empty;
|
||||
|
||||
if (message.Contains("SAP Einkaufsquelle ist noch nicht konfiguriert", StringComparison.OrdinalIgnoreCase))
|
||||
return T("SAP Einkaufsquelle ist noch nicht konfiguriert.", "SAP purchasing source is not configured yet.");
|
||||
if (message.Contains("SAP URL oder Zugangsdaten fehlen", StringComparison.OrdinalIgnoreCase))
|
||||
return T("SAP URL oder Zugangsdaten fehlen.", "SAP URL or credentials are missing.");
|
||||
if (message.Contains("SAP Einkaufsdaten inkl. EKPO/EKET geladen", StringComparison.OrdinalIgnoreCase))
|
||||
return T("SAP Einkaufsdaten inkl. EKPO/EKET geladen.", "SAP purchasing data including EKPO/EKET loaded.");
|
||||
if (message.Contains("SAP Einkaufsdaten inkl. EKPO geladen", StringComparison.OrdinalIgnoreCase))
|
||||
return T("SAP Einkaufsdaten inkl. EKPO geladen; EKET liefert noch keine Termindaten.", "SAP purchasing data including EKPO loaded; EKET does not return schedule data yet.");
|
||||
if (message.Contains("EKKO ist live geladen", StringComparison.OrdinalIgnoreCase))
|
||||
return T("EKKO ist live geladen; EKPO/EKET liefern aktuell noch keine Positionsdaten.", "EKKO is loaded live; EKPO/EKET currently do not return item data.");
|
||||
if (message.Contains("Noch kein Einkauf Full Load ausgefuehrt", StringComparison.OrdinalIgnoreCase))
|
||||
return T("Noch kein Einkauf Full Load ausgefuehrt.", "No purchasing full load has been run yet.");
|
||||
if (message.StartsWith("Full Load gestartet", StringComparison.OrdinalIgnoreCase))
|
||||
return T("Full Load gestartet.", "Full load started.");
|
||||
if (message.StartsWith("Delta gestartet", StringComparison.OrdinalIgnoreCase))
|
||||
return T("Delta gestartet.", "Delta started.");
|
||||
if (message.StartsWith("Full Load abgeschlossen", StringComparison.OrdinalIgnoreCase))
|
||||
return message.Replace("Full Load abgeschlossen", T("Full Load abgeschlossen", "Full load completed"), StringComparison.OrdinalIgnoreCase);
|
||||
if (message.StartsWith("Delta abgeschlossen", StringComparison.OrdinalIgnoreCase))
|
||||
return message
|
||||
.Replace("Delta abgeschlossen", T("Delta abgeschlossen", "Delta completed"), StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("geaenderte Belege", T("geaenderte Belege", "changed documents"), StringComparison.OrdinalIgnoreCase);
|
||||
if (message.StartsWith("Einkauf Cache geladen fuer", StringComparison.OrdinalIgnoreCase))
|
||||
return message
|
||||
.Replace("Einkauf Cache geladen fuer", T("Einkauf Cache geladen fuer", "Purchasing cache loaded for"), StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(" bis ", $" {T("bis", "to")} ", StringComparison.OrdinalIgnoreCase);
|
||||
if (message.StartsWith("SAP Einkauf konnte nicht geladen werden", StringComparison.OrdinalIgnoreCase))
|
||||
return message.Replace("SAP Einkauf konnte nicht geladen werden", T("SAP Einkauf konnte nicht geladen werden", "SAP purchasing could not be loaded"), StringComparison.OrdinalIgnoreCase);
|
||||
if (message.StartsWith("Full Load fehlgeschlagen", StringComparison.OrdinalIgnoreCase))
|
||||
return message.Replace("Full Load fehlgeschlagen", T("Full Load fehlgeschlagen", "Full load failed"), StringComparison.OrdinalIgnoreCase);
|
||||
if (message.StartsWith("Delta fehlgeschlagen", StringComparison.OrdinalIgnoreCase))
|
||||
return message.Replace("Delta fehlgeschlagen", T("Delta fehlgeschlagen", "Delta failed"), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private string FormatLatestOrderDate()
|
||||
=> _liveState.LatestOrderDate.HasValue
|
||||
|
||||
@@ -207,7 +207,7 @@ else
|
||||
await RunAsync(async () =>
|
||||
{
|
||||
var result = await DataSourceService.TestConnectionAsync(_state);
|
||||
Snackbar.Add(result.Message, result.Success ? Severity.Success : result.Warning ? Severity.Warning : Severity.Error);
|
||||
Snackbar.Add(TranslateConnectionMessage(result.Message), result.Success ? Severity.Success : result.Warning ? Severity.Warning : Severity.Error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -246,6 +246,23 @@ else
|
||||
|
||||
private string T(string german, string english) => UiText.Text(german, english);
|
||||
private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "-" : value;
|
||||
|
||||
private string TranslateConnectionMessage(string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return string.Empty;
|
||||
|
||||
if (message.Contains("Keine SAP Service URL gepflegt", StringComparison.OrdinalIgnoreCase))
|
||||
return T("Keine SAP Service URL gepflegt.", "No SAP service URL maintained.");
|
||||
if (message.Contains("Keine SAP Gateway Zugangsdaten gepflegt", StringComparison.OrdinalIgnoreCase))
|
||||
return T("Keine SAP Gateway Zugangsdaten gepflegt.", "No SAP Gateway credentials maintained.");
|
||||
if (message.Contains("SAP OData Verbindung erfolgreich", StringComparison.OrdinalIgnoreCase))
|
||||
return T("SAP OData Verbindung erfolgreich.", "SAP OData connection successful.");
|
||||
if (message.StartsWith("SAP OData Verbindung fehlgeschlagen", StringComparison.OrdinalIgnoreCase))
|
||||
return message.Replace("SAP OData Verbindung fehlgeschlagen", T("SAP OData Verbindung fehlgeschlagen", "SAP OData connection failed"), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -6,29 +6,31 @@
|
||||
@inject ISettingsPageService SettingsPageActions
|
||||
@inject IJSRuntime JS
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IUiTextService UiText
|
||||
|
||||
<PageTitle>Settings</PageTitle>
|
||||
<PageTitle>@T("Einstellungen", "Settings")</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Settings</MudText>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">@T("Einstellungen", "Settings")</MudText>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Konfiguration Import/Export</MudText>
|
||||
<MudText Typo="Typo.h5" Class="mb-2">@T("Konfiguration Import/Export", "Import/export configuration")</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCheckBox @bind-Value="_includeSecretsInExport" Label="Mit Secrets exportieren" />
|
||||
<MudCheckBox @bind-Value="_includeSecretsInExport" Label="@T("Mit Secrets exportieren", "Export with secrets")" />
|
||||
<MudText Typo="Typo.caption">
|
||||
Wenn deaktiviert, bleiben Passwörter und Secrets beim Export leer. Beim Import ohne Secrets werden bestehende Secrets auf dem Zielsystem beibehalten.
|
||||
@T("Wenn deaktiviert, bleiben Passwoerter und Secrets beim Export leer. Beim Import ohne Secrets werden bestehende Secrets auf dem Zielsystem beibehalten.",
|
||||
"If disabled, passwords and secrets remain empty during export. When importing without secrets, existing secrets on the target system are kept.")
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudStack Row Spacing="2">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ExportConfiguration"
|
||||
StartIcon="@Icons.Material.Filled.Download" Disabled="_exportingConfig">
|
||||
@(_exportingConfig ? "Exportiere..." : "Konfiguration exportieren")
|
||||
@(_exportingConfig ? T("Exportiere...", "Exporting...") : T("Konfiguration exportieren", "Export configuration"))
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Warning" HtmlTag="label"
|
||||
StartIcon="@Icons.Material.Filled.UploadFile" Disabled="_importingConfig">
|
||||
@(_importingConfig ? "Importiere..." : "Konfiguration importieren")
|
||||
@(_importingConfig ? T("Importiere...", "Importing...") : T("Konfiguration importieren", "Import configuration"))
|
||||
<InputFile OnChange="ImportConfiguration" accept=".json,application/json" style="display:none" />
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@@ -37,7 +39,7 @@
|
||||
</MudPaper>
|
||||
|
||||
@* SharePoint Config *@
|
||||
<MudText Typo="Typo.h5" Class="mb-2">SharePoint Konfiguration</MudText>
|
||||
<MudText Typo="Typo.h5" Class="mb-2">@T("SharePoint Konfiguration", "SharePoint configuration")</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="6">
|
||||
@@ -49,7 +51,7 @@
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="_spConfig.CentralExportFolder"
|
||||
Label="Central Export Folder"
|
||||
HelperText="Optional. Wenn leer, wird weiterhin Export Folder/Alle verwendet." />
|
||||
HelperText="@T("Optional. Wenn leer, wird weiterhin Export Folder/Alle verwendet.", "Optional. If empty, Export Folder/All is still used.")" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField @bind-Value="_spConfig.TenantId" Label="Tenant ID" />
|
||||
@@ -64,18 +66,18 @@
|
||||
<MudStack Row Spacing="2">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSharePoint"
|
||||
StartIcon="@Icons.Material.Filled.Save">
|
||||
Speichern
|
||||
@T("Speichern", "Save")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="TestSharePoint"
|
||||
StartIcon="@Icons.Material.Filled.NetworkCheck" Disabled="_testingSp">
|
||||
@if (_testingSp)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
|
||||
@("Teste...")
|
||||
@T("Teste...", "Testing...")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("SharePoint Verbindung testen")
|
||||
@T("SharePoint Verbindung testen", "Test SharePoint connection")
|
||||
}
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@@ -84,7 +86,7 @@
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mt-3">
|
||||
<div><b>Test Preview</b></div>
|
||||
<div><b>@T("Testvorschau", "Test preview")</b></div>
|
||||
<div style="white-space: pre-wrap">@_sharePointTestPreview</div>
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
@@ -92,28 +94,29 @@
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Quellsysteme</MudText>
|
||||
<MudText Typo="Typo.h5" Class="mb-2">@T("Quellsysteme", "Source systems")</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined">
|
||||
Diese Zugangsdaten werden pro Quellsystem als Standard verwendet. Ein Standort kann sie bei Bedarf mit eigenen Overrides überschreiben.
|
||||
@T("Diese Zugangsdaten werden pro Quellsystem als Standard verwendet. Ein Standort kann sie bei Bedarf mit eigenen Overrides ueberschreiben.",
|
||||
"These credentials are used as defaults per source system. A site can override them if needed.")
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="AddSourceSystem"
|
||||
StartIcon="@Icons.Material.Filled.Add" Class="mb-3">
|
||||
Quellsystem hinzufuegen
|
||||
@T("Quellsystem hinzufuegen", "Add source system")
|
||||
</MudButton>
|
||||
<MudTable Items="_sourceSystems" Dense Hover Striped Breakpoint="Breakpoint.Md">
|
||||
<HeaderContent>
|
||||
<MudTh>Code</MudTh>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Anschlussart</MudTh>
|
||||
<MudTh>Zentrale URL</MudTh>
|
||||
<MudTh>@T("Name", "Name")</MudTh>
|
||||
<MudTh>@T("Anschlussart", "Connection type")</MudTh>
|
||||
<MudTh>@T("Zentrale URL", "Central URL")</MudTh>
|
||||
<MudTh>User</MudTh>
|
||||
<MudTh>Aktiv</MudTh>
|
||||
<MudTh>Test</MudTh>
|
||||
<MudTh>@T("Aktiv", "Active")</MudTh>
|
||||
<MudTh>@T("Test", "Test")</MudTh>
|
||||
<MudTh></MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
@@ -138,7 +141,7 @@
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
|
||||
OnClick='@(() => TestCentralCredentials(context.Code))'
|
||||
Disabled='@_testingSystems.Contains(context.Code)'>
|
||||
@(_testingSystems.Contains(context.Code) ? "Teste..." : "Testen")
|
||||
@(_testingSystems.Contains(context.Code) ? T("Teste...", "Testing...") : T("Testen", "Test"))
|
||||
</MudButton>
|
||||
}
|
||||
</MudTd>
|
||||
@@ -154,7 +157,7 @@
|
||||
<MudItem xs="12">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSourceSystems"
|
||||
StartIcon="@Icons.Material.Filled.Save">
|
||||
Quellsysteme speichern
|
||||
@T("Quellsysteme speichern", "Save source systems")
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
@@ -162,12 +165,12 @@
|
||||
|
||||
<MudDialog @bind-Visible="_sourceSystemDialogVisible" Options="_sourceSystemDialogOptions">
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">@(_editingSourceSystem.Id == 0 ? "Quellsystem hinzufuegen" : "Quellsystem bearbeiten")</MudText>
|
||||
<MudText Typo="Typo.h6">@(_editingSourceSystem.Id == 0 ? T("Quellsystem hinzufuegen", "Add source system") : T("Quellsystem bearbeiten", "Edit source system"))</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudTextField @bind-Value="_editingSourceSystem.Code" Label="Code" Required />
|
||||
<MudTextField @bind-Value="_editingSourceSystem.DisplayName" Label="Name" Required />
|
||||
<MudSelect T="string" @bind-Value="_editingSourceSystem.ConnectionKind" Label="Anschlussart" Required>
|
||||
<MudTextField @bind-Value="_editingSourceSystem.DisplayName" Label="@T("Name", "Name")" Required />
|
||||
<MudSelect T="string" @bind-Value="_editingSourceSystem.ConnectionKind" Label="@T("Anschlussart", "Connection type")" Required>
|
||||
@foreach (var kind in SourceSystemConnectionKinds.All)
|
||||
{
|
||||
<MudSelectItem Value="@kind">@GetConnectionKindLabel(kind)</MudSelectItem>
|
||||
@@ -176,46 +179,47 @@
|
||||
@if (UsesSapGateway(_editingSourceSystem))
|
||||
{
|
||||
<MudTextField @bind-Value="_editingSourceSystem.CentralServiceUrl" Label="Zentrale SAP Service URL"
|
||||
HelperText="Zentrale Standard-URL fuer SAP Gateway. Ein Standort darf sie nur bei Bedarf ueberschreiben." />
|
||||
HelperText="@T("Zentrale Standard-URL fuer SAP Gateway. Ein Standort darf sie nur bei Bedarf ueberschreiben.", "Central default URL for SAP Gateway. A site may override it only if needed.")" />
|
||||
}
|
||||
<MudTextField @bind-Value="_editingSourceSystem.CentralUsername" Label="Zentraler Username" />
|
||||
<MudTextField @bind-Value="_editingSourceSystem.CentralPassword" Label="Zentrales Passwort" InputType="InputType.Password" />
|
||||
<MudCheckBox @bind-Value="_editingSourceSystem.IsActive" Label="Aktiv" />
|
||||
<MudTextField @bind-Value="_editingSourceSystem.CentralUsername" Label="@T("Zentraler Username", "Central username")" />
|
||||
<MudTextField @bind-Value="_editingSourceSystem.CentralPassword" Label="@T("Zentrales Passwort", "Central password")" InputType="InputType.Password" />
|
||||
<MudCheckBox @bind-Value="_editingSourceSystem.IsActive" Label="@T("Aktiv", "Active")" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="CloseSourceSystemDialog">Abbrechen</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSourceSystemEdit">Uebernehmen</MudButton>
|
||||
<MudButton OnClick="CloseSourceSystemDialog">@T("Abbrechen", "Cancel")</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSourceSystemEdit">@T("Uebernehmen", "Apply")</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Wechselkurse</MudText>
|
||||
<MudText Typo="Typo.h5" Class="mb-2">@T("Wechselkurse", "Exchange rates")</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudText Typo="Typo.body2" Class="mb-3">
|
||||
Diese Kurstabelle wird von der Transformation <b>ConvertCurrency</b> verwendet. Gleiche Waehrung rechnet automatisch mit Faktor 1.
|
||||
@((MarkupString)T("Diese Kurstabelle wird von der Transformation <b>ConvertCurrency</b> verwendet. Gleiche Waehrung rechnet automatisch mit Faktor 1.",
|
||||
"This rate table is used by the <b>ConvertCurrency</b> transformation. Same-currency conversion automatically uses factor 1."))
|
||||
</MudText>
|
||||
<MudStack Row Spacing="2" Class="mb-3">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="AddExchangeRate"
|
||||
StartIcon="@Icons.Material.Filled.Add">
|
||||
Kurs hinzufuegen
|
||||
@T("Kurs hinzufuegen", "Add rate")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="RefreshEcbRates"
|
||||
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_refreshingExchangeRates">
|
||||
@(_refreshingExchangeRates ? "Aktualisiere ECB-Kurse..." : "Refresh Kurse")
|
||||
@(_refreshingExchangeRates ? T("Aktualisiere ECB-Kurse...", "Refreshing ECB rates...") : T("Refresh Kurse", "Refresh rates"))
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveExchangeRates"
|
||||
StartIcon="@Icons.Material.Filled.Save">
|
||||
Kurse speichern
|
||||
@T("Kurse speichern", "Save rates")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudTable Items="_exchangeRates" Hover="true" Breakpoint="Breakpoint.Md">
|
||||
<HeaderContent>
|
||||
<MudTh>Von</MudTh>
|
||||
<MudTh>Nach</MudTh>
|
||||
<MudTh>Kurs</MudTh>
|
||||
<MudTh>Gueltig ab</MudTh>
|
||||
<MudTh>Gueltig bis</MudTh>
|
||||
<MudTh>Notiz</MudTh>
|
||||
<MudTh>Aktiv</MudTh>
|
||||
<MudTh>@T("Von", "From")</MudTh>
|
||||
<MudTh>@T("Nach", "To")</MudTh>
|
||||
<MudTh>@T("Kurs", "Rate")</MudTh>
|
||||
<MudTh>@T("Gueltig ab", "Valid from")</MudTh>
|
||||
<MudTh>@T("Gueltig bis", "Valid to")</MudTh>
|
||||
<MudTh>@T("Notiz", "Note")</MudTh>
|
||||
<MudTh>@T("Aktiv", "Active")</MudTh>
|
||||
<MudTh></MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
@@ -253,66 +257,121 @@
|
||||
</MudPaper>
|
||||
|
||||
@* Export Settings *@
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Export Einstellungen</MudText>
|
||||
<MudText Typo="Typo.h5" Class="mb-2">@T("Export Einstellungen", "Export settings")</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField @bind-Value="_exportSettings.DateFilter" Label="Datum-Filter (ab)"
|
||||
HelperText="Format: yyyy-MM-dd" />
|
||||
<MudTextField @bind-Value="_exportSettings.DateFilter" Label="@T("Datum-Filter (ab)", "Date filter from")"
|
||||
HelperText="@T("Format: yyyy-MM-dd", "Format: yyyy-MM-dd")" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudNumericField @bind-Value="_exportSettings.TimerHour" Label="Timer Stunde" Min="0" Max="23" />
|
||||
<MudNumericField @bind-Value="_exportSettings.TimerHour" Label="@T("Timer Stunde", "Timer hour")" Min="0" Max="23" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudNumericField @bind-Value="_exportSettings.TimerMinute" Label="Timer Minute" Min="0" Max="59" />
|
||||
<MudNumericField @bind-Value="_exportSettings.TimerMinute" Label="@T("Timer Minute", "Timer minute")" Min="0" Max="59" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudSwitch @bind-Value="_exportSettings.TimerEnabled" Label="Timer aktiviert" Color="Color.Primary" />
|
||||
<MudSwitch @bind-Value="_exportSettings.TimerEnabled" Label="@T("Timer aktiviert", "Timer enabled")" Color="Color.Primary" />
|
||||
</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>
|
||||
Label="@T("Wechselkurse anwenden auf", "Apply exchange rates to")"
|
||||
HelperText="@T("Datumsfeld fuer Kursgueltigkeit in Management-Analysen.", "Date field for rate validity in management analyses.")">
|
||||
<MudSelectItem Value="@ExchangeRateDateFields.PostingDate">@T("PostingDate / Buchungsdatum", "PostingDate / posting date")</MudSelectItem>
|
||||
<MudSelectItem Value="@ExchangeRateDateFields.InvoiceDate">@T("InvoiceDate / Rechnungsdatum", "InvoiceDate / invoice date")</MudSelectItem>
|
||||
<MudSelectItem Value="@ExchangeRateDateFields.ExtractionDate">@T("ExtractionDate / Extraktionsdatum", "ExtractionDate / extraction date")</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudSwitch @bind-Value="_exportSettings.DebugLoggingEnabled" Label="Debug Live-Logging" Color="Color.Warning" />
|
||||
<MudSwitch @bind-Value="_exportSettings.DebugLoggingEnabled" Label="@T("Debug Live-Logging", "Debug live logging")" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.caption">
|
||||
Schreibt zusätzliche technische Fortschrittsmeldungen für HANA- und SAP-Lesevorgänge ins Dashboard und in die Logs.
|
||||
@T("Schreibt zusaetzliche technische Fortschrittsmeldungen fuer HANA- und SAP-Lesevorgaenge ins Dashboard und in die Logs.",
|
||||
"Writes additional technical progress messages for HANA and SAP reads to the dashboard and logs.")
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="_exportSettings.LocalSiteExportFolder" Label="Lokaler Standardpfad Standort-Dateien"
|
||||
HelperText="Wenn leer, wird ./output unter dem Programmverzeichnis verwendet." />
|
||||
<MudTextField @bind-Value="_exportSettings.LocalSiteExportFolder" Label="@T("Lokaler Standardpfad Standort-Dateien", "Local default path for site files")"
|
||||
HelperText="@T("Wenn leer, wird ./output unter dem Programmverzeichnis verwendet.", "If empty, ./output under the application directory is used.")" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="_exportSettings.LocalConsolidatedExportFolder" Label="Lokaler Pfad Zentrale Datei"
|
||||
HelperText="Optional. Wenn leer, wird der Standardpfad der Standort-Dateien verwendet." />
|
||||
<MudTextField @bind-Value="_exportSettings.LocalConsolidatedExportFolder" Label="@T("Lokaler Pfad Zentrale Datei", "Local path for central file")"
|
||||
HelperText="@T("Optional. Wenn leer, wird der Standardpfad der Standort-Dateien verwendet.", "Optional. If empty, the default path for site files is used.")" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<div class="audit-csv-settings">
|
||||
<div class="audit-csv-header">
|
||||
<MudIcon Icon="@Icons.Material.Filled.RuleFolder" Color="Color.Info" Size="Size.Medium" />
|
||||
<div>
|
||||
<MudText Typo="Typo.h6">@T("Audit-CSV / nachvollziehbarer Datenfluss", "Audit CSV / traceable data flow")</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Fuer Finance und Wirtschaftspruefung: lesbare Standort-CSV nach Mapping und Konvertierung, optional als Quelle fuer zentrale Auswertungen.",
|
||||
"For finance and auditors: readable site CSV after mapping and conversion, optionally as the source for central analyses.")
|
||||
</MudText>
|
||||
</div>
|
||||
</div>
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" md="4">
|
||||
<MudSwitch @bind-Value="_exportSettings.AuditCsvEnabled" Label="@T("Audit-CSV je Standort schreiben", "Write audit CSV per site")" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.caption">
|
||||
@T("Schreibt beim Laenderexport je Standort eine Sales_ProcessedMergeInput_*.csv mit den transformierten Daten.",
|
||||
"Writes one Sales_ProcessedMergeInput_*.csv per site during country export with the transformed data.")
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudSwitch @bind-Value="_exportSettings.UseAuditCsvAsCentralSource" Label="@T("Zentrale Auswertung aus Audit-CSV", "Central analysis from audit CSV")" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.caption">
|
||||
@T("Dashboard, zentrale Excel-Datei und Finance-Auswertungen lesen die neuesten Standort-CSV-Dateien statt CentralSalesRecords.",
|
||||
"Dashboard, central Excel file and finance analyses read the latest site CSV files instead of CentralSalesRecords.")
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Filled">
|
||||
@((MarkupString)T("Audit-CSV wird immer im gleichen Ordner wie die lokalen Standort-Dateien abgelegt. Der Pfad wird oben bei <b>Lokaler Standardpfad Standort-Dateien</b> gesetzt.",
|
||||
"Audit CSV is always stored in the same folder as the local site files. The path is set above under <b>Local default path for site files</b>."))
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</div>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveExportSettings"
|
||||
StartIcon="@Icons.Material.Filled.Save">
|
||||
Speichern
|
||||
@T("Speichern", "Save")
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@* Filename Preview *@
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Dateiname Vorschau</MudText>
|
||||
<MudText Typo="Typo.h5" Class="mb-2">@T("Dateiname Vorschau", "Filename preview")</MudText>
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.body1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.InsertDriveFile" Size="Size.Small" Class="mr-1" />
|
||||
Sales_{"{TSC}"}_{DateTime.Now:yyyy-MM-dd}.xlsx
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mt-1">
|
||||
Beispiel: Sales_TRFR_@(DateTime.Now.ToString("yyyy-MM-dd")).xlsx
|
||||
@T("Beispiel:", "Example:") Sales_TRFR_@(DateTime.Now.ToString("yyyy-MM-dd")).xlsx
|
||||
/ Sales_ProcessedMergeInput_TRFR_@(DateTime.Now.ToString("yyyy-MM-dd")).csv
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
|
||||
<style>
|
||||
.audit-csv-settings {
|
||||
background: #e8f3ff;
|
||||
border: 1px solid #90caf9;
|
||||
border-left: 6px solid #1976d2;
|
||||
border-radius: 8px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.audit-csv-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private SharePointConfig _spConfig = new();
|
||||
private ExportSettings _exportSettings = new();
|
||||
@@ -341,7 +400,7 @@
|
||||
private async Task SaveSharePoint()
|
||||
{
|
||||
await SettingsPageActions.SaveSharePointAsync(_spConfig);
|
||||
Snackbar.Add("SharePoint Konfiguration gespeichert", Severity.Success);
|
||||
Snackbar.Add(T("SharePoint Konfiguration gespeichert", "SharePoint configuration saved"), Severity.Success);
|
||||
}
|
||||
|
||||
private async Task TestSharePoint()
|
||||
@@ -350,11 +409,11 @@
|
||||
try
|
||||
{
|
||||
_sharePointTestPreview = await SettingsPageActions.BuildSharePointTestPreviewAsync(_spConfig);
|
||||
Snackbar.Add("SharePoint Verbindung erfolgreich!", Severity.Success);
|
||||
Snackbar.Add(T("SharePoint Verbindung erfolgreich!", "SharePoint connection successful!"), Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Verbindung fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||
Snackbar.Add($"{T("Verbindung fehlgeschlagen", "Connection failed")}: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -365,7 +424,7 @@
|
||||
private async Task SaveExportSettings()
|
||||
{
|
||||
await SettingsPageActions.SaveExportSettingsAsync(_exportSettings);
|
||||
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
|
||||
Snackbar.Add(T("Export Einstellungen gespeichert", "Export settings saved"), Severity.Success);
|
||||
}
|
||||
|
||||
private void AddSourceSystem()
|
||||
@@ -407,13 +466,13 @@
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_editingSourceSystem.Code) || string.IsNullOrWhiteSpace(_editingSourceSystem.DisplayName))
|
||||
{
|
||||
Snackbar.Add("Code und Name fuer das Quellsystem sind Pflicht.", Severity.Warning);
|
||||
Snackbar.Add(T("Code und Name fuer das Quellsystem sind Pflicht.", "Code and name are required for the source system."), Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sourceSystems.Any(x => x.Id != _editingSourceSystem.Id && x.Code == _editingSourceSystem.Code))
|
||||
{
|
||||
Snackbar.Add($"Quellsystem-Code doppelt vorhanden: {_editingSourceSystem.Code}", Severity.Warning);
|
||||
Snackbar.Add($"{T("Quellsystem-Code doppelt vorhanden", "Duplicate source-system code")}: {_editingSourceSystem.Code}", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -455,7 +514,7 @@
|
||||
try
|
||||
{
|
||||
_sourceSystems = await SettingsPageActions.SaveSourceSystemsAsync(_sourceSystems);
|
||||
Snackbar.Add("Quellsysteme gespeichert", Severity.Success);
|
||||
Snackbar.Add(T("Quellsysteme gespeichert", "Source systems saved"), Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -483,7 +542,7 @@
|
||||
private async Task SaveExchangeRates()
|
||||
{
|
||||
_exchangeRates = await SettingsPageActions.SaveExchangeRatesAsync(_exchangeRates);
|
||||
Snackbar.Add("Wechselkurse gespeichert", Severity.Success);
|
||||
Snackbar.Add(T("Wechselkurse gespeichert", "Exchange rates saved"), Severity.Success);
|
||||
}
|
||||
|
||||
private async Task RefreshEcbRates()
|
||||
@@ -496,11 +555,11 @@
|
||||
{
|
||||
var result = await SettingsPageActions.RefreshEcbRatesAsync();
|
||||
_exchangeRates = result.ExchangeRates;
|
||||
Snackbar.Add($"ECB-Kurse aktualisiert: {result.ImportedCount} Kurse vom {result.RateDate:yyyy-MM-dd}.", Severity.Success);
|
||||
Snackbar.Add($"{T("ECB-Kurse aktualisiert", "ECB rates refreshed")}: {result.ImportedCount} {T("Kurse vom", "rates from")} {result.RateDate:yyyy-MM-dd}.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"ECB-Kursimport fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||
Snackbar.Add($"{T("ECB-Kursimport fehlgeschlagen", "ECB rate import failed")}: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -520,11 +579,11 @@
|
||||
var suffix = _includeSecretsInExport ? "with-secrets" : "without-secrets";
|
||||
var fileName = $"trafag-config-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{suffix}.json";
|
||||
await JS.InvokeVoidAsync("trafagDownload.saveTextFile", fileName, json, "application/json;charset=utf-8");
|
||||
Snackbar.Add("Konfiguration exportiert", Severity.Success);
|
||||
Snackbar.Add(T("Konfiguration exportiert", "Configuration exported"), Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Export fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||
Snackbar.Add($"{T("Export fehlgeschlagen", "Export failed")}: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -549,11 +608,11 @@
|
||||
_exportSettings = state.ExportSettings;
|
||||
_sourceSystems = state.SourceSystems;
|
||||
_exchangeRates = state.ExchangeRates;
|
||||
Snackbar.Add("Konfiguration importiert", Severity.Success);
|
||||
Snackbar.Add(T("Konfiguration importiert", "Configuration imported"), Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Import fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||
Snackbar.Add($"{T("Import fehlgeschlagen", "Import failed")}: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -566,7 +625,7 @@
|
||||
var definition = _sourceSystems.FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase));
|
||||
if (definition is null)
|
||||
{
|
||||
Snackbar.Add($"Quellsystem '{sourceSystem}' nicht gefunden.", Severity.Warning);
|
||||
Snackbar.Add($"{T("Quellsystem nicht gefunden", "Source system not found")}: {sourceSystem}", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -609,5 +668,7 @@
|
||||
=> string.IsNullOrWhiteSpace(definition.CentralUsername) ? "-" : definition.CentralUsername;
|
||||
|
||||
private static string NormalizeConfigValue(string? value) => Services.SettingsPageService.NormalizeConfigValue(value);
|
||||
|
||||
private string T(string german, string english) => UiText.Text(german, english);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ public class ConfigTransferExportSettings
|
||||
public bool DebugLoggingEnabled { get; set; }
|
||||
public string LocalSiteExportFolder { get; set; } = string.Empty;
|
||||
public string LocalConsolidatedExportFolder { get; set; } = string.Empty;
|
||||
public bool AuditCsvEnabled { get; set; } = true;
|
||||
public bool UseAuditCsvAsCentralSource { get; set; }
|
||||
public string LocalAuditCsvFolder { get; set; } = string.Empty;
|
||||
public string ExchangeRateDateField { get; set; } = ExchangeRateDateFields.PostingDate;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ public class ExportSettings
|
||||
public bool DebugLoggingEnabled { get; set; }
|
||||
public string LocalSiteExportFolder { get; set; } = string.Empty;
|
||||
public string LocalConsolidatedExportFolder { get; set; } = string.Empty;
|
||||
public bool AuditCsvEnabled { get; set; } = true;
|
||||
public bool UseAuditCsvAsCentralSource { get; set; }
|
||||
public string LocalAuditCsvFolder { get; set; } = string.Empty;
|
||||
public string ExchangeRateDateField { get; set; } = ExchangeRateDateFields.PostingDate;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,9 +80,11 @@ builder.Services.AddSingleton<IAppEventLogService, AppEventLogService>();
|
||||
builder.Services.AddSingleton<IManagementCockpitService, ManagementCockpitService>();
|
||||
builder.Services.AddSingleton<IHrKpiService, HrKpiService>();
|
||||
builder.Services.AddSingleton<IManualExcelImportService, ManualExcelImportService>();
|
||||
builder.Services.AddSingleton<IExportAuditCsvService, ExportAuditCsvService>();
|
||||
builder.Services.AddSingleton<IConsolidatedExportService, ConsolidatedExportService>();
|
||||
builder.Services.AddSingleton<IExportLogService, ExportLogService>();
|
||||
builder.Services.AddSingleton<ICentralSalesRecordService, CentralSalesRecordService>();
|
||||
builder.Services.AddSingleton<ICentralSalesDataProvider, CentralSalesDataProvider>();
|
||||
builder.Services.AddSingleton<IConfigTransferService, ConfigTransferService>();
|
||||
builder.Services.AddSingleton<IFinanceReconciliationService, FinanceReconciliationService>();
|
||||
builder.Services.AddSingleton<IDatabaseSchemaMaintenanceService, DatabaseSchemaMaintenanceService>();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface ICentralSalesDataProvider
|
||||
{
|
||||
Task<List<SalesRecord>> GetRecordsAsync();
|
||||
Task<bool> UsesAuditCsvAsync();
|
||||
}
|
||||
|
||||
public sealed class CentralSalesDataProvider : ICentralSalesDataProvider
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly ICentralSalesRecordService _centralSalesRecordService;
|
||||
private readonly IExportAuditCsvService _auditCsvService;
|
||||
|
||||
public CentralSalesDataProvider(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
ICentralSalesRecordService centralSalesRecordService,
|
||||
IExportAuditCsvService auditCsvService)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_centralSalesRecordService = centralSalesRecordService;
|
||||
_auditCsvService = auditCsvService;
|
||||
}
|
||||
|
||||
public async Task<List<SalesRecord>> GetRecordsAsync()
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.AsNoTracking().FirstOrDefaultAsync() ?? new ExportSettings();
|
||||
if (!settings.UseAuditCsvAsCentralSource)
|
||||
return await _centralSalesRecordService.GetAllAsync();
|
||||
|
||||
var records = await _auditCsvService.ReadLatestSiteAuditCsvRecordsAsync(settings);
|
||||
if (records.Count == 0)
|
||||
{
|
||||
var directory = _auditCsvService.ResolveAuditCsvDirectory(settings);
|
||||
throw new InvalidOperationException(
|
||||
$"Audit-CSV ist als zentrale Quelle aktiv, aber im Ordner '{directory}' wurden keine Sales_ProcessedMergeInput_*.csv-Dateien gefunden.");
|
||||
}
|
||||
|
||||
return records
|
||||
.OrderBy(r => r.Land)
|
||||
.ThenBy(r => r.Tsc)
|
||||
.ThenByDescending(r => r.InvoiceDate ?? DateTime.MinValue)
|
||||
.ThenBy(r => r.InvoiceNumber)
|
||||
.ThenBy(r => r.PositionOnInvoice)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<bool> UsesAuditCsvAsync()
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.AsNoTracking().FirstOrDefaultAsync() ?? new ExportSettings();
|
||||
return settings.UseAuditCsvAsCentralSource;
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
||||
.ThenBy(r => r.Tsc)
|
||||
.Select(r => new SalesRecord
|
||||
{
|
||||
SourceSystem = r.SourceSystem,
|
||||
ExtractionDate = r.ExtractionDate,
|
||||
Tsc = r.Tsc,
|
||||
DocumentEntry = r.DocumentEntry,
|
||||
|
||||
@@ -71,6 +71,9 @@ public class ConfigTransferService : IConfigTransferService
|
||||
DebugLoggingEnabled = exportSettings.DebugLoggingEnabled,
|
||||
LocalSiteExportFolder = exportSettings.LocalSiteExportFolder,
|
||||
LocalConsolidatedExportFolder = exportSettings.LocalConsolidatedExportFolder,
|
||||
AuditCsvEnabled = exportSettings.AuditCsvEnabled,
|
||||
UseAuditCsvAsCentralSource = exportSettings.UseAuditCsvAsCentralSource,
|
||||
LocalAuditCsvFolder = exportSettings.LocalAuditCsvFolder,
|
||||
ExchangeRateDateField = SettingsPageService.NormalizeExchangeRateDateField(exportSettings.ExchangeRateDateField)
|
||||
},
|
||||
SourceSystemDefinitions = sourceSystems.Select(system => new ConfigTransferSourceSystemDefinition
|
||||
@@ -285,6 +288,9 @@ public class ConfigTransferService : IConfigTransferService
|
||||
DebugLoggingEnabled = importedSettings.DebugLoggingEnabled,
|
||||
LocalSiteExportFolder = importedSettings.LocalSiteExportFolder,
|
||||
LocalConsolidatedExportFolder = importedSettings.LocalConsolidatedExportFolder,
|
||||
AuditCsvEnabled = importedSettings.AuditCsvEnabled,
|
||||
UseAuditCsvAsCentralSource = importedSettings.UseAuditCsvAsCentralSource,
|
||||
LocalAuditCsvFolder = importedSettings.LocalAuditCsvFolder,
|
||||
ExchangeRateDateField = SettingsPageService.NormalizeExchangeRateDateField(importedSettings.ExchangeRateDateField)
|
||||
});
|
||||
|
||||
|
||||
@@ -7,25 +7,25 @@ namespace TrafagSalesExporter.Services;
|
||||
public class ConsolidatedExportService : IConsolidatedExportService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly ICentralSalesRecordService _centralSalesRecordService;
|
||||
private readonly ICentralSalesDataProvider _centralSalesDataProvider;
|
||||
private readonly IExcelExportService _excelService;
|
||||
private readonly ISharePointUploadService _sharePointService;
|
||||
|
||||
public ConsolidatedExportService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
ICentralSalesRecordService centralSalesRecordService,
|
||||
ICentralSalesDataProvider centralSalesDataProvider,
|
||||
IExcelExportService excelService,
|
||||
ISharePointUploadService sharePointService)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_centralSalesRecordService = centralSalesRecordService;
|
||||
_centralSalesDataProvider = centralSalesDataProvider;
|
||||
_excelService = excelService;
|
||||
_sharePointService = sharePointService;
|
||||
}
|
||||
|
||||
public async Task<string?> ExportAsync()
|
||||
{
|
||||
var consolidatedRecords = await _centralSalesRecordService.GetAllAsync();
|
||||
var consolidatedRecords = await _centralSalesDataProvider.GetRecordsAsync();
|
||||
if (consolidatedRecords.Count == 0)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ CREATE TABLE ExportSettings (
|
||||
DebugLoggingEnabled INTEGER NOT NULL DEFAULT 0,
|
||||
LocalSiteExportFolder TEXT NOT NULL DEFAULT '',
|
||||
LocalConsolidatedExportFolder TEXT NOT NULL DEFAULT '',
|
||||
AuditCsvEnabled INTEGER NOT NULL DEFAULT 1,
|
||||
UseAuditCsvAsCentralSource INTEGER NOT NULL DEFAULT 0,
|
||||
LocalAuditCsvFolder TEXT NOT NULL DEFAULT '',
|
||||
ExchangeRateDateField TEXT NOT NULL DEFAULT 'PostingDate'
|
||||
);";
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ 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", "AuditCsvEnabled", "INTEGER NOT NULL DEFAULT 1");
|
||||
AddColumnIfMissing(db, "ExportSettings", "UseAuditCsvAsCentralSource", "INTEGER NOT NULL DEFAULT 0");
|
||||
AddColumnIfMissing(db, "ExportSettings", "LocalAuditCsvFolder", "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 ''");
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IExportAuditCsvService
|
||||
{
|
||||
Task<string?> WriteSiteAuditCsvAsync(
|
||||
Site site,
|
||||
ExportSettings settings,
|
||||
string sourceSystem,
|
||||
string fallbackOutputDirectory,
|
||||
IReadOnlyList<SalesRecord> records);
|
||||
|
||||
Task<List<SalesRecord>> ReadLatestSiteAuditCsvRecordsAsync(ExportSettings settings);
|
||||
|
||||
string ResolveAuditCsvDirectory(ExportSettings settings, string? fallbackOutputDirectory = null);
|
||||
}
|
||||
|
||||
public sealed class ExportAuditCsvService : IExportAuditCsvService
|
||||
{
|
||||
private const char Delimiter = ';';
|
||||
private const string ProcessedMergeInputFilePrefix = "Sales_ProcessedMergeInput_";
|
||||
private const string LegacyFilePrefix = "Sales_";
|
||||
|
||||
private static readonly string[] Headers =
|
||||
[
|
||||
"SourceSystem",
|
||||
"ExtractionDate",
|
||||
"TSC",
|
||||
"SourceLineId",
|
||||
"DocumentEntry",
|
||||
"InvoiceNumber",
|
||||
"PositionOnInvoice",
|
||||
"Material",
|
||||
"Name",
|
||||
"ProductGroup",
|
||||
"ProductHierarchyCode",
|
||||
"ProductHierarchyText",
|
||||
"ProductFamilyCode",
|
||||
"ProductFamilyText",
|
||||
"ProductDivisionCode",
|
||||
"ProductDivisionText",
|
||||
"ProductMappingAssigned",
|
||||
"Quantity",
|
||||
"SupplierNumber",
|
||||
"SupplierName",
|
||||
"SupplierCountry",
|
||||
"CustomerNumber",
|
||||
"CustomerName",
|
||||
"CustomerCountry",
|
||||
"CustomerIndustry",
|
||||
"StandardCost",
|
||||
"StandardCostCurrency",
|
||||
"PurchaseOrderNumber",
|
||||
"SalesPriceValue",
|
||||
"SalesCurrency",
|
||||
"DocumentCurrency",
|
||||
"DocumentTotalForeignCurrency",
|
||||
"DocumentTotalLocalCurrency",
|
||||
"VatSumForeignCurrency",
|
||||
"VatSumLocalCurrency",
|
||||
"DocumentRate",
|
||||
"CompanyCurrency",
|
||||
"Incoterms2020",
|
||||
"SalesResponsibleEmployee",
|
||||
"PostingDate",
|
||||
"InvoiceDate",
|
||||
"OrderDate",
|
||||
"Land",
|
||||
"DocumentType"
|
||||
];
|
||||
|
||||
public async Task<string?> WriteSiteAuditCsvAsync(
|
||||
Site site,
|
||||
ExportSettings settings,
|
||||
string sourceSystem,
|
||||
string fallbackOutputDirectory,
|
||||
IReadOnlyList<SalesRecord> records)
|
||||
{
|
||||
if (!settings.AuditCsvEnabled)
|
||||
return null;
|
||||
|
||||
var directory = ResolveAuditCsvDirectory(settings, fallbackOutputDirectory);
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var tsc = string.IsNullOrWhiteSpace(site.TSC) ? "UNKNOWN" : site.TSC.Trim();
|
||||
var fileName = $"{ProcessedMergeInputFilePrefix}{SanitizeFileNamePart(tsc)}_{DateTime.UtcNow:yyyy-MM-dd}.csv";
|
||||
var path = Path.Combine(directory, fileName);
|
||||
|
||||
await using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
|
||||
|
||||
await writer.WriteLineAsync(string.Join(Delimiter, Headers.Select(Escape)));
|
||||
foreach (var record in records)
|
||||
{
|
||||
await writer.WriteLineAsync(string.Join(Delimiter, BuildRow(site, sourceSystem, record).Select(Escape)));
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public async Task<List<SalesRecord>> ReadLatestSiteAuditCsvRecordsAsync(ExportSettings settings)
|
||||
{
|
||||
var directory = ResolveAuditCsvDirectory(settings);
|
||||
if (!Directory.Exists(directory))
|
||||
return [];
|
||||
|
||||
var latestFiles = EnumerateAuditCsvFiles(directory)
|
||||
.Select(path => new { Path = path, Tsc = ResolveTscFromFileName(path) })
|
||||
.Where(file => !string.IsNullOrWhiteSpace(file.Tsc))
|
||||
.GroupBy(file => file.Tsc, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group
|
||||
.OrderByDescending(file => File.GetLastWriteTimeUtc(file.Path))
|
||||
.ThenByDescending(file => IsProcessedMergeInputFile(file.Path))
|
||||
.ThenByDescending(file => Path.GetFileName(file.Path), StringComparer.OrdinalIgnoreCase)
|
||||
.First()
|
||||
.Path)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var records = new List<SalesRecord>();
|
||||
foreach (var file in latestFiles)
|
||||
records.AddRange(await ReadFileAsync(file));
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
public string ResolveAuditCsvDirectory(ExportSettings settings, string? fallbackOutputDirectory = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(fallbackOutputDirectory))
|
||||
return fallbackOutputDirectory.Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(settings.LocalSiteExportFolder))
|
||||
return settings.LocalSiteExportFolder.Trim();
|
||||
|
||||
return Path.Combine(AppContext.BaseDirectory, "output");
|
||||
}
|
||||
|
||||
private static IEnumerable<string> BuildRow(Site site, string sourceSystem, SalesRecord record)
|
||||
{
|
||||
yield return string.IsNullOrWhiteSpace(record.SourceSystem) ? sourceSystem : record.SourceSystem;
|
||||
yield return FormatDate(record.ExtractionDate);
|
||||
yield return record.Tsc;
|
||||
yield return record.SourceLineId;
|
||||
yield return FormatInt(record.DocumentEntry);
|
||||
yield return record.InvoiceNumber;
|
||||
yield return FormatInt(record.PositionOnInvoice);
|
||||
yield return record.Material;
|
||||
yield return record.Name;
|
||||
yield return record.ProductGroup;
|
||||
yield return record.ProductHierarchyCode;
|
||||
yield return record.ProductHierarchyText;
|
||||
yield return record.ProductFamilyCode;
|
||||
yield return record.ProductFamilyText;
|
||||
yield return record.ProductDivisionCode;
|
||||
yield return record.ProductDivisionText;
|
||||
yield return record.ProductMappingAssigned;
|
||||
yield return FormatDecimal(record.Quantity);
|
||||
yield return record.SupplierNumber;
|
||||
yield return record.SupplierName;
|
||||
yield return record.SupplierCountry;
|
||||
yield return record.CustomerNumber;
|
||||
yield return record.CustomerName;
|
||||
yield return record.CustomerCountry;
|
||||
yield return record.CustomerIndustry;
|
||||
yield return FormatDecimal(record.StandardCost);
|
||||
yield return record.StandardCostCurrency;
|
||||
yield return record.PurchaseOrderNumber;
|
||||
yield return FormatDecimal(record.SalesPriceValue);
|
||||
yield return record.SalesCurrency;
|
||||
yield return record.DocumentCurrency;
|
||||
yield return FormatDecimal(record.DocumentTotalForeignCurrency);
|
||||
yield return FormatDecimal(record.DocumentTotalLocalCurrency);
|
||||
yield return FormatDecimal(record.VatSumForeignCurrency);
|
||||
yield return FormatDecimal(record.VatSumLocalCurrency);
|
||||
yield return FormatDecimal(record.DocumentRate);
|
||||
yield return record.CompanyCurrency;
|
||||
yield return record.Incoterms2020;
|
||||
yield return record.SalesResponsibleEmployee;
|
||||
yield return FormatNullableDate(record.PostingDate);
|
||||
yield return FormatNullableDate(record.InvoiceDate);
|
||||
yield return FormatNullableDate(record.OrderDate);
|
||||
yield return string.IsNullOrWhiteSpace(record.Land) ? site.Land : record.Land;
|
||||
yield return record.DocumentType;
|
||||
}
|
||||
|
||||
private static async Task<List<SalesRecord>> ReadFileAsync(string path)
|
||||
{
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||
|
||||
var headerLine = await reader.ReadLineAsync();
|
||||
if (string.IsNullOrWhiteSpace(headerLine))
|
||||
return [];
|
||||
|
||||
var headers = ParseLine(headerLine)
|
||||
.Select((value, index) => new { Header = NormalizeHeader(value), Index = index })
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Header))
|
||||
.ToDictionary(x => x.Header, x => x.Index, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var records = new List<SalesRecord>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
var values = ParseLine(line);
|
||||
records.Add(new SalesRecord
|
||||
{
|
||||
SourceSystem = GetText(values, headers, "SourceSystem"),
|
||||
ExtractionDate = GetDate(values, headers, "ExtractionDate") ?? File.GetLastWriteTime(path),
|
||||
Tsc = GetText(values, headers, "TSC"),
|
||||
SourceLineId = GetText(values, headers, "SourceLineId"),
|
||||
DocumentEntry = GetInt(values, headers, "DocumentEntry"),
|
||||
InvoiceNumber = GetText(values, headers, "InvoiceNumber"),
|
||||
PositionOnInvoice = GetInt(values, headers, "PositionOnInvoice"),
|
||||
Material = GetText(values, headers, "Material"),
|
||||
Name = GetText(values, headers, "Name"),
|
||||
ProductGroup = GetText(values, headers, "ProductGroup"),
|
||||
ProductHierarchyCode = GetText(values, headers, "ProductHierarchyCode"),
|
||||
ProductHierarchyText = GetText(values, headers, "ProductHierarchyText"),
|
||||
ProductFamilyCode = GetText(values, headers, "ProductFamilyCode"),
|
||||
ProductFamilyText = GetText(values, headers, "ProductFamilyText"),
|
||||
ProductDivisionCode = GetText(values, headers, "ProductDivisionCode"),
|
||||
ProductDivisionText = GetText(values, headers, "ProductDivisionText"),
|
||||
ProductMappingAssigned = GetText(values, headers, "ProductMappingAssigned"),
|
||||
Quantity = GetDecimal(values, headers, "Quantity"),
|
||||
SupplierNumber = GetText(values, headers, "SupplierNumber"),
|
||||
SupplierName = GetText(values, headers, "SupplierName"),
|
||||
SupplierCountry = GetText(values, headers, "SupplierCountry"),
|
||||
CustomerNumber = GetText(values, headers, "CustomerNumber"),
|
||||
CustomerName = GetText(values, headers, "CustomerName"),
|
||||
CustomerCountry = GetText(values, headers, "CustomerCountry"),
|
||||
CustomerIndustry = GetText(values, headers, "CustomerIndustry"),
|
||||
StandardCost = GetDecimal(values, headers, "StandardCost"),
|
||||
StandardCostCurrency = GetText(values, headers, "StandardCostCurrency"),
|
||||
PurchaseOrderNumber = GetText(values, headers, "PurchaseOrderNumber"),
|
||||
SalesPriceValue = GetDecimal(values, headers, "SalesPriceValue"),
|
||||
SalesCurrency = GetText(values, headers, "SalesCurrency"),
|
||||
DocumentCurrency = GetText(values, headers, "DocumentCurrency"),
|
||||
DocumentTotalForeignCurrency = GetDecimal(values, headers, "DocumentTotalForeignCurrency"),
|
||||
DocumentTotalLocalCurrency = GetDecimal(values, headers, "DocumentTotalLocalCurrency"),
|
||||
VatSumForeignCurrency = GetDecimal(values, headers, "VatSumForeignCurrency"),
|
||||
VatSumLocalCurrency = GetDecimal(values, headers, "VatSumLocalCurrency"),
|
||||
DocumentRate = GetDecimal(values, headers, "DocumentRate"),
|
||||
CompanyCurrency = GetText(values, headers, "CompanyCurrency"),
|
||||
Incoterms2020 = GetText(values, headers, "Incoterms2020"),
|
||||
SalesResponsibleEmployee = GetText(values, headers, "SalesResponsibleEmployee"),
|
||||
PostingDate = GetDate(values, headers, "PostingDate"),
|
||||
InvoiceDate = GetDate(values, headers, "InvoiceDate"),
|
||||
OrderDate = GetDate(values, headers, "OrderDate"),
|
||||
Land = GetText(values, headers, "Land"),
|
||||
DocumentType = GetText(values, headers, "DocumentType")
|
||||
});
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
private static string ResolveTscFromFileName(string path)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
if (name.StartsWith(ProcessedMergeInputFilePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
return ResolveTscFromSuffix(name[ProcessedMergeInputFilePrefix.Length..]);
|
||||
|
||||
if (name.StartsWith(LegacyFilePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
return ResolveTscFromSuffix(name[LegacyFilePrefix.Length..]);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string ResolveTscFromSuffix(string suffix)
|
||||
{
|
||||
var lastUnderscore = suffix.LastIndexOf('_');
|
||||
return lastUnderscore <= 0 ? suffix : suffix[..lastUnderscore];
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateAuditCsvFiles(string directory)
|
||||
=> Directory.EnumerateFiles(directory, $"{ProcessedMergeInputFilePrefix}*.csv", SearchOption.TopDirectoryOnly)
|
||||
.Concat(Directory.EnumerateFiles(directory, $"{LegacyFilePrefix}*.csv", SearchOption.TopDirectoryOnly)
|
||||
.Where(path => !IsProcessedMergeInputFile(path)));
|
||||
|
||||
private static bool IsProcessedMergeInputFile(string path)
|
||||
=> Path.GetFileName(path).StartsWith(ProcessedMergeInputFilePrefix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string SanitizeFileNamePart(string value)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
var chars = value.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray();
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
private static string Escape(string? value)
|
||||
{
|
||||
var text = (value ?? string.Empty)
|
||||
.Replace("\r\n", " ", StringComparison.Ordinal)
|
||||
.Replace('\r', ' ')
|
||||
.Replace('\n', ' ');
|
||||
if (text.Contains(Delimiter) || text.Contains('"') || text.Contains('\r') || text.Contains('\n'))
|
||||
return $"\"{text.Replace("\"", "\"\"")}\"";
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private static List<string> ParseLine(string line)
|
||||
{
|
||||
var values = new List<string>();
|
||||
var current = new StringBuilder();
|
||||
var inQuotes = false;
|
||||
|
||||
for (var i = 0; i < line.Length; i++)
|
||||
{
|
||||
var ch = line[i];
|
||||
if (ch == '"')
|
||||
{
|
||||
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
|
||||
{
|
||||
current.Append('"');
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == Delimiter && !inQuotes)
|
||||
{
|
||||
values.Add(current.ToString());
|
||||
current.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
current.Append(ch);
|
||||
}
|
||||
|
||||
values.Add(current.ToString());
|
||||
return values;
|
||||
}
|
||||
|
||||
private static string NormalizeHeader(string value)
|
||||
=> new(value.Where(char.IsLetterOrDigit).ToArray());
|
||||
|
||||
private static string GetText(IReadOnlyList<string> values, IReadOnlyDictionary<string, int> headers, string header)
|
||||
=> headers.TryGetValue(NormalizeHeader(header), out var index) && index >= 0 && index < values.Count
|
||||
? values[index].Trim()
|
||||
: string.Empty;
|
||||
|
||||
private static int GetInt(IReadOnlyList<string> values, IReadOnlyDictionary<string, int> headers, string header)
|
||||
=> int.TryParse(GetText(values, headers, header), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
|
||||
? value
|
||||
: 0;
|
||||
|
||||
private static decimal GetDecimal(IReadOnlyList<string> values, IReadOnlyDictionary<string, int> headers, string header)
|
||||
{
|
||||
var text = GetText(values, headers, header);
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var invariant))
|
||||
return invariant;
|
||||
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("de-CH"), out var swiss))
|
||||
return swiss;
|
||||
|
||||
return 0m;
|
||||
}
|
||||
|
||||
private static DateTime? GetDate(IReadOnlyList<string> values, IReadOnlyDictionary<string, int> headers, string header)
|
||||
{
|
||||
var text = GetText(values, headers, header);
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return null;
|
||||
|
||||
if (DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var roundtrip))
|
||||
return roundtrip;
|
||||
|
||||
if (DateTime.TryParse(text, CultureInfo.GetCultureInfo("de-CH"), DateTimeStyles.AssumeLocal, out var swiss))
|
||||
return swiss;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string FormatInt(int value)
|
||||
=> value.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
private static string FormatDecimal(decimal value)
|
||||
=> value.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
private static string FormatDate(DateTime value)
|
||||
=> value.ToString("O", CultureInfo.InvariantCulture);
|
||||
|
||||
private static string FormatNullableDate(DateTime? value)
|
||||
=> value?.ToString("O", CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
}
|
||||
@@ -12,10 +12,19 @@ public interface IFinanceReconciliationService
|
||||
public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly ICentralSalesDataProvider? _centralSalesDataProvider;
|
||||
|
||||
public FinanceReconciliationService(IDbContextFactory<AppDbContext> dbFactory)
|
||||
: this(dbFactory, null)
|
||||
{
|
||||
}
|
||||
|
||||
public FinanceReconciliationService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
ICentralSalesDataProvider? centralSalesDataProvider)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_centralSalesDataProvider = centralSalesDataProvider;
|
||||
}
|
||||
|
||||
public async Task<List<NetSalesReferenceRow>> BuildNetSalesReferenceRowsAsync(int year = 2025)
|
||||
@@ -41,35 +50,7 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
financeRules = FinanceRuleEngine.CreateDefaultRules().ToList();
|
||||
var financeRuleEngine = new FinanceRuleEngine(financeRules);
|
||||
|
||||
var centralRecords = await db.CentralSalesRecords
|
||||
.AsNoTracking()
|
||||
.Select(r => new SalesRecord
|
||||
{
|
||||
Land = r.Land,
|
||||
Tsc = r.Tsc,
|
||||
DocumentEntry = r.DocumentEntry,
|
||||
InvoiceNumber = r.InvoiceNumber,
|
||||
PositionOnInvoice = r.PositionOnInvoice,
|
||||
Material = r.Material,
|
||||
Name = r.Name,
|
||||
Quantity = r.Quantity,
|
||||
DocumentType = r.DocumentType,
|
||||
PostingDate = r.PostingDate,
|
||||
InvoiceDate = r.InvoiceDate,
|
||||
ExtractionDate = r.ExtractionDate,
|
||||
CustomerNumber = r.CustomerNumber,
|
||||
CustomerName = r.CustomerName,
|
||||
SupplierCountry = r.SupplierCountry,
|
||||
SalesCurrency = r.SalesCurrency,
|
||||
DocumentCurrency = r.DocumentCurrency,
|
||||
CompanyCurrency = r.CompanyCurrency,
|
||||
SalesPriceValue = r.SalesPriceValue,
|
||||
DocumentTotalForeignCurrency = r.DocumentTotalForeignCurrency,
|
||||
DocumentTotalLocalCurrency = r.DocumentTotalLocalCurrency,
|
||||
VatSumForeignCurrency = r.VatSumForeignCurrency,
|
||||
VatSumLocalCurrency = r.VatSumLocalCurrency
|
||||
})
|
||||
.ToListAsync();
|
||||
var centralRecords = await LoadCentralRecordsAsync(db);
|
||||
|
||||
var centralRows = centralRecords
|
||||
.Select(record => ApplyFinanceRules(record, year, financeRuleEngine))
|
||||
@@ -165,6 +146,42 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<List<SalesRecord>> LoadCentralRecordsAsync(AppDbContext db)
|
||||
{
|
||||
if (_centralSalesDataProvider is not null)
|
||||
return await _centralSalesDataProvider.GetRecordsAsync();
|
||||
|
||||
return await db.CentralSalesRecords
|
||||
.AsNoTracking()
|
||||
.Select(r => new SalesRecord
|
||||
{
|
||||
Land = r.Land,
|
||||
Tsc = r.Tsc,
|
||||
DocumentEntry = r.DocumentEntry,
|
||||
InvoiceNumber = r.InvoiceNumber,
|
||||
PositionOnInvoice = r.PositionOnInvoice,
|
||||
Material = r.Material,
|
||||
Name = r.Name,
|
||||
Quantity = r.Quantity,
|
||||
DocumentType = r.DocumentType,
|
||||
PostingDate = r.PostingDate,
|
||||
InvoiceDate = r.InvoiceDate,
|
||||
ExtractionDate = r.ExtractionDate,
|
||||
CustomerNumber = r.CustomerNumber,
|
||||
CustomerName = r.CustomerName,
|
||||
SupplierCountry = r.SupplierCountry,
|
||||
SalesCurrency = r.SalesCurrency,
|
||||
DocumentCurrency = r.DocumentCurrency,
|
||||
CompanyCurrency = r.CompanyCurrency,
|
||||
SalesPriceValue = r.SalesPriceValue,
|
||||
DocumentTotalForeignCurrency = r.DocumentTotalForeignCurrency,
|
||||
DocumentTotalLocalCurrency = r.DocumentTotalLocalCurrency,
|
||||
VatSumForeignCurrency = r.VatSumForeignCurrency,
|
||||
VatSumLocalCurrency = r.VatSumLocalCurrency
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static NetSalesActualSourceRow? ApplyFinanceRules(SalesRecord record, int year, FinanceRuleEngine financeRuleEngine)
|
||||
{
|
||||
var referenceKey = ResolveReferenceKey(record.Land, record.Tsc);
|
||||
|
||||
@@ -9,16 +9,26 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly ICurrencyExchangeRateService _exchangeRateService;
|
||||
private readonly ICentralSalesDataProvider? _centralSalesDataProvider;
|
||||
|
||||
public ManagementCockpitService(IDbContextFactory<AppDbContext> dbFactory)
|
||||
: this(dbFactory, new CurrencyExchangeRateService(dbFactory))
|
||||
: this(dbFactory, new CurrencyExchangeRateService(dbFactory), null)
|
||||
{
|
||||
}
|
||||
|
||||
public ManagementCockpitService(IDbContextFactory<AppDbContext> dbFactory, ICurrencyExchangeRateService exchangeRateService)
|
||||
: this(dbFactory, exchangeRateService, null)
|
||||
{
|
||||
}
|
||||
|
||||
public ManagementCockpitService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
ICurrencyExchangeRateService exchangeRateService,
|
||||
ICentralSalesDataProvider? centralSalesDataProvider)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_exchangeRateService = exchangeRateService;
|
||||
_centralSalesDataProvider = centralSalesDataProvider;
|
||||
}
|
||||
|
||||
private static readonly List<ValueFieldDefinition> ValueFieldDefinitions =
|
||||
@@ -166,12 +176,12 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
|
||||
public async Task<List<int>> GetAvailableCentralYearsAsync()
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var years = await db.CentralSalesRecords
|
||||
var records = await LoadCentralRecordsAsync();
|
||||
var years = records
|
||||
.Select(r => r.InvoiceDate.HasValue ? r.InvoiceDate.Value.Year : r.ExtractionDate.Year)
|
||||
.Distinct()
|
||||
.OrderBy(x => x)
|
||||
.ToListAsync();
|
||||
.ToList();
|
||||
|
||||
return years;
|
||||
}
|
||||
@@ -186,7 +196,8 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
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 centralRecords = await LoadCentralRecordsAsync();
|
||||
var baseRows = centralRecords
|
||||
.Select(r => new CentralCockpitRow
|
||||
{
|
||||
SourceSystem = r.SourceSystem,
|
||||
@@ -204,7 +215,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
PeriodDate = r.InvoiceDate ?? r.ExtractionDate,
|
||||
ExchangeRateDate = r.ExtractionDate
|
||||
})
|
||||
.ToListAsync();
|
||||
.ToList();
|
||||
|
||||
foreach (var row in baseRows)
|
||||
row.ExchangeRateDate = ResolveExchangeRateDate(exchangeRateDateField, row.PostingDate, row.InvoiceDate, row.ExtractionDate);
|
||||
@@ -318,6 +329,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
public async Task<ManagementFinanceSummaryResult> AnalyzeFinanceSummaryAsync(int year, string? countryKey, string? currency)
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.AsNoTracking().FirstOrDefaultAsync() ?? new ExportSettings();
|
||||
var financeRules = await db.FinanceRules
|
||||
.AsNoTracking()
|
||||
.Where(rule => rule.IsActive)
|
||||
@@ -332,40 +344,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
.Where(rule => rule.IsActive)
|
||||
.ToListAsync();
|
||||
var financeRuleEngine = new FinanceRuleEngine(financeRules);
|
||||
var records = await db.CentralSalesRecords
|
||||
.AsNoTracking()
|
||||
.Select(r => new SalesRecord
|
||||
{
|
||||
SourceSystem = r.SourceSystem,
|
||||
Land = r.Land,
|
||||
Tsc = r.Tsc,
|
||||
DocumentEntry = r.DocumentEntry,
|
||||
InvoiceNumber = r.InvoiceNumber,
|
||||
PositionOnInvoice = r.PositionOnInvoice,
|
||||
Material = r.Material,
|
||||
Name = r.Name,
|
||||
ProductGroup = r.ProductGroup,
|
||||
ProductHierarchyCode = r.ProductHierarchyCode,
|
||||
ProductHierarchyText = r.ProductHierarchyText,
|
||||
ProductFamilyCode = r.ProductFamilyCode,
|
||||
ProductFamilyText = r.ProductFamilyText,
|
||||
ProductDivisionCode = r.ProductDivisionCode,
|
||||
ProductDivisionText = r.ProductDivisionText,
|
||||
ProductMappingAssigned = r.ProductMappingAssigned,
|
||||
Quantity = r.Quantity,
|
||||
SupplierCountry = r.SupplierCountry,
|
||||
CustomerNumber = r.CustomerNumber,
|
||||
CustomerName = r.CustomerName,
|
||||
SalesCurrency = r.SalesCurrency,
|
||||
DocumentCurrency = r.DocumentCurrency,
|
||||
CompanyCurrency = r.CompanyCurrency,
|
||||
SalesPriceValue = r.SalesPriceValue,
|
||||
DocumentType = r.DocumentType,
|
||||
PostingDate = r.PostingDate,
|
||||
InvoiceDate = r.InvoiceDate,
|
||||
ExtractionDate = r.ExtractionDate
|
||||
})
|
||||
.ToListAsync();
|
||||
var records = await LoadCentralRecordsAsync();
|
||||
|
||||
if (records.Count == 0)
|
||||
throw new InvalidOperationException("Die zentrale Tabelle enthaelt noch keine Datensaetze.");
|
||||
@@ -411,13 +390,43 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var references = await db.FinanceReferences
|
||||
.AsNoTracking()
|
||||
.Where(reference => reference.IsActive)
|
||||
.ToListAsync();
|
||||
var referenceByKey = references
|
||||
.Where(reference => reference.Year == year)
|
||||
.GroupBy(reference => reference.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group
|
||||
.Select(reference => new FinanceReferenceValue(
|
||||
reference.Key,
|
||||
reference.Label,
|
||||
reference.CheckValue ?? reference.LocalCurrencyValue))
|
||||
.FirstOrDefault(reference => reference.Value.HasValue),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var yearOptions = allRows
|
||||
.Select(row => row.Year)
|
||||
.Concat(references.Select(reference => reference.Year))
|
||||
.Distinct()
|
||||
.OrderBy(yearValue => yearValue)
|
||||
.ToList();
|
||||
if (year == 0)
|
||||
year = yearOptions.LastOrDefault();
|
||||
referenceByKey = references
|
||||
.Where(reference => reference.Year == year)
|
||||
.GroupBy(reference => reference.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group
|
||||
.Select(reference => new FinanceReferenceValue(
|
||||
reference.Key,
|
||||
reference.Label,
|
||||
reference.CheckValue ?? reference.LocalCurrencyValue))
|
||||
.FirstOrDefault(reference => reference.Value.HasValue),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var countryFilter = NormalizeOptionalFilter(countryKey);
|
||||
var currencyFilter = NormalizeOptionalFilter(currency);
|
||||
@@ -473,19 +482,8 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
notices.Insert(0, "Fuer die gewaehlten Finance-Filter gibt es keine Datensaetze im aktuellen Zentraldatenbestand.");
|
||||
}
|
||||
|
||||
var references = await db.FinanceReferences
|
||||
.AsNoTracking()
|
||||
.Where(reference => reference.IsActive && reference.Year == year)
|
||||
.ToListAsync();
|
||||
var referenceByKey = references
|
||||
.GroupBy(reference => reference.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.Select(reference => reference.CheckValue ?? reference.LocalCurrencyValue).FirstOrDefault(value => value.HasValue),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var dataStatusRows = await BuildFinanceDataStatusRowsAsync(db);
|
||||
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey);
|
||||
var dataStatusRows = await BuildFinanceDataStatusRowsAsync(db, records, settings.UseAuditCsvAsCentralSource);
|
||||
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey, year, countryFilter, currencyFilter);
|
||||
var productAssignmentRows = BuildProductAssignmentRows(scopedRows, allRows);
|
||||
var productFinanceSummary = BuildProductFinanceSummary(productAssignmentRows, resultCurrencies);
|
||||
notices.AddRange(BuildProductAssignmentNotices(productAssignmentRows, productFinanceSummary));
|
||||
@@ -501,6 +499,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
YearOptions = yearOptions,
|
||||
CountryOptions = allRows
|
||||
.Select(row => row.CountryKey)
|
||||
.Concat(references.Where(reference => reference.Year == year).Select(reference => reference.Key))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList(),
|
||||
@@ -536,24 +535,38 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<List<ManagementFinanceDataStatusRow>> BuildFinanceDataStatusRowsAsync(AppDbContext db)
|
||||
private static async Task<List<ManagementFinanceDataStatusRow>> BuildFinanceDataStatusRowsAsync(
|
||||
AppDbContext db,
|
||||
IReadOnlyCollection<SalesRecord> centralRecords,
|
||||
bool useAuditCsv)
|
||||
{
|
||||
var sites = await db.Sites
|
||||
.AsNoTracking()
|
||||
.OrderBy(site => site.Land)
|
||||
.ThenBy(site => site.TSC)
|
||||
.ToListAsync();
|
||||
var records = await db.CentralSalesRecords
|
||||
.AsNoTracking()
|
||||
.GroupBy(record => record.Tsc)
|
||||
.Select(group => new
|
||||
{
|
||||
Tsc = group.Key,
|
||||
RowCount = group.Count(),
|
||||
LatestStoredAtUtc = group.Max(record => record.StoredAtUtc),
|
||||
LatestExtractionDate = group.Max(record => record.ExtractionDate)
|
||||
})
|
||||
.ToListAsync();
|
||||
var records = useAuditCsv
|
||||
? centralRecords
|
||||
.GroupBy(record => record.Tsc)
|
||||
.Select(group => new
|
||||
{
|
||||
Tsc = group.Key,
|
||||
RowCount = group.Count(),
|
||||
LatestStoredAtUtc = (DateTime?)null,
|
||||
LatestExtractionDate = group.Max(record => record.ExtractionDate)
|
||||
})
|
||||
.ToList()
|
||||
: await db.CentralSalesRecords
|
||||
.AsNoTracking()
|
||||
.GroupBy(record => record.Tsc)
|
||||
.Select(group => new
|
||||
{
|
||||
Tsc = group.Key,
|
||||
RowCount = group.Count(),
|
||||
LatestStoredAtUtc = (DateTime?)group.Max(record => record.StoredAtUtc),
|
||||
LatestExtractionDate = group.Max(record => record.ExtractionDate)
|
||||
})
|
||||
.ToListAsync();
|
||||
var logs = await db.ExportLogs
|
||||
.AsNoTracking()
|
||||
.GroupBy(log => log.TSC)
|
||||
@@ -582,7 +595,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
{
|
||||
Land = site.Land,
|
||||
Tsc = site.TSC,
|
||||
SourceSystem = site.SourceSystem,
|
||||
SourceSystem = useAuditCsv ? $"{site.SourceSystem} / Audit-CSV" : site.SourceSystem,
|
||||
IsActive = site.IsActive,
|
||||
RowCount = record?.RowCount ?? 0,
|
||||
LatestStoredAtUtc = record?.LatestStoredAtUtc,
|
||||
@@ -595,17 +608,77 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<SalesRecord>> LoadCentralRecordsAsync()
|
||||
{
|
||||
if (_centralSalesDataProvider is not null)
|
||||
return await _centralSalesDataProvider.GetRecordsAsync();
|
||||
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
return await db.CentralSalesRecords
|
||||
.AsNoTracking()
|
||||
.Select(r => new SalesRecord
|
||||
{
|
||||
SourceSystem = r.SourceSystem,
|
||||
Land = r.Land,
|
||||
Tsc = r.Tsc,
|
||||
DocumentEntry = r.DocumentEntry,
|
||||
InvoiceNumber = r.InvoiceNumber,
|
||||
PositionOnInvoice = r.PositionOnInvoice,
|
||||
Material = r.Material,
|
||||
Name = r.Name,
|
||||
ProductGroup = r.ProductGroup,
|
||||
ProductHierarchyCode = r.ProductHierarchyCode,
|
||||
ProductHierarchyText = r.ProductHierarchyText,
|
||||
ProductFamilyCode = r.ProductFamilyCode,
|
||||
ProductFamilyText = r.ProductFamilyText,
|
||||
ProductDivisionCode = r.ProductDivisionCode,
|
||||
ProductDivisionText = r.ProductDivisionText,
|
||||
ProductMappingAssigned = r.ProductMappingAssigned,
|
||||
Quantity = r.Quantity,
|
||||
SupplierNumber = r.SupplierNumber,
|
||||
SupplierName = r.SupplierName,
|
||||
SupplierCountry = r.SupplierCountry,
|
||||
CustomerNumber = r.CustomerNumber,
|
||||
CustomerName = r.CustomerName,
|
||||
CustomerCountry = r.CustomerCountry,
|
||||
CustomerIndustry = r.CustomerIndustry,
|
||||
StandardCost = r.StandardCost,
|
||||
StandardCostCurrency = r.StandardCostCurrency,
|
||||
PurchaseOrderNumber = r.PurchaseOrderNumber,
|
||||
SalesPriceValue = r.SalesPriceValue,
|
||||
SalesCurrency = r.SalesCurrency,
|
||||
DocumentCurrency = r.DocumentCurrency,
|
||||
DocumentTotalForeignCurrency = r.DocumentTotalForeignCurrency,
|
||||
DocumentTotalLocalCurrency = r.DocumentTotalLocalCurrency,
|
||||
VatSumForeignCurrency = r.VatSumForeignCurrency,
|
||||
VatSumLocalCurrency = r.VatSumLocalCurrency,
|
||||
DocumentRate = r.DocumentRate,
|
||||
CompanyCurrency = r.CompanyCurrency,
|
||||
Incoterms2020 = r.Incoterms2020,
|
||||
SalesResponsibleEmployee = r.SalesResponsibleEmployee,
|
||||
PostingDate = r.PostingDate,
|
||||
InvoiceDate = r.InvoiceDate,
|
||||
OrderDate = r.OrderDate,
|
||||
ExtractionDate = r.ExtractionDate,
|
||||
DocumentType = r.DocumentType
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static List<ManagementFinanceCountryStatusRow> BuildFinanceCountryStatusRows(
|
||||
IReadOnlyCollection<FinanceAggregationRow> rows,
|
||||
IReadOnlyDictionary<string, decimal?> referenceByKey)
|
||||
=> rows
|
||||
IReadOnlyDictionary<string, FinanceReferenceValue?> referenceByKey,
|
||||
int year,
|
||||
string? countryFilter,
|
||||
string? currencyFilter)
|
||||
{
|
||||
var actualRows = rows
|
||||
.GroupBy(row => new { row.Year, row.CountryKey, row.Currency })
|
||||
.OrderBy(group => group.Key.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(group => group.Key.Currency, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group =>
|
||||
{
|
||||
var rowList = group.ToList();
|
||||
referenceByKey.TryGetValue(group.Key.CountryKey, out var referenceValue);
|
||||
referenceByKey.TryGetValue(group.Key.CountryKey, out var reference);
|
||||
var referenceValue = reference?.Value;
|
||||
var actual = rowList.Sum(row => row.Value);
|
||||
var intercompanyValue = rowList.Where(row => row.IsIntercompany).Sum(row => row.Value);
|
||||
var difference = referenceValue.HasValue ? actual - referenceValue.Value : (decimal?)null;
|
||||
@@ -624,11 +697,38 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
ReferenceValue = referenceValue,
|
||||
Difference = difference,
|
||||
DifferencePercent = referenceValue is > 0m && difference.HasValue ? difference.Value / referenceValue.Value * 100m : null,
|
||||
Status = BuildFinanceStatus(difference)
|
||||
Status = BuildFinanceStatus(referenceValue, rowList.Count, difference)
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var actualCountryKeys = actualRows
|
||||
.Select(row => row.CountryKey)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var referenceOnlyRows = currencyFilter is null
|
||||
? referenceByKey.Values
|
||||
.Where(reference => reference?.Value.HasValue == true)
|
||||
.Select(reference => reference!)
|
||||
.Where(reference => countryFilter is null || reference.Key.Equals(countryFilter, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(reference => !actualCountryKeys.Contains(reference.Key))
|
||||
.Select(reference => new ManagementFinanceCountryStatusRow
|
||||
{
|
||||
Year = year,
|
||||
CountryKey = reference.Key,
|
||||
Currency = "-",
|
||||
ReferenceValue = reference.Value,
|
||||
Status = BuildFinanceStatus(reference.Value, 0, null)
|
||||
})
|
||||
.ToList()
|
||||
: [];
|
||||
|
||||
return actualRows
|
||||
.Concat(referenceOnlyRows)
|
||||
.OrderBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(row => row.Currency, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<ManagementFinanceCreditCandidateRow> BuildFinanceCreditCandidates(IEnumerable<FinanceAggregationRow> rows)
|
||||
=> rows
|
||||
.Where(row => row.Value < 0m || row.RawSalesValue < 0m || LooksLikeCreditDocument(row.DocumentType, row.InvoiceNumber))
|
||||
@@ -946,10 +1046,14 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildFinanceStatus(decimal? difference)
|
||||
private static string BuildFinanceStatus(decimal? referenceValue, int actualRowCount, decimal? difference)
|
||||
{
|
||||
if (!difference.HasValue)
|
||||
if (!referenceValue.HasValue)
|
||||
return "Kein Sollwert";
|
||||
if (actualRowCount == 0)
|
||||
return "Keine Daten";
|
||||
if (!difference.HasValue)
|
||||
return "Pruefen";
|
||||
|
||||
return Math.Abs(difference.Value) <= 1m ? "OK" : "Pruefen";
|
||||
}
|
||||
@@ -1743,6 +1847,8 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
public DateTime ExtractionDate { get; set; }
|
||||
}
|
||||
|
||||
private sealed record FinanceReferenceValue(string Key, string Label, decimal? Value);
|
||||
|
||||
private sealed record AggregationSelection(
|
||||
ValueFieldDefinition ValueField,
|
||||
IReadOnlyList<ValueFieldDefinition> AdditionalValueFields,
|
||||
|
||||
@@ -108,6 +108,9 @@ public sealed class SettingsPageService : ISettingsPageService
|
||||
existing.DebugLoggingEnabled = settings.DebugLoggingEnabled;
|
||||
existing.LocalSiteExportFolder = settings.LocalSiteExportFolder;
|
||||
existing.LocalConsolidatedExportFolder = settings.LocalConsolidatedExportFolder;
|
||||
existing.AuditCsvEnabled = settings.AuditCsvEnabled;
|
||||
existing.UseAuditCsvAsCentralSource = settings.UseAuditCsvAsCentralSource;
|
||||
existing.LocalAuditCsvFolder = settings.LocalAuditCsvFolder;
|
||||
existing.ExchangeRateDateField = NormalizeExchangeRateDateField(settings.ExchangeRateDateField);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ public class SiteExportService : ISiteExportService
|
||||
private readonly ISharePointUploadService _sharePointService;
|
||||
private readonly IRecordTransformationService _transformationService;
|
||||
private readonly ICentralSalesRecordService _centralSalesRecordService;
|
||||
private readonly IExportAuditCsvService _auditCsvService;
|
||||
private readonly IAppEventLogService _appEventLogService;
|
||||
private readonly ILogger<SiteExportService> _logger;
|
||||
|
||||
@@ -24,6 +25,7 @@ public class SiteExportService : ISiteExportService
|
||||
ISharePointUploadService sharePointService,
|
||||
IRecordTransformationService transformationService,
|
||||
ICentralSalesRecordService centralSalesRecordService,
|
||||
IExportAuditCsvService auditCsvService,
|
||||
IAppEventLogService appEventLogService,
|
||||
ILogger<SiteExportService> logger)
|
||||
{
|
||||
@@ -33,6 +35,7 @@ public class SiteExportService : ISiteExportService
|
||||
_sharePointService = sharePointService;
|
||||
_transformationService = transformationService;
|
||||
_centralSalesRecordService = centralSalesRecordService;
|
||||
_auditCsvService = auditCsvService;
|
||||
_appEventLogService = appEventLogService;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -76,6 +79,15 @@ public class SiteExportService : ISiteExportService
|
||||
details: $"Records vor Transformation={records.Count}");
|
||||
_transformationService.Apply(records, rules);
|
||||
|
||||
var auditCsvPath = await _auditCsvService.WriteSiteAuditCsvAsync(
|
||||
site, settings, sourceSystem, outputDir, records);
|
||||
if (!string.IsNullOrWhiteSpace(auditCsvPath))
|
||||
{
|
||||
await _appEventLogService.WriteAsync("Export", "Audit-CSV geschrieben",
|
||||
siteId: site.Id, land: site.Land,
|
||||
details: auditCsvPath);
|
||||
}
|
||||
|
||||
var filePath = fetchResult.ReferenceFilePath;
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
@@ -94,7 +106,7 @@ public class SiteExportService : ISiteExportService
|
||||
details: $"Records={records.Count}");
|
||||
await _centralSalesRecordService.ReplaceForSiteAsync(site, records, updateStatus);
|
||||
|
||||
await UploadToSharePointIfConfiguredAsync(site, spConfig, filePath, updateStatus, fetchResult);
|
||||
await UploadToSharePointIfConfiguredAsync(site, spConfig, filePath, auditCsvPath, updateStatus, fetchResult);
|
||||
|
||||
sw.Stop();
|
||||
log.Status = "OK";
|
||||
@@ -159,6 +171,7 @@ public class SiteExportService : ISiteExportService
|
||||
Site site,
|
||||
SharePointConfig? spConfig,
|
||||
string filePath,
|
||||
string? auditCsvPath,
|
||||
Action<string>? updateStatus,
|
||||
DataSourceFetchResult fetchResult)
|
||||
{
|
||||
@@ -179,6 +192,17 @@ public class SiteExportService : ISiteExportService
|
||||
await _sharePointService.UploadAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret,
|
||||
spConfig.SiteUrl, uploadFolder, uploadLand, filePath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(auditCsvPath) || !File.Exists(auditCsvPath))
|
||||
return;
|
||||
|
||||
updateStatus?.Invoke("Audit-CSV SharePoint Upload...");
|
||||
await _appEventLogService.WriteAsync("Export", "Audit-CSV SharePoint Upload gestartet",
|
||||
siteId: site.Id, land: site.Land,
|
||||
details: $"{spConfig.SiteUrl} | {uploadFolder}");
|
||||
await _sharePointService.UploadAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret,
|
||||
spConfig.SiteUrl, uploadFolder, uploadLand, auditCsvPath);
|
||||
}
|
||||
|
||||
private static string NormalizeSourceSystem(string? sourceSystem)
|
||||
|
||||
@@ -254,7 +254,102 @@ public sealed class UiTextService : IUiTextService
|
||||
["Transformer Ansicht"] = "Vista de transformaciones",
|
||||
["Transformationscode"] = "Código de transformación",
|
||||
["Keine Beschreibung."] = "Sin descripción.",
|
||||
["Optionales Argument."] = "Argumento opcional."
|
||||
["Optionales Argument."] = "Argumento opcional.",
|
||||
["Einkauf"] = "Compras",
|
||||
["Einkauf Dashboard"] = "Panel de compras",
|
||||
["Einkauf Datenquellen"] = "Fuentes de datos de compras",
|
||||
["Einkauf Cockpit"] = "Cockpit de compras",
|
||||
["Spend, Lieferanten, offene Verpflichtungen"] = "Gasto, proveedores, compromisos abiertos",
|
||||
["Operative Einkaufsanalyse mit Live-EKKO, klarer SAP-Datenpipeline, Simulationen und 3D-What-if-Ansicht."] = "Analisis operativo de compras con EKKO en vivo, canal SAP claro, simulaciones y vista 3D what-if.",
|
||||
["Zeitraum"] = "Periodo",
|
||||
["Alle Einkauf-Kennzahlen, Top-Listen und 3D-Daten werden auf diesen Zeitraum abgegrenzt."] = "Todos los KPIs de compras, listas principales y datos 3D se limitan a este periodo.",
|
||||
["Von Monat"] = "Desde mes",
|
||||
["Bis Monat"] = "Hasta mes",
|
||||
["Anwenden"] = "Aplicar",
|
||||
["Letzte 3 Jahre"] = "Ultimos 3 anos",
|
||||
["Spend total"] = "Gasto total",
|
||||
["Offene Bestellungen"] = "Pedidos abiertos",
|
||||
["Verpflichtungen"] = "Compromisos",
|
||||
["Lieferantenperformance"] = "Rendimiento de proveedores",
|
||||
["Spend total vergangen"] = "Gasto historico total",
|
||||
["Beschaffungsvolumen in CHF nach Jahr, Lieferant, Warengruppe und Artikel. Spend-Werte brauchen EKPO; bis SAP Positionen liefert, ist die Ansicht als Simulation markiert."] = "Volumen de compras en CHF por ano, proveedor, grupo de materiales y articulo. Los valores de gasto necesitan EKPO; hasta que SAP entregue posiciones, la vista queda marcada como simulacion.",
|
||||
["Spend-Verlauf nach Einkaufsdimension"] = "Evolucion del gasto por dimension de compras",
|
||||
["Offene Bestellwerte und Mengen"] = "Valores y cantidades de pedidos abiertos",
|
||||
["Live-Bestellkoepfe aus EKKO sind angebunden. Offene Werte und Mengen brauchen zusaetzlich EKPO/EKET."] = "Las cabeceras de pedido en vivo de EKKO estan conectadas. Los valores y cantidades abiertos necesitan ademas EKPO/EKET.",
|
||||
["Bestellaktivitaet und offene Positionen"] = "Actividad de pedidos y posiciones abiertas",
|
||||
["Offene Verpflichtungen"] = "Compromisos abiertos",
|
||||
["Restverpflichtungen werden aus EKET-Offenmenge und EKPO-Stueckwert berechnet und nach Lieferant, Artikel und Faelligkeitsmonat gezeigt."] = "Los compromisos restantes se calculan con cantidad abierta EKET y valor unitario EKPO y se muestran por proveedor, articulo y mes de vencimiento.",
|
||||
["Top Verpflichtungen nach Lieferant, Artikel und Faelligkeit"] = "Compromisos principales por proveedor, articulo y vencimiento",
|
||||
["Lieferantenbewertung und Performance"] = "Evaluacion y rendimiento de proveedores",
|
||||
["Lieferantenbasis kommt live aus EKKO. Bewertung, Termintreue und Preisentwicklung brauchen spaeter EKPO/EKET und Reklamationsdaten."] = "La base de proveedores viene en vivo de EKKO. Evaluacion, puntualidad y evolucion de precios necesitaran EKPO/EKET y datos de reclamaciones.",
|
||||
["Lieferantenbasis und Performance-Indikatoren"] = "Base de proveedores e indicadores de rendimiento",
|
||||
["Weitere Einkaufsanalysen"] = "Mas analisis de compras",
|
||||
["Analysen, die dem Einkauf neben PowerBI mehr Steuerung, Risiko- und Sparpotenzial zeigen."] = "Analisis que dan a compras mas control, riesgo y potencial de ahorro ademas de Power BI.",
|
||||
["Prioritaet"] = "Prioridad",
|
||||
["Ideen ausgearbeitet"] = "Ideas desarrolladas",
|
||||
["Jede Idee ist als aufklappbarer Ausbau-Baustein beschrieben."] = "Cada idea se describe como bloque ampliable.",
|
||||
["Bausteine"] = "bloques",
|
||||
["Ziel"] = "Objetivo",
|
||||
["Datenbasis"] = "Base de datos",
|
||||
["Kennzahlen"] = "KPIs",
|
||||
["Visualisierung"] = "Visualizacion",
|
||||
["Umsetzung"] = "Implementacion",
|
||||
["Zielbild"] = "Estado objetivo",
|
||||
["Technische Umsetzung"] = "Implementacion tecnica",
|
||||
["Refresh Steuerung"] = "Control de refresco",
|
||||
["Full Load baut die korrekte Basis auf. Delta aktualisiert danach nur geaenderte Einkaufsbelege."] = "Full load crea la base correcta. Delta actualiza despues solo documentos de compra modificados.",
|
||||
["Bestellkoepfe im Cache"] = "Cabeceras de pedido en cache",
|
||||
["Positionen im Cache"] = "Posiciones en cache",
|
||||
["Einteilungen im Cache"] = "Planificaciones en cache",
|
||||
["Letzter Stand"] = "Ultimo estado",
|
||||
["Full Load starten"] = "Iniciar full load",
|
||||
["Delta aktualisieren"] = "Actualizar delta",
|
||||
["Detail-Hotlist"] = "Hotlist de detalle",
|
||||
["Direkt aus dem Einkauf-Cache berechnet, keine Simulation."] = "Calculado directamente desde el cache de compras, sin simulacion.",
|
||||
["Objekt"] = "Objeto",
|
||||
["Kennzahlen-Katalog fuer den naechsten Ausbau"] = "Catalogo de KPIs para la siguiente ampliacion",
|
||||
["Fachlicher Ausbauplan mit Kennzahl, Dimension, Datenbasis und aktuellem Umsetzungsstand."] = "Plan funcional de ampliacion con KPI, dimension, base de datos y estado actual.",
|
||||
["Analyse"] = "Analisis",
|
||||
["Aus x.pbix uebernommene Seiten"] = "Paginas derivadas de x.pbix",
|
||||
["Power-BI-Seite"] = "Pagina Power BI",
|
||||
["Visuals"] = "Visuales",
|
||||
["Dimensionen"] = "Dimensiones",
|
||||
["Indikator"] = "Indicador",
|
||||
["Grafik"] = "Grafico",
|
||||
["Balken"] = "Barras",
|
||||
["Linie"] = "Linea",
|
||||
["Flaeche"] = "Superficie",
|
||||
["Kreis"] = "Circular",
|
||||
["Preis-/Wechselkurs-Szenario"] = "Escenario de precio/tipo de cambio",
|
||||
["Beschriftung"] = "Etiquetas",
|
||||
["Neu zeichnen"] = "Redibujar",
|
||||
["SAP Datenfluss"] = "Flujo de datos SAP",
|
||||
["Vom OData-Service bis zur Kennzahl sichtbar, welcher Baustein echt ist."] = "Desde el servicio OData hasta el KPI se ve que bloque es real.",
|
||||
["Management Insights"] = "Insights de gestion",
|
||||
["Live"] = "En vivo",
|
||||
["Wartet"] = "Esperando",
|
||||
["Simulation"] = "Simulacion",
|
||||
["Analyseachsen"] = "Ejes de analisis",
|
||||
["Verbindung"] = "Conexion",
|
||||
["Quellsystem"] = "Sistema fuente",
|
||||
["Leer lassen = zentrale SAP OData URL aus Settings verwenden."] = "Dejar vacio = usar la URL central SAP OData de ajustes.",
|
||||
["Leer lassen = zentraler SAP User."] = "Dejar vacio = usuario SAP central.",
|
||||
["Leer lassen = zentrales SAP Passwort."] = "Dejar vacio = contrasena SAP central.",
|
||||
["Einkaufsquelle fuer Import aktivieren"] = "Activar fuente de compras para importacion",
|
||||
["Verbindung testen"] = "Probar conexion",
|
||||
["Defaults wiederherstellen"] = "Restaurar valores predeterminados",
|
||||
["Aktuelle Basis"] = "Base actual",
|
||||
["Zentrale URL"] = "URL central",
|
||||
["Quellen"] = "Fuentes",
|
||||
["Join-Fluss"] = "Flujo de joins",
|
||||
["Links"] = "Izquierda",
|
||||
["Rechts"] = "Derecha",
|
||||
["Zielfelder"] = "Campos destino",
|
||||
["Pflicht"] = "Obligatorio",
|
||||
["Primaer"] = "Primario",
|
||||
["Einkaufsdatenquellen gespeichert."] = "Fuentes de datos de compras guardadas.",
|
||||
["Einkaufsdatenquellen auf Defaults gesetzt."] = "Fuentes de datos de compras restauradas a valores predeterminados.",
|
||||
["Schreibt beim Laenderexport je Standort eine Sales_ProcessedMergeInput_*.csv mit den transformierten Daten."] = "Escribe una Sales_ProcessedMergeInput_*.csv por sitio durante la exportacion de paises con los datos transformados."
|
||||
},
|
||||
["it"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -487,7 +582,102 @@ public sealed class UiTextService : IUiTextService
|
||||
["Transformer Ansicht"] = "Vista trasformazioni",
|
||||
["Transformationscode"] = "Codice trasformazione",
|
||||
["Keine Beschreibung."] = "Nessuna descrizione.",
|
||||
["Optionales Argument."] = "Argomento opzionale."
|
||||
["Optionales Argument."] = "Argomento opzionale.",
|
||||
["Einkauf"] = "Acquisti",
|
||||
["Einkauf Dashboard"] = "Dashboard acquisti",
|
||||
["Einkauf Datenquellen"] = "Fonti dati acquisti",
|
||||
["Einkauf Cockpit"] = "Cockpit acquisti",
|
||||
["Spend, Lieferanten, offene Verpflichtungen"] = "Spesa, fornitori, impegni aperti",
|
||||
["Operative Einkaufsanalyse mit Live-EKKO, klarer SAP-Datenpipeline, Simulationen und 3D-What-if-Ansicht."] = "Analisi operativa acquisti con EKKO live, pipeline SAP chiara, simulazioni e vista 3D what-if.",
|
||||
["Zeitraum"] = "Periodo",
|
||||
["Alle Einkauf-Kennzahlen, Top-Listen und 3D-Daten werden auf diesen Zeitraum abgegrenzt."] = "Tutti i KPI acquisti, le top list e i dati 3D sono limitati a questo periodo.",
|
||||
["Von Monat"] = "Da mese",
|
||||
["Bis Monat"] = "A mese",
|
||||
["Anwenden"] = "Applica",
|
||||
["Letzte 3 Jahre"] = "Ultimi 3 anni",
|
||||
["Spend total"] = "Spesa totale",
|
||||
["Offene Bestellungen"] = "Ordini aperti",
|
||||
["Verpflichtungen"] = "Impegni",
|
||||
["Lieferantenperformance"] = "Performance fornitori",
|
||||
["Spend total vergangen"] = "Spesa storica totale",
|
||||
["Beschaffungsvolumen in CHF nach Jahr, Lieferant, Warengruppe und Artikel. Spend-Werte brauchen EKPO; bis SAP Positionen liefert, ist die Ansicht als Simulation markiert."] = "Volume acquisti in CHF per anno, fornitore, gruppo merce e articolo. I valori di spesa richiedono EKPO; finche SAP non fornisce le posizioni, la vista resta marcata come simulazione.",
|
||||
["Spend-Verlauf nach Einkaufsdimension"] = "Andamento spesa per dimensione acquisti",
|
||||
["Offene Bestellwerte und Mengen"] = "Valori e quantita ordini aperti",
|
||||
["Live-Bestellkoepfe aus EKKO sind angebunden. Offene Werte und Mengen brauchen zusaetzlich EKPO/EKET."] = "Le testate ordine live da EKKO sono collegate. Valori e quantita aperte richiedono inoltre EKPO/EKET.",
|
||||
["Bestellaktivitaet und offene Positionen"] = "Attivita ordini e posizioni aperte",
|
||||
["Offene Verpflichtungen"] = "Impegni aperti",
|
||||
["Restverpflichtungen werden aus EKET-Offenmenge und EKPO-Stueckwert berechnet und nach Lieferant, Artikel und Faelligkeitsmonat gezeigt."] = "Gli impegni residui sono calcolati da quantita aperta EKET e valore unitario EKPO e mostrati per fornitore, articolo e mese di scadenza.",
|
||||
["Top Verpflichtungen nach Lieferant, Artikel und Faelligkeit"] = "Top impegni per fornitore, articolo e scadenza",
|
||||
["Lieferantenbewertung und Performance"] = "Valutazione e performance fornitori",
|
||||
["Lieferantenbasis kommt live aus EKKO. Bewertung, Termintreue und Preisentwicklung brauchen spaeter EKPO/EKET und Reklamationsdaten."] = "La base fornitori arriva live da EKKO. Valutazione, puntualita e andamento prezzi richiedono poi EKPO/EKET e dati reclami.",
|
||||
["Lieferantenbasis und Performance-Indikatoren"] = "Base fornitori e indicatori di performance",
|
||||
["Weitere Einkaufsanalysen"] = "Altre analisi acquisti",
|
||||
["Analysen, die dem Einkauf neben PowerBI mehr Steuerung, Risiko- und Sparpotenzial zeigen."] = "Analisi che offrono agli acquisti piu controllo, rischio e potenziale risparmio oltre a Power BI.",
|
||||
["Prioritaet"] = "Priorita",
|
||||
["Ideen ausgearbeitet"] = "Idee sviluppate",
|
||||
["Jede Idee ist als aufklappbarer Ausbau-Baustein beschrieben."] = "Ogni idea e descritta come blocco di ampliamento espandibile.",
|
||||
["Bausteine"] = "blocchi",
|
||||
["Ziel"] = "Obiettivo",
|
||||
["Datenbasis"] = "Base dati",
|
||||
["Kennzahlen"] = "KPI",
|
||||
["Visualisierung"] = "Visualizzazione",
|
||||
["Umsetzung"] = "Implementazione",
|
||||
["Zielbild"] = "Stato target",
|
||||
["Technische Umsetzung"] = "Implementazione tecnica",
|
||||
["Refresh Steuerung"] = "Controllo refresh",
|
||||
["Full Load baut die korrekte Basis auf. Delta aktualisiert danach nur geaenderte Einkaufsbelege."] = "Il full load crea la base corretta. Delta aggiorna poi solo i documenti acquisti modificati.",
|
||||
["Bestellkoepfe im Cache"] = "Testate ordine in cache",
|
||||
["Positionen im Cache"] = "Posizioni in cache",
|
||||
["Einteilungen im Cache"] = "Schedulazioni in cache",
|
||||
["Letzter Stand"] = "Ultimo stato",
|
||||
["Full Load starten"] = "Avvia full load",
|
||||
["Delta aktualisieren"] = "Aggiorna delta",
|
||||
["Detail-Hotlist"] = "Hotlist dettagli",
|
||||
["Direkt aus dem Einkauf-Cache berechnet, keine Simulation."] = "Calcolato direttamente dalla cache acquisti, senza simulazione.",
|
||||
["Objekt"] = "Oggetto",
|
||||
["Kennzahlen-Katalog fuer den naechsten Ausbau"] = "Catalogo KPI per il prossimo ampliamento",
|
||||
["Fachlicher Ausbauplan mit Kennzahl, Dimension, Datenbasis und aktuellem Umsetzungsstand."] = "Piano funzionale di ampliamento con KPI, dimensione, base dati e stato attuale.",
|
||||
["Analyse"] = "Analisi",
|
||||
["Aus x.pbix uebernommene Seiten"] = "Pagine derivate da x.pbix",
|
||||
["Power-BI-Seite"] = "Pagina Power BI",
|
||||
["Visuals"] = "Visual",
|
||||
["Dimensionen"] = "Dimensioni",
|
||||
["Indikator"] = "Indicatore",
|
||||
["Grafik"] = "Grafico",
|
||||
["Balken"] = "Barre",
|
||||
["Linie"] = "Linea",
|
||||
["Flaeche"] = "Superficie",
|
||||
["Kreis"] = "Torta",
|
||||
["Preis-/Wechselkurs-Szenario"] = "Scenario prezzo/cambio",
|
||||
["Beschriftung"] = "Etichette",
|
||||
["Neu zeichnen"] = "Ridisegna",
|
||||
["SAP Datenfluss"] = "Flusso dati SAP",
|
||||
["Vom OData-Service bis zur Kennzahl sichtbar, welcher Baustein echt ist."] = "Dal servizio OData al KPI e visibile quale blocco e reale.",
|
||||
["Management Insights"] = "Insight management",
|
||||
["Live"] = "Live",
|
||||
["Wartet"] = "In attesa",
|
||||
["Simulation"] = "Simulazione",
|
||||
["Analyseachsen"] = "Assi di analisi",
|
||||
["Verbindung"] = "Connessione",
|
||||
["Quellsystem"] = "Sistema sorgente",
|
||||
["Leer lassen = zentrale SAP OData URL aus Settings verwenden."] = "Lasciare vuoto = usare l'URL SAP OData centrale dalle impostazioni.",
|
||||
["Leer lassen = zentraler SAP User."] = "Lasciare vuoto = utente SAP centrale.",
|
||||
["Leer lassen = zentrales SAP Passwort."] = "Lasciare vuoto = password SAP centrale.",
|
||||
["Einkaufsquelle fuer Import aktivieren"] = "Attiva fonte acquisti per import",
|
||||
["Verbindung testen"] = "Test connessione",
|
||||
["Defaults wiederherstellen"] = "Ripristina default",
|
||||
["Aktuelle Basis"] = "Base attuale",
|
||||
["Zentrale URL"] = "URL centrale",
|
||||
["Quellen"] = "Fonti",
|
||||
["Join-Fluss"] = "Flusso join",
|
||||
["Links"] = "Sinistra",
|
||||
["Rechts"] = "Destra",
|
||||
["Zielfelder"] = "Campi target",
|
||||
["Pflicht"] = "Obbligatorio",
|
||||
["Primaer"] = "Primario",
|
||||
["Einkaufsdatenquellen gespeichert."] = "Fonti dati acquisti salvate.",
|
||||
["Einkaufsdatenquellen auf Defaults gesetzt."] = "Fonti dati acquisti ripristinate ai default.",
|
||||
["Schreibt beim Laenderexport je Standort eine Sales_ProcessedMergeInput_*.csv mit den transformierten Daten."] = "Scrive una Sales_ProcessedMergeInput_*.csv per sito durante l'esportazione dei Paesi con i dati trasformati."
|
||||
},
|
||||
["hi"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -720,7 +910,102 @@ public sealed class UiTextService : IUiTextService
|
||||
["Transformer Ansicht"] = "रूपांतरण दृश्य",
|
||||
["Transformationscode"] = "रूपांतरण कोड",
|
||||
["Keine Beschreibung."] = "कोई विवरण नहीं.",
|
||||
["Optionales Argument."] = "वैकल्पिक तर्क."
|
||||
["Optionales Argument."] = "वैकल्पिक तर्क.",
|
||||
["Einkauf"] = "Kharid",
|
||||
["Einkauf Dashboard"] = "Kharid dashboard",
|
||||
["Einkauf Datenquellen"] = "Kharid data sources",
|
||||
["Einkauf Cockpit"] = "Kharid cockpit",
|
||||
["Spend, Lieferanten, offene Verpflichtungen"] = "Spend, suppliers, open commitments",
|
||||
["Operative Einkaufsanalyse mit Live-EKKO, klarer SAP-Datenpipeline, Simulationen und 3D-What-if-Ansicht."] = "Live EKKO, clear SAP data pipeline, simulations aur 3D what-if ke saath operative purchasing analysis.",
|
||||
["Zeitraum"] = "Period",
|
||||
["Alle Einkauf-Kennzahlen, Top-Listen und 3D-Daten werden auf diesen Zeitraum abgegrenzt."] = "Sabhi purchasing KPIs, top lists aur 3D data is period tak limited hain.",
|
||||
["Von Monat"] = "From month",
|
||||
["Bis Monat"] = "To month",
|
||||
["Anwenden"] = "Apply",
|
||||
["Letzte 3 Jahre"] = "Last 3 years",
|
||||
["Spend total"] = "Total spend",
|
||||
["Offene Bestellungen"] = "Open orders",
|
||||
["Verpflichtungen"] = "Commitments",
|
||||
["Lieferantenperformance"] = "Supplier performance",
|
||||
["Spend total vergangen"] = "Historic total spend",
|
||||
["Beschaffungsvolumen in CHF nach Jahr, Lieferant, Warengruppe und Artikel. Spend-Werte brauchen EKPO; bis SAP Positionen liefert, ist die Ansicht als Simulation markiert."] = "Purchasing volume CHF me year, supplier, material group aur article ke hisab se. Spend values ko EKPO chahiye; SAP item rows aane tak view simulation ke roop me marked hai.",
|
||||
["Spend-Verlauf nach Einkaufsdimension"] = "Spend trend by purchasing dimension",
|
||||
["Offene Bestellwerte und Mengen"] = "Open order values and quantities",
|
||||
["Live-Bestellkoepfe aus EKKO sind angebunden. Offene Werte und Mengen brauchen zusaetzlich EKPO/EKET."] = "EKKO ke live purchase-order headers connected hain. Open values aur quantities ke liye EKPO/EKET bhi chahiye.",
|
||||
["Bestellaktivitaet und offene Positionen"] = "Order activity and open items",
|
||||
["Offene Verpflichtungen"] = "Open commitments",
|
||||
["Restverpflichtungen werden aus EKET-Offenmenge und EKPO-Stueckwert berechnet und nach Lieferant, Artikel und Faelligkeitsmonat gezeigt."] = "Remaining commitments EKET open quantity aur EKPO unit value se calculate hote hain aur supplier, article aur due month ke hisab se dikhte hain.",
|
||||
["Top Verpflichtungen nach Lieferant, Artikel und Faelligkeit"] = "Top commitments by supplier, article and due date",
|
||||
["Lieferantenbewertung und Performance"] = "Supplier rating and performance",
|
||||
["Lieferantenbasis kommt live aus EKKO. Bewertung, Termintreue und Preisentwicklung brauchen spaeter EKPO/EKET und Reklamationsdaten."] = "Supplier base EKKO se live aata hai. Rating, delivery reliability aur price trend ke liye baad me EKPO/EKET aur claims data chahiye.",
|
||||
["Lieferantenbasis und Performance-Indikatoren"] = "Supplier base and performance indicators",
|
||||
["Weitere Einkaufsanalysen"] = "Additional purchasing analytics",
|
||||
["Analysen, die dem Einkauf neben PowerBI mehr Steuerung, Risiko- und Sparpotenzial zeigen."] = "Power BI ke alawa purchasing ko zyada steering, risk aur saving potential dikhane wali analyses.",
|
||||
["Prioritaet"] = "Priority",
|
||||
["Ideen ausgearbeitet"] = "Ideas worked out",
|
||||
["Jede Idee ist als aufklappbarer Ausbau-Baustein beschrieben."] = "Har idea expandable build-out block ke roop me described hai.",
|
||||
["Bausteine"] = "blocks",
|
||||
["Ziel"] = "Goal",
|
||||
["Datenbasis"] = "Data basis",
|
||||
["Kennzahlen"] = "KPIs",
|
||||
["Visualisierung"] = "Visualisation",
|
||||
["Umsetzung"] = "Implementation",
|
||||
["Zielbild"] = "Target state",
|
||||
["Technische Umsetzung"] = "Technical implementation",
|
||||
["Refresh Steuerung"] = "Refresh control",
|
||||
["Full Load baut die korrekte Basis auf. Delta aktualisiert danach nur geaenderte Einkaufsbelege."] = "Full load correct base banata hai. Delta uske baad sirf changed purchase documents update karta hai.",
|
||||
["Bestellkoepfe im Cache"] = "Purchase headers in cache",
|
||||
["Positionen im Cache"] = "Item rows in cache",
|
||||
["Einteilungen im Cache"] = "Schedules in cache",
|
||||
["Letzter Stand"] = "Latest state",
|
||||
["Full Load starten"] = "Start full load",
|
||||
["Delta aktualisieren"] = "Refresh delta",
|
||||
["Detail-Hotlist"] = "Detail hotlist",
|
||||
["Direkt aus dem Einkauf-Cache berechnet, keine Simulation."] = "Purchasing cache se directly calculated, simulation nahi.",
|
||||
["Objekt"] = "Object",
|
||||
["Kennzahlen-Katalog fuer den naechsten Ausbau"] = "KPI catalogue for next build-out",
|
||||
["Fachlicher Ausbauplan mit Kennzahl, Dimension, Datenbasis und aktuellem Umsetzungsstand."] = "KPI, dimension, data basis aur current implementation status ke saath functional build-out plan.",
|
||||
["Analyse"] = "Analysis",
|
||||
["Aus x.pbix uebernommene Seiten"] = "Pages derived from x.pbix",
|
||||
["Power-BI-Seite"] = "Power BI page",
|
||||
["Visuals"] = "Visuals",
|
||||
["Dimensionen"] = "Dimensions",
|
||||
["Indikator"] = "Indicator",
|
||||
["Grafik"] = "Chart",
|
||||
["Balken"] = "Bars",
|
||||
["Linie"] = "Line",
|
||||
["Flaeche"] = "Surface",
|
||||
["Kreis"] = "Pie",
|
||||
["Preis-/Wechselkurs-Szenario"] = "Price/exchange-rate scenario",
|
||||
["Beschriftung"] = "Labels",
|
||||
["Neu zeichnen"] = "Redraw",
|
||||
["SAP Datenfluss"] = "SAP data flow",
|
||||
["Vom OData-Service bis zur Kennzahl sichtbar, welcher Baustein echt ist."] = "OData service se KPI tak visible hai kaunsa block real hai.",
|
||||
["Management Insights"] = "Management insights",
|
||||
["Live"] = "Live",
|
||||
["Wartet"] = "Waiting",
|
||||
["Simulation"] = "Simulation",
|
||||
["Analyseachsen"] = "Analysis axes",
|
||||
["Verbindung"] = "Connection",
|
||||
["Quellsystem"] = "Source system",
|
||||
["Leer lassen = zentrale SAP OData URL aus Settings verwenden."] = "Empty chhodne par settings ki central SAP OData URL use hogi.",
|
||||
["Leer lassen = zentraler SAP User."] = "Empty chhodne par central SAP user use hoga.",
|
||||
["Leer lassen = zentrales SAP Passwort."] = "Empty chhodne par central SAP password use hoga.",
|
||||
["Einkaufsquelle fuer Import aktivieren"] = "Activate purchasing source for import",
|
||||
["Verbindung testen"] = "Test connection",
|
||||
["Defaults wiederherstellen"] = "Restore defaults",
|
||||
["Aktuelle Basis"] = "Current basis",
|
||||
["Zentrale URL"] = "Central URL",
|
||||
["Quellen"] = "Sources",
|
||||
["Join-Fluss"] = "Join flow",
|
||||
["Links"] = "Left",
|
||||
["Rechts"] = "Right",
|
||||
["Zielfelder"] = "Target fields",
|
||||
["Pflicht"] = "Required",
|
||||
["Primaer"] = "Primary",
|
||||
["Einkaufsdatenquellen gespeichert."] = "Purchasing data sources saved.",
|
||||
["Einkaufsdatenquellen auf Defaults gesetzt."] = "Purchasing data sources restored to defaults.",
|
||||
["Schreibt beim Laenderexport je Standort eine Sales_ProcessedMergeInput_*.csv mit den transformierten Daten."] = "Country export ke dauran har site ke liye transformed data wali Sales_ProcessedMergeInput_*.csv likhta hai."
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
using TrafagSalesExporter.Services;
|
||||
|
||||
namespace TrafagSalesExporter.Tests;
|
||||
|
||||
public sealed class ExportAuditCsvServiceTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDirectory;
|
||||
|
||||
public ExportAuditCsvServiceTests()
|
||||
{
|
||||
_tempDirectory = Path.Combine("C:\\TMP", $"trafag-audit-csv-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDirectory))
|
||||
Directory.Delete(_tempDirectory, recursive: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteSiteAuditCsvAsync_Roundtrips_Transformed_SalesRecord()
|
||||
{
|
||||
var service = new ExportAuditCsvService();
|
||||
var settings = new ExportSettings
|
||||
{
|
||||
AuditCsvEnabled = true,
|
||||
LocalSiteExportFolder = _tempDirectory,
|
||||
LocalAuditCsvFolder = Path.Combine(_tempDirectory, "ignored")
|
||||
};
|
||||
var site = new Site { TSC = "TRCH", Land = "Schweiz" };
|
||||
var record = new SalesRecord
|
||||
{
|
||||
SourceSystem = "SAP",
|
||||
ExtractionDate = new DateTime(2026, 6, 11, 8, 30, 0, DateTimeKind.Utc),
|
||||
Tsc = "TRCH",
|
||||
SourceLineId = "line-1",
|
||||
DocumentEntry = 42,
|
||||
InvoiceNumber = "INV-1",
|
||||
PositionOnInvoice = 7,
|
||||
Material = "MAT;1",
|
||||
Name = "Artikel \"Audit\"",
|
||||
ProductDivisionCode = "0001",
|
||||
ProductDivisionText = "Pressure",
|
||||
ProductMappingAssigned = "TRUE",
|
||||
Quantity = 2.5m,
|
||||
SalesPriceValue = 1234.56m,
|
||||
SalesCurrency = "CHF",
|
||||
DocumentCurrency = "EUR",
|
||||
DocumentTotalForeignCurrency = 1300m,
|
||||
DocumentTotalLocalCurrency = 1234.56m,
|
||||
VatSumForeignCurrency = 0m,
|
||||
VatSumLocalCurrency = 0m,
|
||||
DocumentRate = 0.95m,
|
||||
CompanyCurrency = "CHF",
|
||||
PostingDate = new DateTime(2026, 6, 10),
|
||||
InvoiceDate = new DateTime(2026, 6, 11),
|
||||
Land = "Schweiz",
|
||||
DocumentType = "INV"
|
||||
};
|
||||
|
||||
var path = await service.WriteSiteAuditCsvAsync(site, settings, "SAP", _tempDirectory, [record]);
|
||||
|
||||
Assert.True(File.Exists(path));
|
||||
Assert.Equal(_tempDirectory, Path.GetDirectoryName(path));
|
||||
Assert.StartsWith("Sales_ProcessedMergeInput_TRCH_", Path.GetFileName(path), StringComparison.OrdinalIgnoreCase);
|
||||
var records = await service.ReadLatestSiteAuditCsvRecordsAsync(settings);
|
||||
var roundtrip = Assert.Single(records);
|
||||
Assert.Equal("SAP", roundtrip.SourceSystem);
|
||||
Assert.Equal("TRCH", roundtrip.Tsc);
|
||||
Assert.Equal("line-1", roundtrip.SourceLineId);
|
||||
Assert.Equal("MAT;1", roundtrip.Material);
|
||||
Assert.Equal("Artikel \"Audit\"", roundtrip.Name);
|
||||
Assert.Equal(1234.56m, roundtrip.SalesPriceValue);
|
||||
Assert.Equal("CHF", roundtrip.SalesCurrency);
|
||||
Assert.Equal(new DateTime(2026, 6, 10), roundtrip.PostingDate);
|
||||
Assert.Equal(new DateTime(2026, 6, 11), roundtrip.InvoiceDate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadLatestSiteAuditCsvRecordsAsync_Reads_New_Name_Before_Legacy_Name()
|
||||
{
|
||||
var service = new ExportAuditCsvService();
|
||||
var settings = new ExportSettings
|
||||
{
|
||||
AuditCsvEnabled = true,
|
||||
LocalSiteExportFolder = _tempDirectory
|
||||
};
|
||||
var site = new Site { TSC = "TRSE", Land = "Spanien" };
|
||||
|
||||
var legacyPath = await service.WriteSiteAuditCsvAsync(
|
||||
site,
|
||||
settings,
|
||||
"MANUAL_EXCEL",
|
||||
_tempDirectory,
|
||||
[
|
||||
new SalesRecord
|
||||
{
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
ExtractionDate = new DateTime(2026, 6, 10),
|
||||
Tsc = "TRSE",
|
||||
Land = "Spanien",
|
||||
InvoiceNumber = "NEW",
|
||||
SalesPriceValue = 20m
|
||||
}
|
||||
]);
|
||||
var oldPath = Path.Combine(_tempDirectory, "Sales_TRSE_2026-06-10.csv");
|
||||
File.Move(legacyPath!, oldPath);
|
||||
File.SetLastWriteTimeUtc(oldPath, new DateTime(2026, 6, 10, 8, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
var newPath = await service.WriteSiteAuditCsvAsync(
|
||||
site,
|
||||
settings,
|
||||
"MANUAL_EXCEL",
|
||||
_tempDirectory,
|
||||
[
|
||||
new SalesRecord
|
||||
{
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
ExtractionDate = new DateTime(2026, 6, 11),
|
||||
Tsc = "TRSE",
|
||||
Land = "Spanien",
|
||||
InvoiceNumber = "PROCESSED",
|
||||
SalesPriceValue = 30m
|
||||
}
|
||||
]);
|
||||
File.SetLastWriteTimeUtc(newPath!, new DateTime(2026, 6, 11, 8, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
var records = await service.ReadLatestSiteAuditCsvRecordsAsync(settings);
|
||||
|
||||
var record = Assert.Single(records);
|
||||
Assert.Equal("PROCESSED", record.InvoiceNumber);
|
||||
Assert.Equal(30m, record.SalesPriceValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CentralSalesDataProvider_Uses_AuditCsv_When_Configured()
|
||||
{
|
||||
var csvService = new ExportAuditCsvService();
|
||||
await csvService.WriteSiteAuditCsvAsync(
|
||||
new Site { TSC = "TRUK", Land = "England" },
|
||||
new ExportSettings
|
||||
{
|
||||
AuditCsvEnabled = true,
|
||||
LocalSiteExportFolder = _tempDirectory,
|
||||
LocalAuditCsvFolder = Path.Combine(_tempDirectory, "ignored")
|
||||
},
|
||||
"MANUAL_EXCEL",
|
||||
_tempDirectory,
|
||||
[
|
||||
new SalesRecord
|
||||
{
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
ExtractionDate = new DateTime(2026, 6, 11),
|
||||
Tsc = "TRUK",
|
||||
Land = "England",
|
||||
InvoiceNumber = "UK-1",
|
||||
SalesPriceValue = 10m,
|
||||
SalesCurrency = "GBP",
|
||||
InvoiceDate = new DateTime(2026, 1, 1)
|
||||
}
|
||||
]);
|
||||
|
||||
await using var connection = new SqliteConnection("DataSource=:memory:");
|
||||
await connection.OpenAsync();
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
await using (var db = new AppDbContext(options))
|
||||
{
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
db.ExportSettings.Add(new ExportSettings
|
||||
{
|
||||
UseAuditCsvAsCentralSource = true,
|
||||
LocalSiteExportFolder = _tempDirectory,
|
||||
LocalAuditCsvFolder = Path.Combine(_tempDirectory, "ignored")
|
||||
});
|
||||
db.Sites.Add(new Site
|
||||
{
|
||||
Id = 1,
|
||||
Schema = "DB",
|
||||
TSC = "TRDB",
|
||||
Land = "DB",
|
||||
SourceSystem = "DB",
|
||||
IsActive = true
|
||||
});
|
||||
db.CentralSalesRecords.Add(new CentralSalesRecord
|
||||
{
|
||||
StoredAtUtc = DateTime.UtcNow,
|
||||
SiteId = 1,
|
||||
SourceSystem = "DB",
|
||||
ExtractionDate = new DateTime(2026, 6, 11),
|
||||
Tsc = "TRDB",
|
||||
InvoiceNumber = "DB-1",
|
||||
Land = "DB",
|
||||
DocumentType = "INV"
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var dbFactory = new TestDbContextFactory(options);
|
||||
var centralService = new CentralSalesRecordService(dbFactory, new NullAppEventLogService());
|
||||
var provider = new CentralSalesDataProvider(dbFactory, centralService, csvService);
|
||||
|
||||
var records = await provider.GetRecordsAsync();
|
||||
|
||||
var record = Assert.Single(records);
|
||||
Assert.Equal("TRUK", record.Tsc);
|
||||
Assert.Equal("UK-1", record.InvoiceNumber);
|
||||
Assert.Equal(10m, record.SalesPriceValue);
|
||||
}
|
||||
|
||||
private sealed class NullAppEventLogService : IAppEventLogService
|
||||
{
|
||||
public Task WriteAsync(string category, string message, string level = "Info", int? siteId = null, string? land = null, string? details = null)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task WriteDebugAsync(string category, string message, int? siteId = null, string? land = null, string? details = null)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
|
||||
{
|
||||
private readonly DbContextOptions<AppDbContext> _options;
|
||||
|
||||
public TestDbContextFactory(DbContextOptions<AppDbContext> options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public AppDbContext CreateDbContext() => new(_options);
|
||||
|
||||
public Task<AppDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new AppDbContext(_options));
|
||||
}
|
||||
}
|
||||
@@ -350,6 +350,50 @@ public class ManagementCockpitServiceTests : IDisposable
|
||||
Assert.Contains(result.DataQualityRows, row => row.Issue == "Nullwerte im Finance-Wert" && row.Count == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeFinanceSummaryAsync_Keeps_Reference_Only_Countries_In_Expert_Mode()
|
||||
{
|
||||
await using (var db = await _dbFactory.CreateDbContextAsync())
|
||||
{
|
||||
db.FinanceReferences.RemoveRange(db.FinanceReferences);
|
||||
db.FinanceReferences.AddRange(
|
||||
new FinanceReference
|
||||
{
|
||||
Key = "DE",
|
||||
Label = "Trafag DE",
|
||||
Year = 2025,
|
||||
LocalCurrencyValue = 120m,
|
||||
IsActive = true
|
||||
},
|
||||
new FinanceReference
|
||||
{
|
||||
Key = "IT",
|
||||
Label = "Trafag IT",
|
||||
Year = 2025,
|
||||
LocalCurrencyValue = 7669840m,
|
||||
IsActive = true
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await SeedCentralRowsAsync(
|
||||
CreateRow("MANUAL_EXCEL", "Deutschland", "TRDE", "INV-1", "EUR", 100m, new DateTime(2025, 1, 10)));
|
||||
|
||||
var result = await _service.AnalyzeFinanceSummaryAsync(2025, null, null);
|
||||
|
||||
var italy = Assert.Single(result.CountryRows, row => row.CountryKey == "IT");
|
||||
Assert.Equal(7669840m, italy.ReferenceValue);
|
||||
Assert.Equal(0m, italy.NetSalesActual);
|
||||
Assert.Equal(0, italy.TotalRows);
|
||||
Assert.Equal("Keine Daten", italy.Status);
|
||||
Assert.Contains("IT", result.CountryOptions);
|
||||
|
||||
var filteredResult = await _service.AnalyzeFinanceSummaryAsync(2025, "IT", null);
|
||||
var filteredItaly = Assert.Single(filteredResult.CountryRows);
|
||||
Assert.Equal("IT", filteredItaly.CountryKey);
|
||||
Assert.Equal(7669840m, filteredItaly.ReferenceValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeFinanceSummaryAsync_Builds_Central_Product_Assignment_Tab_Data()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
using TrafagSalesExporter.Services;
|
||||
using TrafagSalesExporter.Services.DataSources;
|
||||
|
||||
namespace TrafagSalesExporter.Tests;
|
||||
|
||||
public sealed class SiteExportServiceTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDirectory;
|
||||
|
||||
public SiteExportServiceTests()
|
||||
{
|
||||
_tempDirectory = Path.Combine("C:\\TMP", $"trafag-site-export-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDirectory))
|
||||
Directory.Delete(_tempDirectory, recursive: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_Uploads_AuditCsv_To_Same_SharePoint_Target_As_Excel()
|
||||
{
|
||||
await using var connection = new SqliteConnection("DataSource=:memory:");
|
||||
await connection.OpenAsync();
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
|
||||
await using (var db = new AppDbContext(options))
|
||||
{
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
db.ExportSettings.Add(new ExportSettings
|
||||
{
|
||||
AuditCsvEnabled = true,
|
||||
LocalSiteExportFolder = _tempDirectory
|
||||
});
|
||||
db.SharePointConfigs.Add(new SharePointConfig
|
||||
{
|
||||
TenantId = "tenant",
|
||||
ClientId = "client",
|
||||
ClientSecret = "secret",
|
||||
SiteUrl = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform",
|
||||
ExportFolder = "Import/Finance"
|
||||
});
|
||||
db.SourceSystemDefinitions.Add(new SourceSystemDefinition
|
||||
{
|
||||
Code = "MANUAL_EXCEL",
|
||||
DisplayName = "Manual Excel",
|
||||
ConnectionKind = SourceSystemConnectionKinds.ManualExcel,
|
||||
IsActive = true
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var sharePoint = new RecordingSharePointUploadService();
|
||||
var service = new SiteExportService(
|
||||
new TestDbContextFactory(options),
|
||||
new FixedDataSourceAdapterResolver(new FixedDataSourceAdapter(new DataSourceFetchResult
|
||||
{
|
||||
Records =
|
||||
[
|
||||
new SalesRecord
|
||||
{
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
ExtractionDate = new DateTime(2026, 6, 11, 8, 0, 0, DateTimeKind.Utc),
|
||||
Tsc = "TRSE",
|
||||
Land = "Spanien",
|
||||
InvoiceNumber = "ES-1",
|
||||
SalesPriceValue = 100m,
|
||||
SalesCurrency = "EUR",
|
||||
InvoiceDate = new DateTime(2026, 6, 10),
|
||||
DocumentType = "Invoice"
|
||||
}
|
||||
],
|
||||
SharePointUploadFolderOverride = "Import/Finance/Spanien",
|
||||
SharePointUploadLandOverride = string.Empty
|
||||
})),
|
||||
new FileWritingExcelExportService(),
|
||||
sharePoint,
|
||||
new NoopRecordTransformationService(),
|
||||
new NoopCentralSalesRecordService(),
|
||||
new ExportAuditCsvService(),
|
||||
new NoopAppEventLogService(),
|
||||
NullLogger<SiteExportService>.Instance);
|
||||
|
||||
var result = await service.ExportAsync(new Site
|
||||
{
|
||||
Id = 7,
|
||||
TSC = "TRSE",
|
||||
Land = "Spanien",
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
IsActive = true
|
||||
});
|
||||
|
||||
Assert.NotNull(result.FilePath);
|
||||
Assert.True(File.Exists(result.FilePath));
|
||||
var auditCsv = Directory.GetFiles(_tempDirectory, "Sales_ProcessedMergeInput_TRSE_*.csv").Single();
|
||||
Assert.True(File.Exists(auditCsv));
|
||||
|
||||
Assert.Equal(2, sharePoint.Uploads.Count);
|
||||
Assert.EndsWith(".xlsx", sharePoint.Uploads[0].FileName, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.EndsWith(".csv", sharePoint.Uploads[1].FileName, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.All(sharePoint.Uploads, upload =>
|
||||
{
|
||||
Assert.Equal("Import/Finance/Spanien", upload.ExportFolder);
|
||||
Assert.Equal(string.Empty, upload.Land);
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
|
||||
{
|
||||
private readonly DbContextOptions<AppDbContext> _options;
|
||||
|
||||
public TestDbContextFactory(DbContextOptions<AppDbContext> options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public AppDbContext CreateDbContext() => new(_options);
|
||||
|
||||
public Task<AppDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new AppDbContext(_options));
|
||||
}
|
||||
|
||||
private sealed class FixedDataSourceAdapterResolver : IDataSourceAdapterResolver
|
||||
{
|
||||
private readonly IDataSourceAdapter _adapter;
|
||||
|
||||
public FixedDataSourceAdapterResolver(IDataSourceAdapter adapter)
|
||||
{
|
||||
_adapter = adapter;
|
||||
}
|
||||
|
||||
public IDataSourceAdapter Resolve(string connectionKind) => _adapter;
|
||||
}
|
||||
|
||||
private sealed class FixedDataSourceAdapter : IDataSourceAdapter
|
||||
{
|
||||
private readonly DataSourceFetchResult _result;
|
||||
|
||||
public FixedDataSourceAdapter(DataSourceFetchResult result)
|
||||
{
|
||||
_result = result;
|
||||
}
|
||||
|
||||
public string ConnectionKind => SourceSystemConnectionKinds.ManualExcel;
|
||||
|
||||
public Task<DataSourceFetchResult> FetchAsync(DataSourceFetchContext context)
|
||||
=> Task.FromResult(_result);
|
||||
}
|
||||
|
||||
private sealed class FileWritingExcelExportService : IExcelExportService
|
||||
{
|
||||
public string CreateExcelFile(string outputDirectory, string tsc, DateTime fileDate, List<SalesRecord> records)
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
var path = Path.Combine(outputDirectory, $"Sales_{tsc}_{fileDate:yyyy-MM-dd}.xlsx");
|
||||
File.WriteAllText(path, "excel");
|
||||
return path;
|
||||
}
|
||||
|
||||
public string CreateConsolidatedExcelFile(string outputDirectory, DateTime fileDate, List<SalesRecord> records)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public string CreateGenericExcelFile(string outputDirectory, string filePrefix, DateTime fileDate, string worksheetName, IReadOnlyList<IReadOnlyDictionary<string, object?>> rows)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private sealed class RecordingSharePointUploadService : ISharePointUploadService
|
||||
{
|
||||
public List<UploadCall> Uploads { get; } = [];
|
||||
|
||||
public Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath, bool uploadTimestampedCopyIfLocked = false)
|
||||
{
|
||||
Uploads.Add(new UploadCall(exportFolder, land, Path.GetFileName(localFilePath)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<string> DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<SharePointFileReference> ResolveLatestFileInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<IReadOnlyList<SharePointFileReference>> ResolveManualImportFilesInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed record UploadCall(string ExportFolder, string Land, string FileName);
|
||||
|
||||
private sealed class NoopRecordTransformationService : IRecordTransformationService
|
||||
{
|
||||
public void Apply(List<SalesRecord> records, IEnumerable<FieldTransformationRule> rules)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NoopCentralSalesRecordService : ICentralSalesRecordService
|
||||
{
|
||||
public Task ReplaceForSiteAsync(Site site, IEnumerable<SalesRecord> records, Action<string>? updateStatus = null)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task<List<SalesRecord>> GetAllAsync()
|
||||
=> Task.FromResult(new List<SalesRecord>());
|
||||
}
|
||||
|
||||
private sealed class NoopAppEventLogService : IAppEventLogService
|
||||
{
|
||||
public Task WriteAsync(string category, string message, string level = "Info", int? siteId = null, string? land = null, string? details = null)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task WriteDebugAsync(string category, string message, int? siteId = null, string? land = null, string? details = null)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,69 @@
|
||||
# Deployment / IIS Handoff 2026-05-19
|
||||
|
||||
Letzter Nachtrag: 2026-06-10
|
||||
Letzter Nachtrag: 2026-06-11
|
||||
|
||||
## Nachtrag 2026-06-11 Deploy Einkaufs-Uebersetzungen
|
||||
|
||||
Deploy-Inhalt:
|
||||
|
||||
- Commit `1dbaa66 Add purchasing translations`.
|
||||
- `Services/UiTextService.cs` ergaenzt fuer den Bereich Einkauf:
|
||||
- Hauptmenue `Einkauf` und `Einkauf Dashboard`.
|
||||
- Einkaufsdashboard: Hero, Zeitraumfilter, KPI-Karten, Spend/offene Bestellungen/Kontrakte/Lieferanten, Ideenbereich, Kennzahlen-Katalog, PBIX-Vorlage und 3D-Simulation.
|
||||
- `Einkauf > Datenquellen`: Verbindung, Quellen, Join-Fluss, Mapping, Basisstatus und Snackbar-Meldungen.
|
||||
- Sprachen: Spanisch, Italienisch und Hindi.
|
||||
- Technische Namen und Feldnamen wie `EKKO`, `EKPO`, `EKET`, Entity-Sets, SAP-Felder, TSC und Dateimuster bleiben bewusst unveraendert.
|
||||
- Audit-CSV-Hilfstext ist im Spanisch-/Hindi-Modus nicht mehr englisch.
|
||||
|
||||
Validierung:
|
||||
|
||||
- Saubere Worktree-Kopie unter `C:\TMP\trafag-translation-test-20260611\TrafagSalesExporter`.
|
||||
- Lokaler Entwicklungs-Worktree baute wegen bereits vorher fehlender `Bild.png`/`erg.png` nicht vollstaendig; diese offenen lokalen Loeschungen wurden nicht veraendert.
|
||||
- Im sauberen Worktree wurde die Content-DB in den Projektordner kopiert, danach:
|
||||
- `dotnet test TrafagSalesExporter.sln --verbosity minimal`
|
||||
- Ergebnis: `92/92` Tests gruen.
|
||||
|
||||
Publish:
|
||||
|
||||
- `app_offline.htm` vor Publish gesetzt und nach Publish entfernt.
|
||||
- Publish-Befehl:
|
||||
|
||||
```powershell
|
||||
dotnet publish TrafagSalesExporter.csproj -c Release -o "\\trch-webapp-bidashboard.trafagch.local\BiDashboard$" --no-restore
|
||||
```
|
||||
|
||||
- `BiDashboard.dll` Zeitstempel nach Deploy: `11.06.2026 12:30:27`.
|
||||
- Bekannte Warnungen bleiben unveraendert: CS0649 fuer Username-Felder und MudBlazor MUD0002 Analyzer-Warnungen.
|
||||
|
||||
Hinweis:
|
||||
|
||||
- Die im Entwicklungsarbeitsbaum offenen Loeschungen `Bild.png` und `erg.png` sowie untracked Arbeitsdateien wurden fuer diesen Deploy nicht committed und nicht veraendert.
|
||||
|
||||
## Nachtrag 2026-06-11 Deploy Finance-Schulung und Dashboard-UI
|
||||
|
||||
Deploy-Inhalt:
|
||||
|
||||
- Commit `f751295 Update finance training and dashboard UI`.
|
||||
- Export Dashboard: VU-/Manometer-Grafik als fixes SVG mit Skala und Beschriftung, damit die Grafik nicht mehr nach unten in die Tabelle verschiebt.
|
||||
- Management/Finance-Cockpit: doppelte obere Tab-/Menubaender reduziert; Navigation bleibt ueber linke Reiter/Query-Parameter nutzbar.
|
||||
- Finance-Schulung aktualisiert:
|
||||
- `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md`
|
||||
- `docs/FINANCE_COCKPIT_ANLEITUNG_FINANZ_2026-05-20.docx`
|
||||
- Prozessgrafiken fuer Exportfluss, Audit-CSV-Auswertungsquelle und Waehrungs-/Kursfluss.
|
||||
- Schulung enthaelt 4 Beispielzeilen je Land mit Mapping/Transformation, Merge-Wert, zentraler Summe und Dashboard-Fluss.
|
||||
|
||||
Validierung:
|
||||
|
||||
- Saubere Worktree-Kopie unter `C:\TMP\trafag-deploy-20260611115948`.
|
||||
- `dotnet test TrafagSalesExporter.sln --verbosity minimal`: `92/92` Tests gruen.
|
||||
- Publish:
|
||||
- `dotnet publish TrafagSalesExporter.csproj -c Release -o \\trch-webapp-bidashboard.trafagch.local\BiDashboard$ --no-restore`
|
||||
- `app_offline.htm` vor Publish gesetzt und nach Publish entfernt.
|
||||
- `BiDashboard.dll` Zeitstempel nach Deploy: `11.06.2026 12:04:53`.
|
||||
|
||||
Hinweis:
|
||||
|
||||
- Die im Entwicklungsarbeitsbaum offenen Loeschungen `Bild.png` und `erg.png` wurden fuer diesen Deploy nicht committed und nicht veraendert.
|
||||
|
||||
## Nachtrag 2026-06-10 Deploy Produktsparten-Fallback `ProductDivisionMapSet`
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="680" viewBox="0 0 1180 680" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Umschaltung zentrale Finance-Auswertungsquelle</title>
|
||||
<desc id="desc">Zeigt, wie Finance-Auswertungen entweder aus CentralSalesRecords oder aus verarbeiteten Audit-CSV je Standort lesen.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#334155" />
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 29px Arial, sans-serif; fill: #0f172a; }
|
||||
.subtitle { font: 400 16px Arial, sans-serif; fill: #475569; }
|
||||
.box-title { font: 700 16px Arial, sans-serif; fill: #0f172a; }
|
||||
.box-text { font: 400 13px Arial, sans-serif; fill: #334155; }
|
||||
.small { font: 400 12px Arial, sans-serif; fill: #64748b; }
|
||||
.line { stroke: #334155; stroke-width: 2.2; fill: none; marker-end: url(#arrow); }
|
||||
.muted-line { stroke: #94a3b8; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }
|
||||
.settings { fill: #e0f2fe; stroke: #0284c7; }
|
||||
.db { fill: #eef2ff; stroke: #4f46e5; }
|
||||
.csv { fill: #ecfdf5; stroke: #059669; }
|
||||
.output { fill: #fff7ed; stroke: #ea580c; }
|
||||
.guard { fill: #fef2f2; stroke: #dc2626; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect x="0" y="0" width="1180" height="680" fill="#ffffff" />
|
||||
<text x="48" y="54" class="title">Zentrale Auswertungsquelle: DB oder verarbeitete Audit-CSV</text>
|
||||
<text x="48" y="82" class="subtitle">Der Schalter liegt unter Einstellungen > Export Einstellungen > Audit-CSV / nachvollziehbarer Datenfluss.</text>
|
||||
|
||||
<rect x="60" y="132" width="270" height="150" rx="8" class="settings" />
|
||||
<text x="84" y="166" class="box-title">Settings</text>
|
||||
<text x="84" y="195" class="box-text">Audit-CSV je Standort schreiben</text>
|
||||
<text x="84" y="220" class="box-text">Zentrale Auswertung aus Audit-CSV</text>
|
||||
<text x="84" y="249" class="small">Pfad: Lokaler Standardpfad Standort-Dateien</text>
|
||||
|
||||
<line x1="330" y1="207" x2="406" y2="207" class="line" />
|
||||
|
||||
<rect x="414" y="86" width="270" height="140" rx="8" class="db" />
|
||||
<text x="438" y="120" class="box-title">Standard-Modus</text>
|
||||
<text x="438" y="149" class="box-text">UseAuditCsvAsCentralSource = false</text>
|
||||
<text x="438" y="177" class="box-text">Auswertung liest CentralSalesRecords.</text>
|
||||
<text x="438" y="202" class="small">Schnell und operativ</text>
|
||||
|
||||
<rect x="414" y="292" width="270" height="160" rx="8" class="csv" />
|
||||
<text x="438" y="326" class="box-title">Audit-Modus</text>
|
||||
<text x="438" y="355" class="box-text">UseAuditCsvAsCentralSource = true</text>
|
||||
<text x="438" y="383" class="box-text">Je TSC wird die neueste</text>
|
||||
<text x="438" y="404" class="box-text">Sales_ProcessedMergeInput_*.csv gelesen.</text>
|
||||
<text x="438" y="431" class="small">Lesbar fuer Finance und Revision</text>
|
||||
|
||||
<line x1="684" y1="156" x2="760" y2="238" class="line" />
|
||||
<line x1="684" y1="372" x2="760" y2="302" class="line" />
|
||||
|
||||
<rect x="768" y="214" width="300" height="136" rx="8" class="output" />
|
||||
<text x="792" y="248" class="box-title">Gleiche Finance-Logik</text>
|
||||
<text x="792" y="277" class="box-text">FinanceRuleEngine</text>
|
||||
<text x="792" y="300" class="box-text">Spartenanalyse</text>
|
||||
<text x="792" y="323" class="box-text">Waehrungsanalyse bei Zielwaehrung</text>
|
||||
|
||||
<line x1="918" y1="350" x2="918" y2="416" class="line" />
|
||||
|
||||
<rect x="730" y="424" width="376" height="124" rx="8" class="output" />
|
||||
<text x="754" y="458" class="box-title">Ausgaben</text>
|
||||
<text x="754" y="487" class="box-text">Dashboard / Management Analyse</text>
|
||||
<text x="754" y="510" class="box-text">Soll/Ist-Vergleich</text>
|
||||
<text x="754" y="533" class="box-text">Zentrale Excel Sales_All_<Datum>.xlsx</text>
|
||||
|
||||
<rect x="60" y="486" width="560" height="110" rx="8" class="guard" />
|
||||
<text x="84" y="520" class="box-title">Pruefregel im Audit-Modus</text>
|
||||
<text x="84" y="549" class="box-text">Fuer alle relevanten TSC muessen aktuelle Sales_ProcessedMergeInput_*.csv vorhanden sein.</text>
|
||||
<text x="84" y="574" class="small">Fehlt die CSV, kann die zentrale Auswertung keine belastbare CSV-Sicht bilden.</text>
|
||||
|
||||
<line x1="195" y1="282" x2="195" y2="478" class="muted-line" />
|
||||
<line x1="620" y1="541" x2="722" y2="501" class="muted-line" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -1,12 +1,13 @@
|
||||
# Finance Berechnungsformeln pro Land
|
||||
|
||||
Stand: 2026-06-01
|
||||
Stand: 2026-06-11
|
||||
|
||||
Nachtrag 2026-06-01:
|
||||
Nachtrag 2026-06-11:
|
||||
|
||||
- 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.
|
||||
- Zentrale Finance-Auswertungen koennen optional aus den neuesten `Sales_ProcessedMergeInput_*.csv` je TSC statt aus `CentralSalesRecords` lesen. Die Formel bleibt gleich; nur die Datenquelle wird per Setting umgeschaltet.
|
||||
|
||||
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.
|
||||
|
||||
@@ -14,7 +15,12 @@ Zweck: Dieses Dokument beschreibt die aktuell im Programm verwendeten Formeln fu
|
||||
|
||||
Die echte Webseite `/finance-cockpit/vergleich` und das Testprogramm `/finance` verwenden beide `FinanceReconciliationService`.
|
||||
|
||||
Quelle fuer den Ist-Wert ist immer `CentralSalesRecords`. Die Jahresabgrenzung ist:
|
||||
Quelle fuer den Ist-Wert ist die zentrale Auswertungsquelle:
|
||||
|
||||
- Standard: `CentralSalesRecords`.
|
||||
- Audit-Modus: neueste `Sales_ProcessedMergeInput_*.csv` je TSC.
|
||||
|
||||
Die Jahresabgrenzung ist:
|
||||
|
||||
```text
|
||||
Jahr = Year(PostingDate ?? InvoiceDate ?? ExtractionDate)
|
||||
@@ -26,7 +32,7 @@ Pro Land berechnet das Programm mehrere Kandidaten:
|
||||
|
||||
```text
|
||||
SalesPriceValue
|
||||
= Sum(CentralSalesRecords.SalesPriceValue)
|
||||
= Sum(SalesPriceValue aus zentraler Auswertungsquelle)
|
||||
|
||||
DocTotalFC - VatSumFC
|
||||
= Sum(DocumentTotalForeignCurrency - VatSumForeignCurrency)
|
||||
|
||||
Binary file not shown.
@@ -1,17 +1,28 @@
|
||||
# Finance Datenfluss fuer Andreas
|
||||
|
||||
Stand: 2026-06-08
|
||||
Stand: 2026-06-11
|
||||
|
||||
Zweck: Diese Notiz beschreibt den tatsaechlichen technischen Datenfluss im Finance Cockpit: wo Daten geholt werden, wann Felder veraendert werden, wann Wechselkurse wirken, wie die zentrale Excel entsteht und welche Quelle die Sparteninformationen liefert.
|
||||
|
||||
Fokus nur Wechselkurs/Kursanwendung: `docs/FINANCE_KURS_WORKFLOW_2026-06-09.md`.
|
||||
Aktuelle Finance-Schulung: `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md`.
|
||||
Prozessgrafiken:
|
||||
|
||||
- `docs/FINANCE_PROZESS_EXPORT_DASHBOARD_2026-06-11.svg`
|
||||
- `docs/FINANCE_AUDIT_CSV_QUELLE_2026-06-11.svg`
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Kurzfazit
|
||||
|
||||
- Finance Summary, Management Analyse und Spartenanalyse lesen nicht aus dem SharePoint-Excel, sondern direkt aus der App-Datenbank `CentralSalesRecords`.
|
||||
- Finance Summary, Management Analyse und Spartenanalyse lesen nicht aus dem SharePoint-Excel. Sie lesen entweder aus `CentralSalesRecords` oder, wenn aktiviert, aus den neuesten verarbeiteten Audit-CSV je Standort.
|
||||
- Fuer Finance/Revision gibt es einen Audit-CSV-Modus. Standortexporte koennen nach Mapping und Transformation je Standort eine CSV schreiben; per Setting koennen zentrale Excel, Finance Summary, Soll/Ist und Management-Analyse aus diesen CSV statt aus `CentralSalesRecords` lesen.
|
||||
- Die Audit-CSV heisst `Sales_ProcessedMergeInput_<TSC>_<yyyy-MM-dd>.csv`. Der Name markiert bewusst, dass es das verarbeitete Merge-Eingangsfile ist, nicht die originale Standortdatei.
|
||||
- Das SharePoint-Excel `Sales_All_*.xlsx` ist ein Export-/Ablageergebnis, nicht die Live-Quelle der Cockpit-Anzeige.
|
||||
- Jeder Standortexport ersetzt in `CentralSalesRecords` nur die Daten dieses Standorts.
|
||||
- Die zentrale Excel wird danach aus dem aktuellen Stand von `CentralSalesRecords` erzeugt.
|
||||
- Die zentrale Excel wird danach aus der aktuell gewaehlten zentralen Auswertungsquelle erzeugt: Standard `CentralSalesRecords`, optional Audit-CSV.
|
||||
- Wechselkurse veraendern den Standortexport und `CentralSalesRecords` normalerweise nicht. Sie wirken in Analyse-/Anzeige-Sichten, wenn eine Zielwaehrung wie CHF/EUR/USD ausgewaehlt ist, oder in explizit konfigurierten Transformationen.
|
||||
- Sparteninformationen kommen fuehrend aus SAP/TR-AG `ProductDivisionRefSet`. Aktuell werden sie beim ZSCHWEIZ-/CH-/AT-Export direkt mitgeladen. Andere Laender werden in der Analyse ueber ihre Materialnummer gegen diese TR-AG-Referenz gematcht.
|
||||
|
||||
@@ -32,13 +43,33 @@ Ablauf pro Standort:
|
||||
- `ManualExcelDataSourceAdapter` fuer Excel/CSV/SharePoint-Dateien.
|
||||
3. Rohdaten werden als `SalesRecord`-Liste aufgebaut.
|
||||
4. Aktive `FieldTransformationRules` fuer das Quellsystem werden angewendet.
|
||||
5. Eine lokale Standort-Excel `Sales_<TSC>_<Datum>.xlsx` wird erzeugt.
|
||||
6. `CentralSalesRecords` wird fuer diesen Standort ersetzt:
|
||||
5. Falls `Audit-CSV je Standort schreiben` aktiv ist, wird eine verarbeitete CSV geschrieben:
|
||||
- Datei: `Sales_ProcessedMergeInput_<TSC>_<Datum>.csv`.
|
||||
- Ordner: gleicher lokaler Ordner wie die Standort-Excel.
|
||||
- Inhalt: Daten nach Mapping und Transformation.
|
||||
6. Eine lokale Standort-Excel `Sales_<TSC>_<Datum>.xlsx` wird erzeugt.
|
||||
7. `CentralSalesRecords` wird fuer diesen Standort ersetzt:
|
||||
- alte Saetze mit `SiteId = Standort` loeschen.
|
||||
- neue Saetze einfuegen.
|
||||
7. Falls SharePoint komplett konfiguriert ist, wird die Standort-Excel nach SharePoint hochgeladen.
|
||||
8. Falls SharePoint komplett konfiguriert ist, werden Standort-Excel und Audit-CSV in den Landesordner hochgeladen.
|
||||
|
||||
Wichtig: Die Reihenfolge ist zuerst Daten holen, dann Transformationen, dann lokale Excel, dann zentrale Tabelle, dann SharePoint-Upload. Der SharePoint-Upload entscheidet nicht, was in der Cockpit-Anzeige erscheint.
|
||||
Wichtig: Die Reihenfolge ist zuerst Daten holen, dann Transformationen, dann Audit-CSV, dann lokale Excel, dann zentrale Tabelle, dann SharePoint-Upload. Der SharePoint-Upload entscheidet nicht, was in der Cockpit-Anzeige erscheint.
|
||||
|
||||
## Zentrale Auswertungsquelle
|
||||
|
||||
Der Schalter liegt in der App unter:
|
||||
|
||||
```text
|
||||
Einstellungen > Export Einstellungen > Audit-CSV / nachvollziehbarer Datenfluss
|
||||
```
|
||||
|
||||
| Schalter | Wirkung |
|
||||
| --- | --- |
|
||||
| `Audit-CSV je Standort schreiben` | Standortexport schreibt `Sales_ProcessedMergeInput_*.csv` nach Mapping und Transformation. |
|
||||
| `Zentrale Auswertung aus Audit-CSV` | Finance Summary, Management Analyse, Soll/Ist und zentrale Excel lesen die neuesten Audit-CSV je TSC statt `CentralSalesRecords`. |
|
||||
| `Lokaler Standardpfad Standort-Dateien` | Ordner fuer Standort-Excel und Audit-CSV. |
|
||||
|
||||
Wenn `Zentrale Auswertung aus Audit-CSV` aktiv ist, sucht die App im Standort-Exportordner je TSC die neueste passende CSV. Wenn keine `Sales_ProcessedMergeInput_*.csv` vorhanden ist, ist der Audit-Modus nicht auswertbar.
|
||||
|
||||
## Datenquellen pro Quelltyp
|
||||
|
||||
@@ -314,7 +345,9 @@ Ausloeser:
|
||||
|
||||
Ablauf:
|
||||
|
||||
1. `ConsolidatedExportService` liest alle Saetze aus `CentralSalesRecords`.
|
||||
1. `ConsolidatedExportService` liest alle Saetze aus der zentralen Auswertungsquelle:
|
||||
- Standard: `CentralSalesRecords`.
|
||||
- Audit-Modus: neueste `Sales_ProcessedMergeInput_*.csv` je TSC.
|
||||
2. `ExcelExportService.CreateConsolidatedExcelFile(...)` erzeugt `Sales_All_<Datum>.xlsx`.
|
||||
3. Die Datei wird lokal geschrieben.
|
||||
4. Falls SharePoint konfiguriert ist, wird sie hochgeladen.
|
||||
@@ -341,14 +374,16 @@ Wichtig:
|
||||
|
||||
- `Finance Summary` im Excel wird beim Schreiben aus den Records berechnet.
|
||||
- Es liest nicht aus einem vorherigen SharePoint-Excel.
|
||||
- Je nach Setting sind diese Records entweder DB-Eintraege oder die neuesten verarbeiteten Audit-CSV.
|
||||
- Wechselkurs-Zielwaehrung aus der UI wird dabei nicht angewendet.
|
||||
|
||||
## Finance Summary und Spartenanalyse in der App
|
||||
|
||||
Die App-Anzeigen lesen direkt aus:
|
||||
Die App-Anzeigen lesen direkt aus der zentralen Auswertungsquelle:
|
||||
|
||||
```text
|
||||
CentralSalesRecords
|
||||
Standard: CentralSalesRecords
|
||||
Audit-Modus: neueste Sales_ProcessedMergeInput_*.csv je TSC
|
||||
```
|
||||
|
||||
Nicht aus:
|
||||
@@ -359,9 +394,9 @@ SharePoint Sales_All_*.xlsx
|
||||
|
||||
Das bedeutet:
|
||||
|
||||
- Lokal zeigt die App lokale DB-Daten.
|
||||
- Publizierter Server zeigt Server-DB-Daten.
|
||||
- Wenn lokale und Server-DB gleich sind, sehen beide gleich aus.
|
||||
- Lokal zeigt die App lokale DB-Daten oder lokale Audit-CSV, je nach Setting.
|
||||
- Publizierter Server zeigt Server-DB-Daten oder Server-Audit-CSV, je nach Setting.
|
||||
- Wenn lokale und Server-Auswertungsquelle gleich sind, sehen beide gleich aus.
|
||||
- Ein SharePoint-Upload veraendert die App-Anzeige nicht.
|
||||
|
||||
## Spartenanalyse: genaue Logik
|
||||
@@ -432,21 +467,26 @@ SalesRecord-Liste
|
||||
+-- FieldTransformationRules anwenden
|
||||
| -> optional Feldkopien, FirstNonEmpty, ConvertCurrency
|
||||
|
|
||||
+-- optional Audit-CSV Sales_ProcessedMergeInput_<TSC>_<Datum>.csv schreiben
|
||||
| -> verarbeitete Daten fuer Finance/Revision
|
||||
|
|
||||
+-- Standort-Excel Sales_<TSC>_<Datum>.xlsx lokal schreiben
|
||||
|
|
||||
+-- CentralSalesRecords fuer SiteId ersetzen
|
||||
|
|
||||
+-- Standort-Excel optional nach SharePoint hochladen
|
||||
+-- Standort-Excel und Audit-CSV optional nach SharePoint hochladen
|
||||
|
||||
Finance Summary / Spartenanalyse
|
||||
|
|
||||
+-- liest CentralSalesRecords
|
||||
+-- liest zentrale Auswertungsquelle
|
||||
| -> Standard: CentralSalesRecords
|
||||
| -> optional: neueste Sales_ProcessedMergeInput_*.csv je TSC
|
||||
+-- FinanceRuleEngine rechnet Include/Exclude/Net Sales Actual
|
||||
+-- Spartenanalyse matched lokale Materialien gegen TR-AG-Referenz aus CentralSalesRecords
|
||||
+-- Spartenanalyse matched lokale Materialien gegen TR-AG-Referenz aus den Records
|
||||
|
||||
Zentrale Excel
|
||||
|
|
||||
+-- liest CentralSalesRecords
|
||||
+-- liest zentrale Auswertungsquelle
|
||||
+-- erzeugt Sales_All_<Datum>.xlsx lokal
|
||||
+-- erzeugt Finance Summary / Finance Details im Excel
|
||||
+-- laedt Datei optional nach SharePoint
|
||||
@@ -455,10 +495,10 @@ Zentrale Excel
|
||||
## Wichtige Klarstellungen fuer Finance
|
||||
|
||||
1. SharePoint ist Ablage und Quelle fuer manuelle Dateien, aber nicht Live-Quelle der Finance Summary.
|
||||
2. `CentralSalesRecords` ist der operative zentrale Datenbestand der App.
|
||||
2. `CentralSalesRecords` ist der operative zentrale Datenbestand der App, solange nicht Audit-CSV als zentrale Auswertungsquelle aktiv ist.
|
||||
3. Sparten kommen fachlich aus TR-AG/SAP `ProductDivisionRefSet`, nicht aus lokalen ERP-Sparten.
|
||||
4. CH/AT bekommen Spartenfelder direkt beim ZSCHWEIZ-Export.
|
||||
5. Andere Laender bekommen Sparten in der Analyse nur, wenn ihre Materialnummern zur TR-AG-Referenz matchen.
|
||||
6. Wechselkurse sind keine stille Vorverarbeitung fuer den Standard-Soll/Ist-Abgleich.
|
||||
7. `Mixed` bedeutet: mehrere Waehrungen im Filter. Prozentwerte auf `Mixed` sind nur eingeschraenkt interpretierbar; fuer belastbare Spartenanteile nach Wert muss Land oder Waehrung gefiltert werden.
|
||||
8. Die zentrale Excel wird nach den Standortexporten aus `CentralSalesRecords` erstellt. Sie ist Ergebnis, nicht Eingang.
|
||||
8. Die zentrale Excel wird nach den Standortexporten aus der gewaehlten zentralen Auswertungsquelle erstellt. Sie ist Ergebnis, nicht Eingang.
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
# Finance Kurs-Workflow
|
||||
|
||||
Stand: 2026-06-09
|
||||
Stand: 2026-06-11
|
||||
|
||||
Zweck: Diese Doku beschreibt isoliert den Weg eines Umrechnungskurses vom einzelnen Land bis zur Analyse eines zentralen Dashboard-Wertes. Sie ersetzt nicht die allgemeine Finance-Datenflussdoku, sondern schneidet nur das Thema Kurs/Waehrung heraus.
|
||||
Aktuelle Finance-Schulung: `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md`.
|
||||
|
||||
Visualisierung: `docs/FINANCE_KURS_WORKFLOW_2026-06-09.svg`
|
||||
Aktuelle Kurs-/Waehrungsgrafik: `docs/FINANCE_WAEHRUNG_KURSFLUSS_2026-06-11.svg`
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Kurzfazit
|
||||
|
||||
- Der Standortimport rechnet Werte normalerweise nicht ueber die App-Kurstabelle um.
|
||||
- `CentralSalesRecords` speichert die Werte und Waehrungen so, wie sie nach Import und optionalen Transformationen vorliegen.
|
||||
- Wenn Audit-CSV als zentrale Auswertungsquelle aktiv ist, enthalten die `Sales_ProcessedMergeInput_*.csv` dieselben verarbeiteten Werte nach Mapping und Transformation.
|
||||
- `DocumentRate` ist ein Quellfeld aus SAP/B1/OData, kein automatisch angewendeter App-Kurs.
|
||||
- Die fuehrende `Finance Summary` und das zentrale Excel nutzen Hauswaehrung je Land. Die App-Kurstabelle wird dort nicht still angewendet.
|
||||
- Eine echte App-Umrechnung passiert nur in Analyse-/Anzeige-Sichten mit Zielwaehrung, in einer expliziten `ConvertCurrency`-Transformation oder im separaten Budget-CHF-Kandidaten.
|
||||
@@ -29,6 +34,7 @@ Visualisierung: `docs/FINANCE_KURS_WORKFLOW_2026-06-09.svg`
|
||||
| App-Kurstabelle | Tabelle `CurrencyExchangeRates` mit `FromCurrency`, `ToCurrency`, `Rate`, `ValidFrom`, `ValidTo`, `Notes`, `IsActive`. |
|
||||
| Anzeige-Waehrung | Zielwaehrung in Analyse-Sichten, aktuell `NATIVE`, `CHF`, `EUR`, `USD`. |
|
||||
| Budgetkurs | Kurs mit `Notes = Budget <Jahr>`, z. B. `Budget 2025`, fuer separaten CHF-Kontrollkandidaten. |
|
||||
| Audit-CSV | Verarbeitete Standort-CSV `Sales_ProcessedMergeInput_<TSC>_<Datum>.csv`; optional zentrale Quelle fuer Dashboard und zentrale Excel. |
|
||||
|
||||
## Gesamtfluss
|
||||
|
||||
@@ -45,8 +51,15 @@ Land / Quellsystem
|
||||
|
|
||||
+-- Standort-Excel schreiben
|
||||
|
|
||||
+-- optional Audit-CSV schreiben
|
||||
| Sales_ProcessedMergeInput_<TSC>_<Datum>.csv
|
||||
|
|
||||
+-- CentralSalesRecords fuer Standort ersetzen
|
||||
|
|
||||
+-- zentrale Auswertungsquelle
|
||||
| Standard: CentralSalesRecords
|
||||
| optional: neueste Audit-CSV je TSC
|
||||
|
|
||||
+-- zentrale Excel / Finance Summary
|
||||
| Hauswaehrung, keine stille App-Kursumrechnung
|
||||
|
|
||||
@@ -111,6 +124,7 @@ Beim normalen Standortexport gilt:
|
||||
```text
|
||||
Daten holen
|
||||
-> Transformationen anwenden
|
||||
-> optional Audit-CSV Sales_ProcessedMergeInput_*.csv schreiben
|
||||
-> Standort-Excel schreiben
|
||||
-> CentralSalesRecords fuer diesen Standort ersetzen
|
||||
-> optional SharePoint Upload
|
||||
@@ -125,6 +139,8 @@ Ohne aktive `ConvertCurrency`-Transformation passiert keine App-Kursumrechnung.
|
||||
|
||||
Damit bleibt nachvollziehbar, ob ein Wert bereits vom Landessystem als Hauswaehrungswert geliefert wurde oder ob er spaeter nur in der Anzeige umgerechnet wurde.
|
||||
|
||||
Die Audit-CSV wird an derselben Stelle im Ablauf geschrieben: nach Mapping/Transformation und vor der zentralen Auswertung. Sie ist deshalb fuer Finance/Revision das lesbare Abbild des verarbeiteten Merge-Eingangs, nicht das originale Standortfile.
|
||||
|
||||
## Schritt 5: Fuehrende Finance Summary
|
||||
|
||||
Die fuehrende Finance Summary im Dashboard und das zentrale Excel-Blatt `Finance Summary` rechnen nicht automatisch in eine globale Zielwaehrung um.
|
||||
@@ -132,7 +148,9 @@ Die fuehrende Finance Summary im Dashboard und das zentrale Excel-Blatt `Finance
|
||||
Logik:
|
||||
|
||||
```text
|
||||
CentralSalesRecords
|
||||
zentrale Auswertungsquelle
|
||||
Standard: CentralSalesRecords
|
||||
optional: Sales_ProcessedMergeInput_*.csv
|
||||
-> FinanceRuleEngine
|
||||
-> Finance | Net Sales Actual
|
||||
-> Gruppierung nach Jahr, Land, Finance-Waehrung
|
||||
@@ -166,7 +184,7 @@ Die Kursanwendung fuer einen zentralen Dashboard-Wert passiert in der Management
|
||||
|
||||
Eingaben:
|
||||
|
||||
- Datenbasis: `CentralSalesRecords`.
|
||||
- Datenbasis: zentrale Auswertungsquelle, also `CentralSalesRecords` oder bei aktivem Audit-Modus die neuesten `Sales_ProcessedMergeInput_*.csv` je TSC.
|
||||
- Summenfeld: z. B. `SalesPriceValue`, `StandardCost`, `StandardCostTotal`, `Quantity`.
|
||||
- Anzeige-Waehrung: `NATIVE`, `CHF`, `EUR` oder `USD`.
|
||||
- Zeitraum/Filter: Jahr, Monat, Land, TSC.
|
||||
@@ -296,6 +314,8 @@ ORDER BY FromCurrency, ToCurrency, ValidFrom DESC;
|
||||
| ECB-Import | `Services/ExchangeRateImportService.cs` |
|
||||
| Settings/Kurspflege | `Services/SettingsPageService.cs`, `Components/Pages/Settings.razor` |
|
||||
| Standortexport-Reihenfolge | `Services/SiteExportService.cs` |
|
||||
| Audit-CSV schreiben/lesen | `Services/ExportAuditCsvService.cs` |
|
||||
| zentrale Quelle DB oder CSV | `Services/CentralSalesDataProvider.cs` |
|
||||
| zentrale Speicherung | `Services/CentralSalesRecordService.cs` |
|
||||
| zentrale Analyse mit Zielwaehrung | `Services/ManagementCockpitService.cs` |
|
||||
| Finance Summary ohne stille Umrechnung | `Services/ManagementCockpitService.cs`, `Services/ExcelExportService.cs` |
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="760" viewBox="0 0 1280 760" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Finance Prozessfluss vom Export bis Dashboard</title>
|
||||
<desc id="desc">Uebersicht: Standortdaten werden gelesen, gemappt, transformiert, optional als Audit-CSV geschrieben und danach fuer Dashboard, Soll/Ist und zentrale Excel verwendet.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#334155" />
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 30px Arial, sans-serif; fill: #0f172a; }
|
||||
.subtitle { font: 400 16px Arial, sans-serif; fill: #475569; }
|
||||
.lane-title { font: 700 15px Arial, sans-serif; fill: #334155; }
|
||||
.box-title { font: 700 16px Arial, sans-serif; fill: #0f172a; }
|
||||
.box-text { font: 400 13px Arial, sans-serif; fill: #334155; }
|
||||
.small { font: 400 12px Arial, sans-serif; fill: #475569; }
|
||||
.note { font: 700 13px Arial, sans-serif; fill: #7c2d12; }
|
||||
.line { stroke: #334155; stroke-width: 2.2; fill: none; marker-end: url(#arrow); }
|
||||
.thin { stroke: #94a3b8; stroke-width: 1.4; fill: none; marker-end: url(#arrow); }
|
||||
.source { fill: #e0f2fe; stroke: #0284c7; }
|
||||
.process { fill: #f1f5f9; stroke: #64748b; }
|
||||
.audit { fill: #ecfdf5; stroke: #059669; }
|
||||
.db { fill: #eef2ff; stroke: #4f46e5; }
|
||||
.out { fill: #fff7ed; stroke: #ea580c; }
|
||||
.sp { fill: #f8fafc; stroke: #94a3b8; stroke-dasharray: 7 5; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect x="0" y="0" width="1280" height="760" fill="#ffffff" />
|
||||
<text x="48" y="54" class="title">Finance Prozessfluss: Export Dashboard bis zentrale Auswertung</text>
|
||||
<text x="48" y="82" class="subtitle">Stand 2026-06-11: Audit-CSV ist das verarbeitete Merge-Eingangsfile und kann optional die zentrale Quelle sein.</text>
|
||||
|
||||
<text x="50" y="142" class="lane-title">1. Quelle lesen</text>
|
||||
<rect x="48" y="158" width="190" height="118" rx="8" class="source" />
|
||||
<text x="68" y="188" class="box-title">Quellsystem</text>
|
||||
<text x="68" y="214" class="box-text">HANA / SAP B1</text>
|
||||
<text x="68" y="235" class="box-text">SAP Gateway / OData</text>
|
||||
<text x="68" y="256" class="box-text">Excel / CSV / SharePoint</text>
|
||||
|
||||
<line x1="238" y1="217" x2="294" y2="217" class="line" />
|
||||
|
||||
<text x="300" y="142" class="lane-title">2. Vereinheitlichen</text>
|
||||
<rect x="300" y="158" width="210" height="118" rx="8" class="process" />
|
||||
<text x="322" y="188" class="box-title">Mapping</text>
|
||||
<text x="322" y="214" class="box-text">Spalten und ERP-Felder</text>
|
||||
<text x="322" y="235" class="box-text">werden SalesRecord</text>
|
||||
<text x="322" y="256" class="box-text">zugeordnet.</text>
|
||||
|
||||
<line x1="510" y1="217" x2="566" y2="217" class="line" />
|
||||
|
||||
<rect x="572" y="158" width="210" height="118" rx="8" class="process" />
|
||||
<text x="594" y="188" class="box-title">Transformation</text>
|
||||
<text x="594" y="214" class="box-text">Finance-Regeln noch nicht</text>
|
||||
<text x="594" y="235" class="box-text">final summiert; Felder</text>
|
||||
<text x="594" y="256" class="box-text">werden vorbereitet.</text>
|
||||
|
||||
<line x1="677" y1="276" x2="677" y2="332" class="line" />
|
||||
|
||||
<text x="300" y="337" class="lane-title">3. Nachvollziehbare Standortartefakte</text>
|
||||
<rect x="300" y="354" width="230" height="128" rx="8" class="audit" />
|
||||
<text x="322" y="384" class="box-title">Audit-CSV</text>
|
||||
<text x="322" y="410" class="box-text">Sales_ProcessedMergeInput</text>
|
||||
<text x="322" y="431" class="box-text">_<TSC>_<Datum>.csv</text>
|
||||
<text x="322" y="456" class="small">Nach Mapping + Transformation</text>
|
||||
|
||||
<line x1="530" y1="418" x2="586" y2="418" class="line" />
|
||||
|
||||
<rect x="592" y="354" width="210" height="128" rx="8" class="out" />
|
||||
<text x="614" y="384" class="box-title">Standort-Excel</text>
|
||||
<text x="614" y="410" class="box-text">Sales_<TSC>_<Datum>.xlsx</text>
|
||||
<text x="614" y="435" class="small">Ablage und manuelle Pruefung</text>
|
||||
|
||||
<line x1="782" y1="217" x2="850" y2="217" class="line" />
|
||||
<line x1="415" y1="482" x2="415" y2="548" class="thin" />
|
||||
<line x1="697" y1="482" x2="697" y2="548" class="thin" />
|
||||
|
||||
<text x="856" y="142" class="lane-title">4. Operative Datenbasis</text>
|
||||
<rect x="856" y="158" width="220" height="118" rx="8" class="db" />
|
||||
<text x="878" y="188" class="box-title">CentralSalesRecords</text>
|
||||
<text x="878" y="214" class="box-text">Standort wird ersetzt</text>
|
||||
<text x="878" y="235" class="box-text">und zentral gespeichert.</text>
|
||||
<text x="878" y="256" class="small">Standard-Auswertungsquelle</text>
|
||||
|
||||
<line x1="966" y1="276" x2="966" y2="332" class="line" />
|
||||
|
||||
<rect x="856" y="354" width="220" height="128" rx="8" class="db" />
|
||||
<text x="878" y="384" class="box-title">Zentrale Quelle</text>
|
||||
<text x="878" y="410" class="box-text">Schalter in Settings:</text>
|
||||
<text x="878" y="431" class="box-text">DB oder Audit-CSV</text>
|
||||
<text x="878" y="456" class="small">je TSC neueste CSV</text>
|
||||
|
||||
<line x1="1076" y1="418" x2="1132" y2="418" class="line" />
|
||||
|
||||
<text x="1138" y="337" class="lane-title">5. Finance-Ergebnis</text>
|
||||
<rect x="1138" y="354" width="96" height="128" rx="8" class="out" />
|
||||
<text x="1154" y="384" class="box-title">Output</text>
|
||||
<text x="1154" y="410" class="box-text">Dashboard</text>
|
||||
<text x="1154" y="431" class="box-text">Soll/Ist</text>
|
||||
<text x="1154" y="452" class="box-text">Sales_All</text>
|
||||
|
||||
<rect x="326" y="548" width="504" height="110" rx="8" class="sp" />
|
||||
<text x="350" y="580" class="box-title">SharePoint-Ablage</text>
|
||||
<text x="350" y="606" class="box-text">Standort-Excel und Audit-CSV werden in denselben Landesordner hochgeladen.</text>
|
||||
<text x="350" y="630" class="note">SharePoint-Dateien sind Ablage/Pruefspur, nicht automatisch die Live-Quelle.</text>
|
||||
|
||||
<line x1="802" y1="418" x2="850" y2="418" class="thin" />
|
||||
<line x1="830" y1="603" x2="966" y2="482" class="thin" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,410 @@
|
||||
# Finance Schulung fuer Finance-Anwender
|
||||
|
||||
Stand: 2026-06-11
|
||||
|
||||
Zweck: Diese Schulungsunterlage beschreibt den aktuellen Finance-Prozess vom Standortexport bis zu Dashboard, zentraler Excel und Soll/Ist-Vergleich. Sie ist fuer Finance, Finance Keyuser und Wirtschaftspruefung gedacht.
|
||||
|
||||
## Prozessgrafiken
|
||||
|
||||
Die folgenden Grafiken zeigen die wichtigsten Zusammenhaenge vor den Detailkapiteln:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Kurzfazit
|
||||
|
||||
- Fuehrende Sicht fuer Soll/Ist ist `Finance Summary` bzw. der Soll/Ist-Vergleich.
|
||||
- Das zentrale Excel ist ein Ergebnis des aktuellen Datenbestands, nicht die Live-Quelle des Dashboards.
|
||||
- Standortexporte schreiben optional eine nachvollziehbare Audit-CSV nach Mapping und Transformation.
|
||||
- Die Audit-CSV heisst `Sales_ProcessedMergeInput_<TSC>_<yyyy-MM-dd>.csv` und ist das verarbeitete Merge-Eingangsfile, nicht das originale Standortfile.
|
||||
- Per Einstellung kann die zentrale Auswertung von `CentralSalesRecords` auf die neuesten Audit-CSV je Standort umgeschaltet werden.
|
||||
- Waehrungsumrechnung passiert nicht still im Standard-Ist. Sie passiert nur in klaren Analyse-/Transformationsfaellen.
|
||||
|
||||
## Rollen
|
||||
|
||||
| Rolle | Aufgabe |
|
||||
| --- | --- |
|
||||
| Finance Anwender | Finance Summary, zentrale Excel und Soll/Ist pruefen |
|
||||
| Finance Keyuser | Standortexporte starten, Audit-CSV kontrollieren, Freigabe vorbereiten |
|
||||
| Wirtschaftspruefung | Datenfluss ueber verarbeitete CSV, zentrale Excel und Detailzeilen nachvollziehen |
|
||||
| Admin / IT | Standorte, Quellen, Mappings, Kurse, SharePoint und Regeln pflegen |
|
||||
|
||||
## Prozessfluss: Export bis Dashboard
|
||||
|
||||
```text
|
||||
Quellsystem oder Standortdatei
|
||||
|
|
||||
+-- Export Dashboard: Standort exportieren
|
||||
|
|
||||
+-- Adapter liest Daten
|
||||
| HANA/B1, SAP Gateway/OData oder Manual Excel/CSV/SharePoint
|
||||
|
|
||||
+-- Mapping ins SalesRecord-Modell
|
||||
|
|
||||
+-- Transformationen anwenden
|
||||
| z. B. Feldkopien, FirstNonEmpty, optional ConvertCurrency
|
||||
|
|
||||
+-- Audit-CSV schreiben, falls aktiv
|
||||
| Sales_ProcessedMergeInput_<TSC>_<Datum>.csv
|
||||
|
|
||||
+-- Standort-Excel schreiben
|
||||
| Sales_<TSC>_<Datum>.xlsx
|
||||
|
|
||||
+-- CentralSalesRecords fuer diesen Standort ersetzen
|
||||
|
|
||||
+-- Standort-Excel und Audit-CSV nach SharePoint hochladen, falls konfiguriert
|
||||
|
||||
Zentrale Auswertungsquelle
|
||||
|
|
||||
+-- Standard: CentralSalesRecords
|
||||
|
|
||||
+-- Optional: neueste Sales_ProcessedMergeInput_*.csv je TSC
|
||||
|
|
||||
+-- Finance Summary / Management Analyse
|
||||
+-- Soll/Ist-Vergleich
|
||||
+-- Zentrale Excel Sales_All_<Datum>.xlsx
|
||||
```
|
||||
|
||||
Wichtig fuer Finance: Der Standortexport schreibt zuerst die verarbeiteten Daten. Danach entscheidet die Einstellung `Zentrale Auswertung aus Audit-CSV`, ob Dashboard und zentrale Excel aus der internen DB oder aus den neuesten verarbeiteten CSV-Dateien lesen.
|
||||
|
||||
## Schalter in der App
|
||||
|
||||
Die Schalter liegen unter:
|
||||
|
||||
```text
|
||||
Einstellungen > Export Einstellungen > Audit-CSV / nachvollziehbarer Datenfluss
|
||||
```
|
||||
|
||||
| Feld | Wirkung |
|
||||
| --- | --- |
|
||||
| `Lokaler Standardpfad Standort-Dateien` | Ordner fuer Standort-Excel und Audit-CSV. Wenn leer, wird `output` im App-Verzeichnis verwendet. |
|
||||
| `Audit-CSV je Standort schreiben` | Schreibt beim Laenderexport je Standort eine verarbeitete CSV. |
|
||||
| `Zentrale Auswertung aus Audit-CSV` | Dashboard, zentrale Excel und Finance-Auswertungen lesen die neuesten Audit-CSV statt der internen DB. |
|
||||
| `Wechselkurse anwenden auf` | Datumsfeld fuer Kursgueltigkeit in Management-Analysen: `PostingDate`, `InvoiceDate` oder `ExtractionDate`. |
|
||||
|
||||
Es gibt keinen separaten sichtbaren Audit-CSV-Pfad. Die Audit-CSV liegt bewusst im gleichen Ordner wie die lokalen Standortdateien und wird beim Standortexport in den gleichen SharePoint-Landesordner hochgeladen.
|
||||
|
||||
## Dateinamen und Bedeutung
|
||||
|
||||
| Datei | Bedeutung |
|
||||
| --- | --- |
|
||||
| `Sales_<TSC>_<yyyy-MM-dd>.xlsx` | Standort-Excel fuer Menschen und Ablage. |
|
||||
| `Sales_ProcessedMergeInput_<TSC>_<yyyy-MM-dd>.csv` | Verarbeitetes Standortfile nach Mapping und Transformation; Eingang fuer Merge/zentrale Auswertung, auditierbar. |
|
||||
| `Sales_All_<yyyy-MM-dd>.xlsx` | Zentrale Excel mit Finance Summary, Finance Details und Sales-Blatt. |
|
||||
|
||||
Die Audit-CSV ist nicht das originale Standortfile aus Sage, Alphaplan, HANA oder SAP. Sie ist das bereits verarbeitete File, das fachlich erklaert, welche Zeilen in den zentralen Merge gehen.
|
||||
|
||||
## Zentrale Auswertungsquelle
|
||||
|
||||
### Standard: interne DB
|
||||
|
||||
Im Standard liest die App aus `CentralSalesRecords`.
|
||||
|
||||
```text
|
||||
Standortexport
|
||||
-> CentralSalesRecords fuer Standort ersetzen
|
||||
-> Dashboard und zentrale Excel lesen CentralSalesRecords
|
||||
```
|
||||
|
||||
Das ist die schnellste operative Variante.
|
||||
|
||||
### Audit-Modus: CSV als zentrale Quelle
|
||||
|
||||
Wenn `Zentrale Auswertung aus Audit-CSV` aktiv ist:
|
||||
|
||||
```text
|
||||
Ordner mit Sales_ProcessedMergeInput_*.csv
|
||||
-> je TSC die neueste Datei suchen
|
||||
-> CSV lesen
|
||||
-> Dashboard, zentrale Excel und Soll/Ist daraus bilden
|
||||
```
|
||||
|
||||
Das ist die nachvollziehbare Variante fuer Finance/Revision. Finance kann die CSV-Dateien oeffnen, summieren und gegen die zentrale Excel pruefen.
|
||||
|
||||
Kontrollregel: Wenn der Audit-Modus aktiv ist, muessen fuer alle relevanten Standorte aktuelle `Sales_ProcessedMergeInput_*.csv` im Standort-Exportordner vorhanden sein.
|
||||
|
||||
## Waehrungsumrechnung
|
||||
|
||||
Die Kurstabelle liegt in der App unter:
|
||||
|
||||
```text
|
||||
Einstellungen > Wechselkurse
|
||||
```
|
||||
|
||||
Technisch ist das die Tabelle `CurrencyExchangeRates` mit:
|
||||
|
||||
| Feld | Bedeutung |
|
||||
| --- | --- |
|
||||
| `FromCurrency` | Quellwaehrung |
|
||||
| `ToCurrency` | Zielwaehrung |
|
||||
| `Rate` | Faktor: Betrag * Rate |
|
||||
| `ValidFrom` / `ValidTo` | Gueltigkeitszeitraum |
|
||||
| `Notes` | z. B. `Budget 2025`, `Budget 2026`, `ECB daily reference rate` |
|
||||
| `IsActive` | nur aktive Kurse werden verwendet |
|
||||
|
||||
Die App sucht Kurse so:
|
||||
|
||||
1. gleiche Waehrung ergibt Faktor `1`.
|
||||
2. direkter Kurs `Quelle -> Ziel`.
|
||||
3. falls fehlt: inverser Kurs `Ziel -> Quelle`, gerechnet als `1 / Rate`.
|
||||
4. falls fehlt: Kreuzkurs ueber `EUR`.
|
||||
5. falls weiterhin fehlt: keine Umrechnung; die Anzeige zaehlt fehlende Kurse.
|
||||
|
||||
## Wo Kurse wirken
|
||||
|
||||
| Bereich | Kurswirkung |
|
||||
| --- | --- |
|
||||
| Standard `Finance Summary` | keine stille Umrechnung; Hauswaehrung je Land bleibt fuehrend |
|
||||
| Zentrale Excel `Finance Summary` / `Finance Details` | keine stille globale Zielwaehrung |
|
||||
| Management Analyse mit Zielwaehrung `CHF`, `EUR`, `USD` | App rechnet zur Anzeige ueber `CurrencyExchangeRates` um |
|
||||
| Transformation `ConvertCurrency` | schreibt beim Standortexport dauerhaft ein Zielfeld um |
|
||||
| Soll/Ist-Kandidat `Nettofakturawert Hauswaehrung -> CHF Budget <Jahr>` | nutzt Kurse mit `Notes = Budget <Jahr>` als separate Kontrollsicht |
|
||||
| ERP-Feld `DocumentRate` | gespeicherte Quellinformation, nicht automatisch die App-Umrechnung |
|
||||
|
||||
Die Standardfreigabe erfolgt zuerst in lokaler Hauswaehrung. Eine CHF- oder EUR-Sicht ist eine separate Reporting-/Analysefrage.
|
||||
|
||||
## Datumsfeld fuer Kurse
|
||||
|
||||
In `Einstellungen > Export Einstellungen` bestimmt `Wechselkurse anwenden auf`, welches Datum fuer die Kursgueltigkeit verwendet wird:
|
||||
|
||||
| Einstellung | Kursdatum |
|
||||
| --- | --- |
|
||||
| `PostingDate` | `PostingDate`, sonst `InvoiceDate`, sonst `ExtractionDate` |
|
||||
| `InvoiceDate` | `InvoiceDate`, sonst `PostingDate`, sonst `ExtractionDate` |
|
||||
| `ExtractionDate` | `ExtractionDate` |
|
||||
|
||||
Diese Einstellung betrifft Management-Analysen mit Zielwaehrung. Sie aendert nicht die Rohdaten und nicht die normalen Standort-Exporte.
|
||||
|
||||
## Zentrale Excel
|
||||
|
||||
Die zentrale Excel wird ueber das Export Dashboard erzeugt:
|
||||
|
||||
```text
|
||||
Export Dashboard > Zentrale Datei neu erzeugen
|
||||
```
|
||||
|
||||
Sie enthaelt typischerweise:
|
||||
|
||||
| Blatt | Zweck |
|
||||
| --- | --- |
|
||||
| `Finance Summary` | Summen nach Jahr, Land und Waehrung |
|
||||
| `Finance Details` | Detailzeilen, die in die Finance Summary eingehen |
|
||||
| `Sales` | vollstaendige verarbeitete Exportdaten |
|
||||
| `Finance Filter Hilfe` | Hinweise fuer Excel-Filter |
|
||||
|
||||
Wenn die zentrale Auswertungsquelle auf Audit-CSV steht, wird die zentrale Excel aus den neuesten Audit-CSV gebildet. Wenn die Auswertungsquelle auf DB steht, wird sie aus `CentralSalesRecords` gebildet.
|
||||
|
||||
## Soll/Ist-Vergleich
|
||||
|
||||
Der Soll/Ist-Vergleich nutzt dieselbe Finance-Logik wie Finance Summary und zentrale Excel.
|
||||
|
||||
| Feld | Bedeutung |
|
||||
| --- | --- |
|
||||
| `Ist` | aktueller Finance-Istwert |
|
||||
| `Referenz` | Soll-/check.xlsx-Wert bzw. FinanceReference |
|
||||
| `Differenz` | Ist minus Referenz |
|
||||
| `Varianten` | alternative technische Berechnungskandidaten |
|
||||
| `IC` | Intercompany-/2nd-party-Diagnose, nicht stiller Abzug |
|
||||
|
||||
Wenn im Expertenmodus Varianten angezeigt werden, muss der Sollwert weiterhin sichtbar bleiben, weil Finance die Differenz nur mit Referenzwert beurteilen kann.
|
||||
|
||||
## Laenderlogik kurz
|
||||
|
||||
| Land | Quelle / Logik |
|
||||
| --- | --- |
|
||||
| CH / AT | SAP Gateway/OData `ZSCHWEIZ`, `NetwrHc`, Spartenreferenz aus `ProductDivisionRefSet` |
|
||||
| DE | Alphaplan Excel, `NettoPreisGesamtX`, Finance-Regeln fuer Ausschluesse und GS negativ |
|
||||
| ES | Sage CSV, Basis plus Range-/Delta-Dateien, `ImporteNeto`, REC/Abono/Credit negativ |
|
||||
| FR | SAP B1/HANA, Rechnungen und Gutschriften als Positions-Netto |
|
||||
| IN | SAGE/HANA `TRIN`, Hauswaehrung INR |
|
||||
| IT | SAP B1/HANA, IT-Abgrenzung mit `Trafag Italia` und Blank-Supplier-Deduplizierung |
|
||||
| UK | Sage/Manual Excel, Jahresdatei plus Delta-Dateien, `[Sales Price/Value] * [Quantity]`, Credit Notes negativ |
|
||||
| US | SAP B1/HANA, Positions-Netto in USD |
|
||||
|
||||
## Schulungsbeispiele: 4 Zeilen je Land
|
||||
|
||||
Die folgenden Zahlen sind bewusst kleine Schulungssamples, keine produktiven Ist-Werte. Sie zeigen den Fluss:
|
||||
|
||||
```text
|
||||
Quellzeile
|
||||
-> Mapping / Transformation beim Standortexport
|
||||
-> Sales_ProcessedMergeInput_<TSC>_<Datum>.csv oder CentralSalesRecords
|
||||
-> FinanceRuleEngine
|
||||
-> Finance Summary / Finance Details im zentralen Excel
|
||||
-> Dashboard / Soll-Ist
|
||||
```
|
||||
|
||||
### Wo die Transformation wirkt
|
||||
|
||||
Transformationen wirken beim Standortexport, nachdem die Quelle gelesen und bevor Audit-CSV, Standort-Excel und zentrale Datenbasis geschrieben werden.
|
||||
|
||||
```text
|
||||
Quelle lesen
|
||||
-> Mapping in SalesRecord
|
||||
-> FieldTransformationRules anwenden
|
||||
-> Audit-CSV schreiben
|
||||
-> Standort-Excel schreiben
|
||||
-> CentralSalesRecords ersetzen
|
||||
```
|
||||
|
||||
Wenn `Zentrale Auswertung aus Audit-CSV` aktiv ist, liest das Dashboard spaeter die bereits transformierten `Sales_ProcessedMergeInput_*.csv`. Wenn der Schalter aus ist, liest es die transformierten DB-Eintraege aus `CentralSalesRecords`. Die Summenlogik ist danach dieselbe.
|
||||
|
||||
### CH / Schweiz, TSC CH
|
||||
|
||||
| Sample | Quellwert | Mapping / Transformation | Wert fuer Merge | Finance-Beitrag |
|
||||
| --- | ---: | --- | ---: | ---: |
|
||||
| CH-1 Rechnung | `NetwrHc = 1'000 CHF` | `Z.NetwrHc -> SalesPriceValue`, `Z.Hwaer -> SalesCurrency` | `1'000 CHF` | `1'000 CHF` |
|
||||
| CH-2 Rechnung | `NetwrHc = 250 CHF` | Spartenfelder aus `ProductDivisionRefSet` angehaengt | `250 CHF` | `250 CHF` |
|
||||
| CH-3 Gutschrift | `NetwrHc = -80 CHF` | Vorzeichen kommt aus Quelle/Beleglogik | `-80 CHF` | `-80 CHF` |
|
||||
| CH-4 Service | `NetwrHc = 30 CHF` | Service bleibt normale Finance-Zeile | `30 CHF` | `30 CHF` |
|
||||
|
||||
Summe CH im zentralen Excel: `1'200 CHF`.
|
||||
|
||||
### AT / Oesterreich, TSC AT
|
||||
|
||||
| Sample | Quellwert | Mapping / Transformation | Wert fuer Merge | Finance-Beitrag |
|
||||
| --- | ---: | --- | ---: | ---: |
|
||||
| AT-1 Rechnung | `NetwrHc = 800 EUR` | `Z.NetwrHc -> SalesPriceValue`, `Z.Hwaer -> SalesCurrency` | `800 EUR` | `800 EUR` |
|
||||
| AT-2 Rechnung | `NetwrHc = 120 EUR` | Material wird gegen TR-AG-Referenz gemappt | `120 EUR` | `120 EUR` |
|
||||
| AT-3 Gutschrift | `NetwrHc = -50 EUR` | negative Belegzeile bleibt negativ | `-50 EUR` | `-50 EUR` |
|
||||
| AT-4 Rechnung | `NetwrHc = 40 EUR` | keine Kursumrechnung im Standard-Ist | `40 EUR` | `40 EUR` |
|
||||
|
||||
Summe AT im zentralen Excel: `910 EUR`.
|
||||
|
||||
### DE / Deutschland, TSC TRDE
|
||||
|
||||
| Sample | Quellwert | Mapping / Transformation | Wert fuer Merge | Finance-Beitrag |
|
||||
| --- | ---: | --- | ---: | ---: |
|
||||
| DE-1 Rechnung | `NettoPreisGesamtX = 1'500 EUR` | Alphaplan-Spalte -> `SalesPriceValue` | `1'500 EUR` | `1'500 EUR` |
|
||||
| DE-2 GS-Gutschrift | `NettoPreisGesamtX = 200 EUR`, `InvoiceNumber = GS...` | Finance-Regel rechnet GS negativ | `200 EUR` | `-200 EUR` |
|
||||
| DE-3 Trafag AG | `500 EUR`, Kunde `Trafag AG` | Finance-Regel schliesst aus | `500 EUR` | `0 EUR` |
|
||||
| DE-4 Magnetic Sense | `120 EUR`, Kunde enthaelt `Magnetic Sense` | Finance-Regel schliesst aus | `120 EUR` | `0 EUR` |
|
||||
|
||||
Summe DE im zentralen Excel: `1'300 EUR`.
|
||||
|
||||
### ES / Spanien, TSC TRSE/TRES
|
||||
|
||||
| Sample | Quellwert | Mapping / Transformation | Wert fuer Merge | Finance-Beitrag |
|
||||
| --- | ---: | --- | ---: | ---: |
|
||||
| ES-1 Basisrechnung | `ImporteNeto = 700 EUR` | Sage `ImporteNeto -> SalesPriceValue` | `700 EUR` | `700 EUR` |
|
||||
| ES-2 Range-Rechnung | `ImporteNeto = 180 EUR` | Range-Datei wird mit Basis zusammengesetzt | `180 EUR` | `180 EUR` |
|
||||
| ES-3 REC/Abono | `ImporteNeto = 60 EUR`, Typ REC | Credit-/REC-Logik setzt negativ | `-60 EUR` | `-60 EUR` |
|
||||
| ES-4 Duplikat | gleiche `SourceLineId` wie ES-2 | Dedupe entfernt zweite Zeile | `0 EUR` | `0 EUR` |
|
||||
|
||||
Summe ES im zentralen Excel: `820 EUR`.
|
||||
|
||||
### FR / Frankreich, TSC TRFR
|
||||
|
||||
| Sample | Quellwert | Mapping / Transformation | Wert fuer Merge | Finance-Beitrag |
|
||||
| --- | ---: | --- | ---: | ---: |
|
||||
| FR-1 Rechnung | `INV1.LineTotal = 900 EUR` | B1-Positionswert -> `SalesPriceValue` | `900 EUR` | `900 EUR` |
|
||||
| FR-2 Rechnung | `INV1.LineTotal = 100 EUR` | `OADM.MainCurncy -> SalesCurrency` | `100 EUR` | `100 EUR` |
|
||||
| FR-3 Credit Note | `RIN1.LineTotal = 40 EUR` | HANA-Abfrage setzt Credit Note negativ | `-40 EUR` | `-40 EUR` |
|
||||
| FR-4 Storno | `CANCELED = Y` | HANA-Filter laesst Storno weg | `0 EUR` | `0 EUR` |
|
||||
|
||||
Summe FR im zentralen Excel: `960 EUR`.
|
||||
|
||||
### IN / Indien, TSC TRIN
|
||||
|
||||
| Sample | Quellwert | Mapping / Transformation | Wert fuer Merge | Finance-Beitrag |
|
||||
| --- | ---: | --- | ---: | ---: |
|
||||
| IN-1 Rechnung | `SalesValue = 90'000 INR` | SAGE/HANA-Wert -> `SalesPriceValue` | `90'000 INR` | `90'000 INR` |
|
||||
| IN-2 Rechnung | `SalesValue = 10'000 INR` | Hauswaehrung INR bleibt fuehrend | `10'000 INR` | `10'000 INR` |
|
||||
| IN-3 Gutschrift | `SalesValue = 5'000 INR` | Credit-Logik setzt negativ | `-5'000 INR` | `-5'000 INR` |
|
||||
| IN-4 fehlende Sparte | `SalesValue = 2'000 INR` | Umsatz bleibt drin, Spartenstatus separat pruefen | `2'000 INR` | `2'000 INR` |
|
||||
|
||||
Summe IN im zentralen Excel: `97'000 INR`.
|
||||
|
||||
### IT / Italien, TSC TRIT
|
||||
|
||||
| Sample | Quellwert | Mapping / Transformation | Wert fuer Merge | Finance-Beitrag |
|
||||
| --- | ---: | --- | ---: | ---: |
|
||||
| IT-1 Rechnung | `INV1.LineTotal = 1'100 EUR` | B1-Positionswert -> `SalesPriceValue` | `1'100 EUR` | `1'100 EUR` |
|
||||
| IT-2 Trafag Italia | `300 EUR`, Kunde enthaelt `Trafag Italia` | IT-Finance-Regel schliesst aus | `300 EUR` | `0 EUR` |
|
||||
| IT-3 Blank-Supplier-Duplikat | `150 EUR` | IT-Dedupe zaehlt Position nur einmal | `150 EUR` | `150 EUR` |
|
||||
| IT-4 Credit Note | `RIN1.LineTotal = 70 EUR` | Credit Note negativ | `-70 EUR` | `-70 EUR` |
|
||||
|
||||
Summe IT im zentralen Excel: `1'180 EUR`.
|
||||
|
||||
### UK / England, TSC TRUK
|
||||
|
||||
| Sample | Quellwert | Mapping / Transformation | Wert fuer Merge | Finance-Beitrag |
|
||||
| --- | ---: | --- | ---: | ---: |
|
||||
| UK-1 Rechnung | `Sales Price/Value = 100 GBP`, `Quantity = 5` | `SageNetSales = 100 * 5` | `500 GBP` | `500 GBP` |
|
||||
| UK-2 Rechnung | `Sales Price/Value = 80 GBP`, `Quantity = 2` | Quantity-Multiplikation | `160 GBP` | `160 GBP` |
|
||||
| UK-3 Credit Note | `50 GBP`, `Quantity = 1`, Credit Type | Credit Notes negativ | `-50 GBP` | `-50 GBP` |
|
||||
| UK-4 Delta | neue Datei `ddMMyy_TRUK.xlsx` | Basis + Delta zusammen gelesen | `40 GBP` | `40 GBP` |
|
||||
|
||||
Summe UK im zentralen Excel: `650 GBP`.
|
||||
|
||||
### US / USA, TSC TRUS
|
||||
|
||||
| Sample | Quellwert | Mapping / Transformation | Wert fuer Merge | Finance-Beitrag |
|
||||
| --- | ---: | --- | ---: | ---: |
|
||||
| US-1 Rechnung | `INV1.LineTotal = 2'000 USD` | B1-Positionswert -> `SalesPriceValue` | `2'000 USD` | `2'000 USD` |
|
||||
| US-2 Rechnung | `INV1.LineTotal = 350 USD` | Hauswaehrung USD bleibt fuehrend | `350 USD` | `350 USD` |
|
||||
| US-3 Credit Note | `RIN1.LineTotal = 100 USD` | Credit Note negativ | `-100 USD` | `-100 USD` |
|
||||
| US-4 Storno | `CANCELED = Y` | HANA-Filter laesst Storno weg | `0 USD` | `0 USD` |
|
||||
|
||||
Summe US im zentralen Excel: `2'250 USD`.
|
||||
|
||||
### Was im Dashboard sichtbar wird
|
||||
|
||||
| Schritt | Sichtbares Ergebnis |
|
||||
| --- | --- |
|
||||
| Standortexport | pro Land entsteht ein verarbeiteter Stand in Audit-CSV und/oder `CentralSalesRecords` |
|
||||
| `Zentrale Datei neu erzeugen` | `Finance Summary` summiert die Finance-Beitraege je Jahr, Land und Waehrung |
|
||||
| `Finance Details` | zeigt die einzelnen eingeschlossenen Detailzeilen hinter der Summe |
|
||||
| Dashboard `Finance Summary` | zeigt dieselben Summen wie das zentrale Excel |
|
||||
| Soll/Ist | vergleicht die Summe gegen `FinanceReference` / `check.xlsx` |
|
||||
| Management Analyse mit Zielwaehrung | rechnet nur fuer die Anzeige ueber `CurrencyExchangeRates` um |
|
||||
|
||||
## Pruefung fuer Finance/Revision
|
||||
|
||||
1. In `Einstellungen` pruefen, ob `Audit-CSV je Standort schreiben` aktiv ist.
|
||||
2. Fuer jedes relevante Land den Standortexport starten.
|
||||
3. Im Standortordner pruefen:
|
||||
- `Sales_<TSC>_<Datum>.xlsx`
|
||||
- `Sales_ProcessedMergeInput_<TSC>_<Datum>.csv`
|
||||
4. Optional `Zentrale Auswertung aus Audit-CSV` aktivieren.
|
||||
5. `Zentrale Datei neu erzeugen`.
|
||||
6. In der zentralen Excel `Finance Summary` und `Finance Details` pruefen.
|
||||
7. Soll/Ist-Vergleich gegen Referenzwerte pruefen.
|
||||
8. Bei Abweichungen zuerst Audit-CSV und Finance Details nach TSC, Land, Jahr, Waehrung und Belegnummer filtern.
|
||||
|
||||
## Typische Fehlerbilder
|
||||
|
||||
| Symptom | Wahrscheinliche Ursache | Pruefung |
|
||||
| --- | --- | --- |
|
||||
| Audit-Modus aktiv, aber Dashboard leer/Fehler | keine `Sales_ProcessedMergeInput_*.csv` im Exportordner | Standortexport erneut starten, Pfad pruefen |
|
||||
| CSV fehlt im SharePoint-Landesordner | Standortexport lief vor Audit-CSV-Upload-Stand oder SharePoint-Upload fehlgeschlagen | aktuellen Export erneut starten, Log pruefen |
|
||||
| zentrale Excel wirkt alt | nach Standortexport nicht neu erzeugt oder falsche zentrale Quelle aktiv | Export Dashboard und Settings pruefen |
|
||||
| `Mixed` bei Waehrung | mehrere native Waehrungen im Filter | Land/Waehrung filtern oder Zielwaehrung in Analyse waehlen |
|
||||
| fehlende Kurse | kein aktiver gueltiger Kurs in `CurrencyExchangeRates` | Kurs, Gueltigkeit und `Wechselkurse anwenden auf` pruefen |
|
||||
|
||||
## Freigabe-Checkliste
|
||||
|
||||
| Nr. | Checkpunkt |
|
||||
| --- | --- |
|
||||
| 1 | Alle relevanten Standorte exportiert |
|
||||
| 2 | Audit-CSV je Standort vorhanden, falls Revision/Finance den CSV-Fluss prueft |
|
||||
| 3 | Zentrale Auswertungsquelle bewusst gewaehlt: DB oder Audit-CSV |
|
||||
| 4 | Zentrale Excel nach den Standortexporten neu erzeugt |
|
||||
| 5 | `Finance Summary` und `Finance Details` stimmen je Jahr/Land/Waehrung zusammen |
|
||||
| 6 | Soll/Ist zeigt keine unerwarteten Abweichungen |
|
||||
| 7 | Wechselkursfragen getrennt vom lokalen Hauswaehrungsvergleich beurteilt |
|
||||
| 8 | offene Laenderpunkte dokumentiert |
|
||||
|
||||
## Abgleich gegen alte Schulungsaussagen
|
||||
|
||||
Diese Punkte waren in aelteren Schulungsunterlagen veraltet und sind mit Stand 2026-06-11 korrigiert:
|
||||
|
||||
- Spanien ist nicht mehr pauschal nur Vollfile; Basis plus Range-/Delta-Dateien sind unterstuetzt.
|
||||
- Nach einem Standortexport kann zusaetzlich eine Audit-CSV entstehen und nach SharePoint hochgeladen werden.
|
||||
- Dashboard und zentrale Excel koennen optional aus Audit-CSV lesen; frueher war nur `CentralSalesRecords` beschrieben.
|
||||
- Die Audit-CSV hat den neuen Namen `Sales_ProcessedMergeInput_<TSC>_<Datum>.csv`.
|
||||
- Die Wechselkurstabelle wird nicht still fuer Standard-Finance-Soll/Ist angewendet.
|
||||
- Die aktuellen Management-Reiter sind links erreichbar; doppelte obere Reiterbaender wurden reduziert.
|
||||
@@ -0,0 +1,85 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="720" viewBox="0 0 1180 720" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Finance Waehrungs- und Kursfluss</title>
|
||||
<desc id="desc">Zeigt, wo die App-Kurstabelle verwendet wird und wo keine stille Umrechnung stattfindet.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#334155" />
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 29px Arial, sans-serif; fill: #0f172a; }
|
||||
.subtitle { font: 400 16px Arial, sans-serif; fill: #475569; }
|
||||
.box-title { font: 700 16px Arial, sans-serif; fill: #0f172a; }
|
||||
.box-text { font: 400 13px Arial, sans-serif; fill: #334155; }
|
||||
.small { font: 400 12px Arial, sans-serif; fill: #64748b; }
|
||||
.warn { font: 700 13px Arial, sans-serif; fill: #991b1b; }
|
||||
.line { stroke: #334155; stroke-width: 2.2; fill: none; marker-end: url(#arrow); }
|
||||
.muted-line { stroke: #94a3b8; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }
|
||||
.source { fill: #e0f2fe; stroke: #0284c7; }
|
||||
.standard { fill: #f1f5f9; stroke: #64748b; }
|
||||
.rate { fill: #fef3c7; stroke: #d97706; }
|
||||
.convert { fill: #ecfdf5; stroke: #059669; }
|
||||
.budget { fill: #eef2ff; stroke: #4f46e5; }
|
||||
.stop { fill: #fef2f2; stroke: #dc2626; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect x="0" y="0" width="1180" height="720" fill="#ffffff" />
|
||||
<text x="48" y="54" class="title">Waehrungsumrechnung: wann wirkt die Kurstabelle?</text>
|
||||
<text x="48" y="82" class="subtitle">Standard-Soll/Ist bleibt in Hauswaehrung. Die App-Kurstabelle wirkt nur in expliziten Pfaden.</text>
|
||||
|
||||
<rect x="56" y="128" width="260" height="140" rx="8" class="source" />
|
||||
<text x="80" y="162" class="box-title">Quelle liefert Werte</text>
|
||||
<text x="80" y="191" class="box-text">SalesPriceValue</text>
|
||||
<text x="80" y="214" class="box-text">SalesCurrency / CompanyCurrency</text>
|
||||
<text x="80" y="237" class="box-text">DocumentRate als Quellinfo</text>
|
||||
|
||||
<line x1="316" y1="198" x2="392" y2="198" class="line" />
|
||||
|
||||
<rect x="400" y="128" width="280" height="140" rx="8" class="standard" />
|
||||
<text x="424" y="162" class="box-title">Standard Finance</text>
|
||||
<text x="424" y="191" class="box-text">Finance Summary / Sales_All</text>
|
||||
<text x="424" y="214" class="box-text">nutzt Hauswaehrung je Land.</text>
|
||||
<text x="424" y="242" class="warn">Keine stille App-Kursumrechnung</text>
|
||||
|
||||
<line x1="540" y1="268" x2="540" y2="344" class="muted-line" />
|
||||
|
||||
<rect x="400" y="352" width="280" height="132" rx="8" class="stop" />
|
||||
<text x="424" y="386" class="box-title">Nicht verwechseln</text>
|
||||
<text x="424" y="415" class="box-text">DocumentRate kommt aus ERP.</text>
|
||||
<text x="424" y="438" class="box-text">CurrencyExchangeRates ist</text>
|
||||
<text x="424" y="461" class="box-text">die App-Kurstabelle.</text>
|
||||
|
||||
<rect x="772" y="92" width="308" height="160" rx="8" class="rate" />
|
||||
<text x="796" y="126" class="box-title">CurrencyExchangeRates</text>
|
||||
<text x="796" y="155" class="box-text">1. gleiche Waehrung = Faktor 1</text>
|
||||
<text x="796" y="178" class="box-text">2. direkter Kurs</text>
|
||||
<text x="796" y="201" class="box-text">3. inverser Kurs</text>
|
||||
<text x="796" y="224" class="box-text">4. Kreuzkurs ueber EUR</text>
|
||||
|
||||
<line x1="926" y1="252" x2="926" y2="316" class="line" />
|
||||
|
||||
<rect x="760" y="324" width="332" height="120" rx="8" class="convert" />
|
||||
<text x="784" y="358" class="box-title">Pfad A: Management Analyse</text>
|
||||
<text x="784" y="387" class="box-text">Zielwaehrung CHF / EUR / USD</text>
|
||||
<text x="784" y="410" class="box-text">Anzeige-Wert = Quellwert * Kurs</text>
|
||||
|
||||
<line x1="926" y1="444" x2="926" y2="494" class="line" />
|
||||
|
||||
<rect x="760" y="502" width="332" height="118" rx="8" class="convert" />
|
||||
<text x="784" y="536" class="box-title">Pfad B: ConvertCurrency</text>
|
||||
<text x="784" y="565" class="box-text">Transformation beim Standortexport</text>
|
||||
<text x="784" y="588" class="box-text">schreibt Zielfeld dauerhaft um.</text>
|
||||
|
||||
<rect x="56" y="438" width="260" height="132" rx="8" class="budget" />
|
||||
<text x="80" y="472" class="box-title">Pfad C: Budget-CHF</text>
|
||||
<text x="80" y="501" class="box-text">separater Soll/Ist-Kandidat</text>
|
||||
<text x="80" y="524" class="box-text">Notes = Budget <Jahr></text>
|
||||
<text x="80" y="547" class="small">Kontrollsicht, nicht Standard-Ist</text>
|
||||
|
||||
<line x1="316" y1="504" x2="392" y2="438" class="muted-line" />
|
||||
<line x1="680" y1="198" x2="764" y2="172" class="muted-line" />
|
||||
<line x1="680" y1="418" x2="752" y2="384" class="muted-line" />
|
||||
|
||||
<rect x="56" y="612" width="1036" height="62" rx="8" fill="#f8fafc" stroke="#cbd5e1" />
|
||||
<text x="80" y="649" class="box-title">Kursdatum: Settings > Export Einstellungen > Wechselkurse anwenden auf = PostingDate, InvoiceDate oder ExtractionDate</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -1,6 +1,6 @@
|
||||
# Manual-Import und Delta-Stand
|
||||
|
||||
Stand: 2026-06-05
|
||||
Stand: 2026-06-11
|
||||
|
||||
Diese Datei beschreibt, wie manuelle Excel-/CSV-Importe aktuell behandelt werden und wie neue Eintraege bzw. Delta-Dateien verarbeitet werden.
|
||||
|
||||
@@ -28,6 +28,7 @@ UK ist aktuell am besten fuer laufende Delta-Lieferungen vorbereitet.
|
||||
| Auswahl | Jahresdatei zuerst, danach alle spaeteren Delta-Dateien im gleichen Jahr |
|
||||
| Import | App liest alle ausgewaehlten Dateien in einem Lauf zusammen |
|
||||
| Persistenz | `CentralSalesRecords` fuer `TRUK` werden ersetzt, nicht blind additiv angehaengt |
|
||||
| Audit-CSV | optional `Sales_ProcessedMergeInput_TRUK_<Datum>.csv` nach Mapping/Transformation |
|
||||
| Nach Delta-Lieferung | Delta-Datei in den Ordner legen, `TRUK` exportieren, danach zentrale Excel neu erzeugen |
|
||||
|
||||
Wichtig:
|
||||
@@ -51,6 +52,7 @@ Aktueller Implementierungsstand:
|
||||
- primaer ueber `SourceLineId`.
|
||||
- Fallback ueber `TSC + InvoiceNumber + PositionOnInvoice + Material`.
|
||||
- Beim Standortexport ersetzt die App weiterhin den bisherigen Spanien-Stand in `CentralSalesRecords`, aber mit dem zuvor zusammengesetzten und deduplizierten Gesamtstand.
|
||||
- Falls Audit-CSV aktiv ist, schreibt der Export zusaetzlich `Sales_ProcessedMergeInput_<TSC>_<Datum>.csv` in den Standort-Exportordner und laedt sie in denselben SharePoint-Landesordner wie die Standort-Excel.
|
||||
- Wenn nur eine einzelne Delta-Datei direkt als Dateipfad hinterlegt wird, kann weiterhin nur dieses Delta gelesen werden. Fuer Delta-Sync muss deshalb der Ordner hinterlegt sein.
|
||||
|
||||
Finance-Logik:
|
||||
@@ -92,11 +94,18 @@ Offen:
|
||||
1. Neue Datei oder Delta-Datei im richtigen Ordner bereitstellen.
|
||||
2. In `Manuelle Importe` Pfad pruefen bzw. Standort aktiv lassen.
|
||||
3. Standortexport fuer das betroffene Land ausfuehren.
|
||||
4. Danach `Zentrale Datei neu erzeugen` starten.
|
||||
5. Im zentralen Excel `Finance Summary` und `Finance Details` pruefen.
|
||||
4. Falls Audit-CSV fuer Finance/Revision gebraucht wird, im Exportordner `Sales_ProcessedMergeInput_<TSC>_<Datum>.csv` pruefen.
|
||||
5. Falls die zentrale Auswertung aus CSV erfolgen soll, in `Einstellungen > Export Einstellungen` den Schalter `Zentrale Auswertung aus Audit-CSV` setzen.
|
||||
6. Danach `Zentrale Datei neu erzeugen` starten.
|
||||
7. Im zentralen Excel `Finance Summary` und `Finance Details` pruefen.
|
||||
|
||||
## Merksatz
|
||||
|
||||
Manual-Importe ersetzen pro Standort den aktuellen Stand in `CentralSalesRecords`. Delta-Dateien muessen daher beim Import zusammen mit der passenden Basisdatei gelesen werden. Das ist aktuell nur fuer UK vorgesehen. Spanien und Deutschland muessen immer Vollfiles liefern.
|
||||
Manual-Importe ersetzen pro Standort den aktuellen Stand in `CentralSalesRecords`. Delta-Dateien muessen daher beim Import zusammen mit der passenden Basisdatei gelesen werden.
|
||||
|
||||
Wichtig: Fuer Spanien und Deutschland ist das fachlich/prozessual so vorgesehen und durch den Ersetzungsmechanismus praktisch erforderlich. Eine technische Validierung, die Delta-Dateien fuer ES/DE aktiv blockiert, ist aktuell noch nicht eingebaut.
|
||||
Aktueller Stand:
|
||||
|
||||
- UK: Basis plus Delta-Dateien.
|
||||
- Spanien: Basis plus `Spain_Sales_range_*.csv`, wenn ein Ordner hinterlegt ist.
|
||||
- Deutschland: weiterhin Vollfile/Jahresfile, keine Delta-Logik.
|
||||
- Audit-CSV ist ein zusaetzliches verarbeitetes Prueffile; es ersetzt nicht die originalen Standortdateien.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Markdown-Dokumentenstatus
|
||||
|
||||
Stand: 2026-06-10
|
||||
Stand: 2026-06-11
|
||||
|
||||
RAG-Hinweis: Fuer tokenarme Kontextauswahl zuerst `docs/RAG_ROUTER.md` laden. Standardmaessig nur die Kurzdateien unter `docs/rag/` laden; diese Datei und andere Original-MDs nur bei Detail-/Auditbedarf.
|
||||
|
||||
@@ -18,6 +18,8 @@ Diese Datei ordnet die vorhandenen Markdown-Dateien ein. Ziel ist, alte Arbeitsn
|
||||
| `docs/FINANCE_ENTSCHEIDE.md` | Finance-Regeln und Kontrollpunkte | Aktuell fuehrend fuer Finance-Logik |
|
||||
| `entscheide.md` | Kurzfassung der Finance-Fachentscheide | Aktuell als Kurzfassung |
|
||||
| `docs/FINANCE_DATENFLUSS_ANDREAS_2026-06-08.md` | Technischer Finance-Datenfluss | Aktuell fuer End-to-end-Datenfluss |
|
||||
| `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md` | Aktuelle Finance-Schulung fuer Anwender, Keyuser und Revision | Fuehrend fuer Schulung; ersetzt den alten Word-Inhalt fachlich |
|
||||
| `docs/PURCHASING_DASHBOARD_2026-06-05.md` | Einkaufsdashboard, PBIX-Bezug, SAP/OData-Quellen, Cache/Refresh und UI-Sprachen | Aktuell fuer Einkauf |
|
||||
| `docs/PRODUCT_SPARTEN_MAPPING_2026-05-27.md` | Produktsparten-Mapping fuer Group Sales Report | Aktuell fuehrend fuer neues Produktmapping-Thema |
|
||||
| `docs/HR_KPI_NACHDOKU_2026-05-13.md` | HR-KPI technische/fachliche Nachdoku | Aktualisiert um 2026-05-20 Erweiterungen |
|
||||
| `docs/PROGRAMM_DIAGRAMME.md` | Uebersicht Diagramme und technische Einordnung | Aktualisiert um neue Anwenderdokus |
|
||||
@@ -27,7 +29,7 @@ Diese Datei ordnet die vorhandenen Markdown-Dateien ein. Ziel ist, alte Arbeitsn
|
||||
| Datei | Rolle | Status |
|
||||
| --- | --- | --- |
|
||||
| `docs/FINANCE_BERECHNUNGSFORMELN_LAENDER_2026-05-19.md` | Detailregeln je Land | Behalten |
|
||||
| `docs/FINANCE_KURS_WORKFLOW_2026-06-09.md` | Isolierter Workflow fuer Kurs-/Waehrungsanwendung vom Land bis Dashboard | Aktuell fuer Kursfragen; SVG daneben |
|
||||
| `docs/FINANCE_KURS_WORKFLOW_2026-06-09.md` | Isolierter Workflow fuer Kurs-/Waehrungsanwendung vom Land bis Dashboard | Aktuell fuer Kursfragen; SVGs daneben |
|
||||
| `docs/FINANCE_IT_VORGEHEN_2026-05-18.md` | Italien-Pruefpfad | Behalten |
|
||||
| `docs/FINANCE_UK_QUELLE_KORREKTUR_2026-05-18.md` | UK-Quellkorrektur | Behalten |
|
||||
| `docs/SAGE_SPAIN_RCLONE_UPLOAD_GUIDE_2026-06-03.md` | Aktueller Spanien-rclone-All-in-one-Workflow | Ersetzt alte deutsche Anleitung vom 2026-06-03 |
|
||||
@@ -59,6 +61,14 @@ Diese Dateien wurden am 2026-06-09 aus der aktiven Markdown-Struktur entfernt, w
|
||||
| `docs/hr_kpi_cockpit_preview.png` | neutrale HR-Cockpit-Vorschaugrafik fuer DOCX |
|
||||
| `docs/finance_cockpit_preview.png` | neutrale Finance-Cockpit-Vorschaugrafik fuer DOCX |
|
||||
|
||||
## Neue Finance-Schulungsgrafiken seit 2026-06-11
|
||||
|
||||
| Datei | Zweck |
|
||||
| --- | --- |
|
||||
| `docs/FINANCE_PROZESS_EXPORT_DASHBOARD_2026-06-11.svg` | End-to-end-Prozess vom Standortexport bis Dashboard/zentrale Excel |
|
||||
| `docs/FINANCE_AUDIT_CSV_QUELLE_2026-06-11.svg` | Umschaltung zentrale Quelle: DB oder verarbeitete Audit-CSV |
|
||||
| `docs/FINANCE_WAEHRUNG_KURSFLUSS_2026-06-11.svg` | Wo die App-Kurstabelle wirkt und wo nicht |
|
||||
|
||||
## Bereinigung
|
||||
|
||||
Bereinigung 2026-06-09:
|
||||
@@ -67,6 +77,8 @@ Bereinigung 2026-06-09:
|
||||
- Die alte deutsche Spanien-rclone-Anleitung wurde entfernt, weil der aktuelle All-in-one-Workflow im Guide vom 2026-06-05 dokumentiert ist.
|
||||
- Die Alphaplan-Konzept- und Anleitungsdateien vom 2026-06-08 wurden bewusst nicht veraendert.
|
||||
- Delta 2026-06-10: Produktsparten-Fallback `ProductDivisionMapSet`, India/SAGE-HANA-Deploy und Server-DB-Seeds wurden in `docs/DEPLOYMENT_IIS_HANDOFF_2026-05-19.md`, `spartenlogic/UEBERGABE_PRODUKTSPARTEN_ZUORDNUNG.md`, `docs/rag/DEPLOYMENT.md`, `docs/rag/PROJECT.md` und `lastchange.md` nachdokumentiert.
|
||||
- Delta 2026-06-11: Finance-Schulung, Audit-CSV-Prozessfluss, zentrale Auswertungsquelle und Kursfluss wurden in `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md` und den neuen SVG-Grafiken dokumentiert.
|
||||
- Delta 2026-06-11: Einkaufs-Uebersetzungen fuer Spanisch, Italienisch und Hindi sowie der zugehoerige Deploy `1dbaa66` wurden in `docs/PURCHASING_DASHBOARD_2026-06-05.md`, `docs/DEPLOYMENT_IIS_HANDOFF_2026-05-19.md`, `docs/rag/DEPLOYMENT.md`, `docs/rag/PROJECT.md` und `lastchange.md` nachdokumentiert.
|
||||
|
||||
Weiterhin gilt:
|
||||
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
# Programm-Diagramme
|
||||
|
||||
Stand: 2026-05-20
|
||||
Stand: 2026-06-11
|
||||
|
||||
## Nachtrag Finance-Schulung 2026-06-11
|
||||
|
||||
Die aktuelle Finance-Schulung nutzt drei neue Prozessgrafiken:
|
||||
|
||||
- `docs/FINANCE_PROZESS_EXPORT_DASHBOARD_2026-06-11.svg`
|
||||
- zeigt den End-to-end-Fluss von Quellsystem ueber Mapping, Transformation, Audit-CSV, Standort-Excel, DB/CSV-Auswertungsquelle bis Dashboard und zentraler Excel.
|
||||
- `docs/FINANCE_AUDIT_CSV_QUELLE_2026-06-11.svg`
|
||||
- zeigt den Schalter zwischen Standard-Auswertung aus `CentralSalesRecords` und Audit-Auswertung aus den neuesten `Sales_ProcessedMergeInput_*.csv` je TSC.
|
||||
- `docs/FINANCE_WAEHRUNG_KURSFLUSS_2026-06-11.svg`
|
||||
- zeigt, dass Standard-Soll/Ist in Hauswaehrung bleibt und die App-Kurstabelle nur in expliziten Analyse-/Transformationspfaden wirkt.
|
||||
|
||||
Die Markdown-Schulung dazu ist `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md`.
|
||||
|
||||
## Nachtrag Anwenderdokus 2026-05-20
|
||||
|
||||
@@ -42,6 +55,15 @@ Fuer das Programm bieten sich zwei Diagrammarten an:
|
||||
- beschreibt Quelle, Mapping, Hauswaehrung, Nettofakturawert, Buchungsdatum, IC-Ausweis und Sollvergleich
|
||||
- macht sichtbar, dass der Algorithmus regelbasiert ist und nicht auf einzelne Testzahlen frisiert wurde
|
||||
|
||||
- `docs/FINANCE_PROZESS_EXPORT_DASHBOARD_2026-06-11.svg`
|
||||
- aktuelle Schulungsgrafik fuer Export Dashboard bis Finance-Ausgabe
|
||||
|
||||
- `docs/FINANCE_AUDIT_CSV_QUELLE_2026-06-11.svg`
|
||||
- aktuelle Schulungsgrafik fuer DB-/Audit-CSV-Auswertungsquelle
|
||||
|
||||
- `docs/FINANCE_WAEHRUNG_KURSFLUSS_2026-06-11.svg`
|
||||
- aktuelle Schulungsgrafik fuer Wechselkurs- und Waehrungspfad
|
||||
|
||||
## Abgleich gegen Quellcode
|
||||
|
||||
Die Diagramme wurden gegen folgende Codebereiche abgeglichen:
|
||||
@@ -50,7 +72,9 @@ Die Diagramme wurden gegen folgende Codebereiche abgeglichen:
|
||||
- `Services/DataSources/*`: HANA, SAP Gateway und Manual Excel/CSV Adapter
|
||||
- `Services/SiteExportService.cs`: Standortexport, Transformation, Excel-Erzeugung, zentrale Speicherung, SharePoint-Upload
|
||||
- `Services/ExportOrchestrationService.cs`: Export aller aktiven Standorte und anschliessender konsolidierter Export
|
||||
- `Services/ConsolidatedExportService.cs`: zentrale Datei aus `CentralSalesRecords`
|
||||
- `Services/ConsolidatedExportService.cs`: zentrale Datei aus der zentralen Auswertungsquelle
|
||||
- `Services/CentralSalesDataProvider.cs`: Umschaltung zwischen `CentralSalesRecords` und Audit-CSV
|
||||
- `Services/ExportAuditCsvService.cs`: Schreiben/Lesen von `Sales_ProcessedMergeInput_*.csv`
|
||||
- `Services/MappedSalesRecordComposer.cs`: gemeinsame Mapping-Engine fuer SAP OData und generisches HANA-Mapping
|
||||
- `Services/FinanceReconciliationService.cs`: Soll/Ist-Kandidaten, Budgetkurse, IC-Regeln und Ampelstatus
|
||||
- `Services/DatabaseSeedService.cs`: Seed fuer Quellsysteme, ZSCHWEIZ, Finance-Referenzen, Budgetkurse und IC-Regeln
|
||||
|
||||
@@ -57,10 +57,32 @@ Das Dashboard wurde fachlich um diese Bereiche erweitert:
|
||||
- `PBIX Vorlage`: aus `x.pbix` uebernommene Seiten/Visuals.
|
||||
- `3D Simulation`: drehbare 3D-What-if-Analyse.
|
||||
- Unterpunkt `Einkauf > Datenquellen` fuer SAP/OData-Verbindung, Quellen, Join-Fluss und Zielmappings.
|
||||
- Die Seite ist als Cockpit-Struktur umgesetzt und zweisprachig ueber den vorhandenen UI-Sprachservice vorbereitet.
|
||||
- Die Seite ist als Cockpit-Struktur umgesetzt und ueber den vorhandenen UI-Sprachservice mehrsprachig vorbereitet.
|
||||
- EKKO, EKPO und EKET werden per SAP/OData in lokale Cache-Tabellen geladen.
|
||||
- Das Cockpit liest zuerst den Cache und nutzt nur noch als Fallback eine begrenzte Live-Probe, falls noch kein Cache vorhanden ist.
|
||||
|
||||
## Mehrsprachigkeit Stand 2026-06-11
|
||||
|
||||
Commit `1dbaa66 Add purchasing translations` hat die fehlenden UI-Texte fuer den Einkaufsbereich im zentralen `UiTextService` nachgezogen.
|
||||
|
||||
Abgedeckt:
|
||||
|
||||
- Hauptnavigation: `Einkauf`, `Einkauf Dashboard`, `Einkauf Datenquellen`.
|
||||
- Einkaufsdashboard: Uebersicht, SAP-Datenfluss, Live-Status, Zeitraumfilter, KPI-Karten, Detailbereiche, Ideen, Kennzahlen-Katalog, PBIX-Vorlage und 3D-Simulation.
|
||||
- `Einkauf > Datenquellen`: Verbindung, Quellen, Join-Fluss, Mapping, aktuelle Basis, Buttons, Hilfstexte und Speicher-/Reset-Meldungen.
|
||||
- Sprachen: Spanisch, Italienisch und Hindi.
|
||||
|
||||
Bewusst nicht uebersetzt:
|
||||
|
||||
- Technische Namen und Feldnamen wie `EKKO`, `EKPO`, `EKET`, `EKKOSet`, `EKPOSet`, `eketSet`, SAP-Felder, Aliasnamen, TSC und Dateimuster.
|
||||
- Power-BI-Seitentitel aus der importierten PBIX-Vorlage bleiben als fachliche Referenz sichtbar.
|
||||
|
||||
Deploy:
|
||||
|
||||
- Publiziert am 2026-06-11 auf `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
||||
- `BiDashboard.dll` Zeitstempel nach Deploy: `11.06.2026 12:30:27`.
|
||||
- Validierung vor Publish: `dotnet test TrafagSalesExporter.sln --verbosity minimal`, Ergebnis `92/92` Tests gruen.
|
||||
|
||||
## Navigation und Admin-Steuerung
|
||||
|
||||
Stand 2026-06-05: Die Einkaufsbereiche sind nicht mehr als obere Tabs im Dashboard versteckt, sondern als eigene URLs umgesetzt:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# RAG Router
|
||||
|
||||
Stand: 2026-06-10
|
||||
Stand: 2026-06-11
|
||||
|
||||
Zweck: Diese Datei zuerst laden. Danach nur die Dateien aus dem passenden Themenblock laden.
|
||||
|
||||
@@ -18,7 +18,7 @@ Zweck: Diese Datei zuerst laden. Danach nur die Dateien aus dem passenden Themen
|
||||
| Aktueller Stand | Projektstatus, letzte Aenderungen, offene Punkte | `docs/rag/PROJECT.md` |
|
||||
| Finance Cockpit | Soll/Ist, Finance Summary, Regeln, Laenderlogik | `docs/rag/FINANCE.md` |
|
||||
| Finance Spezialfaelle | IT, UK, ES, Abweichungen | `docs/rag/FINANCE.md` |
|
||||
| Manual Import | UK-Deltas, ES/DE Vollfiles, Importprozess | `docs/rag/MANUAL_IMPORT.md` |
|
||||
| Manual Import | UK-Deltas, Spanien Basis+Range, DE Vollfile, Importprozess | `docs/rag/MANUAL_IMPORT.md` |
|
||||
| HR KPI | HR Dashboard, Formeln, Datenqualitaet, Anwenderstand | `docs/rag/HR_KPI.md` |
|
||||
| Deployment/IIS | Publish, Server, BiDashboard, TLS, lokaler Uebergang | `docs/rag/DEPLOYMENT.md` |
|
||||
| Admin/Startseite | Admin Login, Sessions, Landing Page | `docs/rag/ADMIN.md` |
|
||||
@@ -33,7 +33,9 @@ Zweck: Diese Datei zuerst laden. Danach nur die Dateien aus dem passenden Themen
|
||||
| `docs/raw_md_archive/original_history_raws.zip` | exakte Originaldateien nur zur Wiederherstellung, nicht fuer RAG laden |
|
||||
| `docs/MD_DOKUMENTENSTATUS_2026-05-20.md` | Einordnung alter Dokumente |
|
||||
| `docs/FINANCE_ENTSCHEIDE.md` | Finance-Entscheide im Detail |
|
||||
| `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md` | aktuelle Finance-Schulung, Prozessgrafiken, Audit-CSV und Waehrungsfluss |
|
||||
| `docs/FINANCE_BERECHNUNGSFORMELN_LAENDER_2026-05-19.md` | Formeln pro Land |
|
||||
| `docs/FINANCE_DATENFLUSS_ANDREAS_2026-06-08.md` | technischer Finance-Datenfluss inklusive Audit-CSV |
|
||||
| `docs/FINANCE_KURS_WORKFLOW_2026-06-09.md` | isolierter Kurs-/Umrechnungsworkflow vom Land bis Dashboard |
|
||||
| `docs/MANUAL_IMPORT_DELTA_STAND_2026-05-21.md` | Manual-Import-Details |
|
||||
| `docs/HR_KPI_NACHDOKU_2026-05-13.md` | HR-KPI-Details |
|
||||
@@ -47,6 +49,7 @@ Zweck: Diese Datei zuerst laden. Danach nur die Dateien aus dem passenden Themen
|
||||
| Suchwort | Thema |
|
||||
| --- | --- |
|
||||
| `Finance Summary`, `Soll/Ist`, `check.xlsx`, `FinanceRuleEngine` | Finance Cockpit |
|
||||
| `Schulung`, `Training`, `Audit-CSV`, `Sales_ProcessedMergeInput`, `Auswertungsquelle`, `Wirtschaftspruefung` | `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md` |
|
||||
| `Wechselkurs`, `Umrechnungskurs`, `CurrencyExchangeRates`, `DocumentRate`, `ConvertCurrency`, `Anzeige-Waehrung` | `docs/FINANCE_KURS_WORKFLOW_2026-06-09.md` |
|
||||
| `TRUK`, `UK_B1`, `Delta`, `Manual Excel` | Manual Import / Finance Spezialfaelle |
|
||||
| `TRDE`, `Alphaplan`, `NettoPreisGesamtX` | Finance Cockpit / Manual Import |
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# RAG Deployment
|
||||
|
||||
Stand: 2026-06-10
|
||||
Stand: 2026-06-11
|
||||
|
||||
## Kurzstand
|
||||
|
||||
- `TrafagSalesExporter` wird als ASP.NET/IIS-Webanwendung im bisherigen `BiDashboard`-Schema publiziert.
|
||||
- Letzter dokumentierter Deploy: 2026-06-10 Produktsparten-Fallback `ProductDivisionMapSet`.
|
||||
- Letzter dokumentierter Deploy: 2026-06-11 Einkaufs-Uebersetzungen, Commit `1dbaa66 Add purchasing translations`.
|
||||
- Publish-Ziel: `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
||||
- Letzter Deploy-Zeitstempel: `BiDashboard.dll` am `10.06.2026 16:09:44`.
|
||||
- Letzter Deploy-Zeitstempel: `BiDashboard.dll` am `11.06.2026 12:30:27`.
|
||||
- Letzte Validierung vor Deploy: sauberer Worktree `C:\TMP\trafag-translation-test-20260611\TrafagSalesExporter`, `dotnet test TrafagSalesExporter.sln --verbosity minimal`, Ergebnis `92/92` Tests gruen.
|
||||
- Deploy-Ablauf: `app_offline.htm` gesetzt, `dotnet publish TrafagSalesExporter.csproj -c Release -o \\trch-webapp-bidashboard.trafagch.local\BiDashboard$ --no-restore`, danach `app_offline.htm` entfernt.
|
||||
- Vorheriger Deploy 2026-06-11: Finance-Schulung/Dashboard-UI, Commit `f751295`, `BiDashboard.dll` `11.06.2026 12:04:53`.
|
||||
- Produktive CH/AT-DB-Konfiguration nach Seed: `ZSCHWEIZ` Quellen `Z:FinanzdataSchweizOeSet`, `P:ProductDivisionRefSet`, `M:ProductDivisionMapSet`; Joins `Z.Matnr=P.Matnr` und `Z.Prodh=M.Paph1`.
|
||||
- CH/AT-Import nach Deploy: `FetchedRecords=40'292`, `Assigned=36'953`, `UnassignedWithReference=0`.
|
||||
- DB-Backup vor Produktsparten-Seed/Import: `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\trafag_exporter.db.before-productdivision-map-20260610-161022.bak`.
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# RAG Finance
|
||||
|
||||
Stand: 2026-06-10
|
||||
Stand: 2026-06-11
|
||||
|
||||
## Kurzstand
|
||||
|
||||
- Fuehrende Sicht: `Finance Summary`.
|
||||
- Aktuelle Schulung: `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md`.
|
||||
- `Finance Summary`, zentrale Excel, Soll/Ist und Management-Analyse koennen wahlweise aus Audit-CSV statt direkt aus `CentralSalesRecords` lesen. Die Audit-CSV werden nach Mapping und Transformation geschrieben und dienen der Nachvollziehbarkeit fuer Finance/Revision.
|
||||
- Audit-CSV-Dateiname: `Sales_ProcessedMergeInput_<TSC>_<yyyy-MM-dd>.csv`; liegt im gleichen Ordner wie das Standort-Excel und wird beim Standortexport in denselben SharePoint-Landesordner hochgeladen.
|
||||
- `Finance Summary` nutzt dieselbe `FinanceRuleEngine` wie das zentrale Excel.
|
||||
- `Management Analyse` bleibt Diagnose-/Plausibilitaetssicht, nicht fuehrende Finance-Zahl.
|
||||
- Nach UX-Vereinfachung gibt es links eine schnellere Finance-Uebersicht; tiefe Diagnosefunktionen sind unter `Experten` gebuendelt.
|
||||
@@ -34,6 +37,7 @@ Stand: 2026-06-10
|
||||
- Gutschriften/Storno laufen als negative Beleg-/Positionszeilen.
|
||||
- Budget-CHF ist Kontroll-/Reporting-Kandidat, nicht Standardabgleich.
|
||||
- `DocumentRate` aus dem ERP ist ein gespeichertes Quellfeld; die App-Kurstabelle wird nur bei Anzeige-Waehrung, expliziter `ConvertCurrency`-Transformation oder Budget-CHF-Kandidat verwendet.
|
||||
- Schalter fuer Finance/Revision: `Einstellungen > Export Einstellungen > Audit-CSV / nachvollziehbarer Datenfluss`.
|
||||
|
||||
## Offene Fachpunkte
|
||||
|
||||
@@ -52,7 +56,15 @@ Stand: 2026-06-10
|
||||
- `Datenqualitaet`: fehlende Materialnummern, ProductGroup, Waehrung, Kunde, Datum, Nullwerte und ausgeschlossene Zeilen.
|
||||
- `Spartenanalyse > Finanzanalyse`: Umsatzabdeckung und Umsatz nach Produktsparte/Familie/PAPH1 auf Basis der TR-AG-Referenz.
|
||||
- `Spartenanalyse > Zentrale Zuordnung`: Materialnummern aller Laender gegen TR-AG-Stamm pruefen.
|
||||
- `Rohdaten Diagnose`: direkte Plausibilitaets-/Rohdatensicht auf `CentralSalesRecords`.
|
||||
- `Rohdaten Diagnose`: direkte Plausibilitaets-/Rohdatensicht auf die zentrale Auswertungsquelle.
|
||||
|
||||
## Audit-CSV / Auswertungsquelle
|
||||
|
||||
- `Audit-CSV je Standort schreiben`: schreibt beim Laenderexport eine verarbeitete CSV nach Mapping und Transformation.
|
||||
- `Zentrale Auswertung aus Audit-CSV`: zentrale Auswertungen lesen je TSC die neueste `Sales_ProcessedMergeInput_*.csv`.
|
||||
- Der Pfad ist der `Lokaler Standardpfad Standort-Dateien`; ein separater sichtbarer Audit-Pfad wird nicht verwendet.
|
||||
- Standard ohne CSV-Schalter: zentrale Auswertungen lesen `CentralSalesRecords`.
|
||||
- Wenn der CSV-Schalter aktiv ist und keine passenden CSV vorhanden sind, ist die zentrale Auswertung nicht ausfuehrbar.
|
||||
|
||||
## Experten / 3D Datenanalyse
|
||||
|
||||
@@ -97,6 +109,7 @@ Stand: 2026-06-10
|
||||
## Rohquellen Nur Bei Bedarf
|
||||
|
||||
- Entscheide: `docs/FINANCE_ENTSCHEIDE.md`, `entscheide.md`
|
||||
- Finance-Schulung: `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md`
|
||||
- Formeln je Land: `docs/FINANCE_BERECHNUNGSFORMELN_LAENDER_2026-05-19.md`
|
||||
- Isolierter Kurs-Workflow: `docs/FINANCE_KURS_WORKFLOW_2026-06-09.md`
|
||||
- IT Detail: `docs/FINANCE_IT_VORGEHEN_2026-05-18.md`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# RAG Manual Import
|
||||
|
||||
Stand: 2026-06-05
|
||||
Stand: 2026-06-11
|
||||
|
||||
## Kurzstand
|
||||
|
||||
@@ -10,6 +10,8 @@ Stand: 2026-06-05
|
||||
- ES/Spanien liest im Ordner alle `Spain_Sales*.csv`, also Basisdatei plus taegliche `Spain_Sales_range_YYYYMMDD_to_YYYYMMDD.csv`.
|
||||
- Spanien-Deltas werden vor dem Speichern dedupliziert: zuerst `SourceLineId`, sonst Invoice/Position/Material.
|
||||
- DE muss weiterhin Vollfiles liefern.
|
||||
- Wenn Audit-CSV aktiv ist, schreibt der Standortexport nach Mapping/Transformation zusaetzlich `Sales_ProcessedMergeInput_<TSC>_<Datum>.csv` in den Standort-Exportordner.
|
||||
- Zentrale Auswertungen koennen per Setting aus den neuesten Audit-CSV je TSC statt direkt aus `CentralSalesRecords` lesen.
|
||||
|
||||
## Laender
|
||||
|
||||
@@ -24,8 +26,10 @@ Stand: 2026-06-05
|
||||
1. Datei oder Delta im richtigen Ordner bereitstellen.
|
||||
2. In `Manuelle Importe` Pfad/Standort pruefen.
|
||||
3. Standortexport ausfuehren.
|
||||
4. Zentrale Datei neu erzeugen.
|
||||
5. `Finance Summary` und `Finance Details` pruefen.
|
||||
4. Optional Audit-CSV im Standort-Exportordner pruefen.
|
||||
5. Zentrale Auswertungsquelle bewusst setzen: DB oder Audit-CSV.
|
||||
6. Zentrale Datei neu erzeugen.
|
||||
7. `Finance Summary` und `Finance Details` pruefen.
|
||||
|
||||
## Spanien Delta-Sync
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
# RAG Project
|
||||
|
||||
Stand: 2026-06-10
|
||||
Stand: 2026-06-11
|
||||
|
||||
## Kurzstand
|
||||
|
||||
- Fuehrende App: `TrafagSalesExporter`, publiziert als `BiDashboard`.
|
||||
- Letzter dokumentierter Deploy: 2026-06-11, Commit `1dbaa66 Add purchasing translations`, `BiDashboard.dll` Zeitstempel `11.06.2026 12:30:27`.
|
||||
- Letzte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal` mit `92/92` Tests gruen.
|
||||
- Neu deployed: Einkaufsdashboard und `Einkauf > Datenquellen` haben erweiterte UI-Uebersetzungen fuer Spanisch, Italienisch und Hindi; technische Feldnamen wie SAP-Entity-Sets bleiben bewusst unveraendert.
|
||||
- Neu lokal: Audit-CSV-Modus fuer Finance/Revision. Standortexporte schreiben optional nach Mapping/Transformation je Standort `Sales_ProcessedMergeInput_<TSC>_<Datum>.csv`; zentrale Excel, Finance Summary, Soll/Ist und Management-Analyse koennen per Setting aus den neuesten Standort-CSV statt aus der internen DB lesen.
|
||||
- Aktuelle Finance-Schulung: `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md` mit Prozessgrafiken fuer Exportfluss, Audit-CSV-Auswertungsquelle und Waehrungsumrechnung.
|
||||
- Vorheriges UI-Delta 2026-06-11: Export-Dashboard-Manometer als fixes SVG mit Beschriftung; doppelte obere Finance-/Management-Tabbaender reduziert.
|
||||
- Letzter dokumentierter Stand: CH/AT-Produktsparten-Fallback ueber `ProductDivisionMapSet` deployed; India/TRIN SAGE-HANA-Fix und Spanien-SharePoint-Pfad bleiben abgesichert.
|
||||
- Validierung laut Doku: `87/87` Tests gruen fuer den Produktsparten-Fallback; fruehere UI-/Deploy-Schritte wurden einzeln umgesetzt und deployed.
|
||||
- Letzter dokumentierter Deploy: 2026-06-10 auf `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
||||
- Vorheriger Deploy: 2026-06-10 Produktsparten-Fallback auf `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
||||
- Produktsparten CH/AT: `ProductDivisionRefSet` bleibt materialbasiert fuehrend; Fallback `ProductDivisionMapSet` joined `Z.Prodh = M.Paph1`. Server-Import: 40'292 CH/AT-Datensaetze, 36'953 assigned, 0 `UnassignedWithReference`.
|
||||
- India/TRIN: produktive Server-DB steht auf `TRIN -> SAGE -> 20.197.20.60:30015`, Schema `TRAFAG_LIVE`, User-Override `TRAFAGCONTROLS`.
|
||||
- Doku-Delta: `docs/FINANCE_KURS_WORKFLOW_2026-06-09.md` plus SVG; alte Finance-Stubs aus aktiver Markdown-Struktur entfernt, Volltexte bleiben im Raw-Archiv.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Last Change
|
||||
|
||||
Stand: 2026-06-10
|
||||
Stand: 2026-06-11
|
||||
|
||||
Diese Datei ist fuer tokenarme RAG-Nutzung komprimiert.
|
||||
|
||||
@@ -8,9 +8,18 @@ Diese Datei ist fuer tokenarme RAG-Nutzung komprimiert.
|
||||
|
||||
- Fuehrender Kurzkontext: `docs/rag/PROJECT.md`.
|
||||
- Themenrouter: `docs/RAG_ROUTER.md`.
|
||||
- Neu deployed: Commit `1dbaa66 Add purchasing translations` auf `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
||||
- Deploy-Status 2026-06-11: `BiDashboard.dll` Zeitstempel `11.06.2026 12:30:27`; `app_offline.htm` wurde nach Publish entfernt.
|
||||
- Validierung vor Deploy aus sauberer Worktree-Kopie: `dotnet test TrafagSalesExporter.sln --verbosity minimal` mit `92/92` Tests gruen.
|
||||
- Neu lokal/deployed: Einkaufsdashboard, Einkaufs-Datenquellen und relevante Einkauf-Hilfstexte sind fuer Spanisch, Italienisch und Hindi im UI-Sprachservice nachgezogen; Audit-CSV-Hilfstext ist nicht mehr englisch im Spanisch-/Hindi-Modus.
|
||||
- Vorheriger Deploy 2026-06-11: Commit `f751295 Update finance training and dashboard UI`, `BiDashboard.dll` Zeitstempel `11.06.2026 12:04:53`.
|
||||
- Neu lokal/deployed: Export-Dashboard-Manometer als fixes SVG mit Beschriftung; doppelte obere Tab-Baender im Management/Finance-Cockpit reduziert.
|
||||
- Neu lokal dokumentiert: aktuelle Finance-Schulung `docs/FINANCE_SCHULUNG_FINANZ_2026-06-11.md` mit Prozessgrafiken fuer Export Dashboard, Audit-CSV-Auswertungsquelle und Waehrungs-/Kursfluss.
|
||||
- Neue Schulungsgrafiken: `docs/FINANCE_PROZESS_EXPORT_DASHBOARD_2026-06-11.svg`, `docs/FINANCE_AUDIT_CSV_QUELLE_2026-06-11.svg`, `docs/FINANCE_WAEHRUNG_KURSFLUSS_2026-06-11.svg`.
|
||||
- Neu lokal umgesetzt: Standortexporte koennen nach Mapping und Transformation eine lesbare Audit-CSV je Standort schreiben; zentrale Excel, Finance Summary und Management-Analyse koennen per Setting wahlweise aus den neuesten Audit-CSV statt aus `CentralSalesRecords` lesen.
|
||||
- Letzter dokumentierter Code-Stand: CH/AT-Produktsparten-Fallback ueber `ProductDivisionMapSet` deployed; India/TRIN HANA-Route und Spanien-SharePoint-Pfad bleiben im Seed abgesichert.
|
||||
- Letzte dokumentierte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal` mit `87/87` Tests gruen.
|
||||
- Letzter dokumentierter Deploy: 2026-06-10 Produktsparten-Fallback nach `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
||||
- Letzte dokumentierte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal` mit `92/92` Tests gruen.
|
||||
- Letzter dokumentierter Deploy: 2026-06-11 Einkaufs-Uebersetzungen nach `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
||||
- Neu umgesetzt und deployed: `ZSCHWEIZ` nutzt zusaetzlich `M = ProductDivisionMapSet` und den Join `Z.Prodh = M.Paph1`; Produktfelder fallen per `FirstNonEmpty(P.*, M.*)` von Materialreferenz auf PAPH1-Mapping zurueck.
|
||||
- Server-DB am 2026-06-10 aktualisiert: CH/AT neu importiert, `FetchedRecords=40'292`, `Assigned=36'953`, `UnassignedWithReference=0`; Backup: `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\trafag_exporter.db.before-productdivision-map-20260610-161022.bak`.
|
||||
- Deploy-Status 2026-06-10: `BiDashboard.dll` Zeitstempel `10.06.2026 16:09:44`; `app_offline.htm` wurde entfernt.
|
||||
|
||||
Reference in New Issue
Block a user