Improve keyuser export workflow
This commit is contained in:
@@ -14,6 +14,9 @@
|
||||
<MudNavLink Href="/finance-cockpit/vergleich" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.CompareArrows">
|
||||
@T("Soll/Ist Vergleich", "Actual/reference comparison")
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/manual-imports" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.UploadFile">
|
||||
@T("Manuelle Importe", "Manual imports")
|
||||
</MudNavLink>
|
||||
<MudNavGroup Title="@T("Admin", "Admin")" Icon="@Icons.Material.Filled.AdminPanelSettings">
|
||||
<AuthorizeView Policy="@SecurityPolicies.AdminOnly">
|
||||
<Authorized>
|
||||
|
||||
@@ -37,6 +37,25 @@
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@if (_readinessWarnings.Count > 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense Class="mb-4">
|
||||
<MudText Typo="Typo.body2">@T("Aktive Standorte sind noch nicht vollstaendig bereit:", "Active sites are not fully ready:")</MudText>
|
||||
@foreach (var warning in _readinessWarnings)
|
||||
{
|
||||
<MudText Typo="Typo.caption">@warning</MudText>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@if (_consolidatedStale)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense Class="mb-4">
|
||||
@T("Seit der letzten zentralen Excel wurde mindestens ein Standort neu exportiert. Bitte `Zentrale Datei neu erzeugen` ausfuehren, damit das Endexcel aktuell ist.",
|
||||
"At least one site was exported after the last consolidated Excel. Please rebuild the consolidated file so the final Excel is current.")
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudTable Items="_dashboardRows" Dense Hover Striped Loading="_loading">
|
||||
<HeaderContent>
|
||||
<MudTh>@T("Land", "Country")</MudTh>
|
||||
@@ -155,6 +174,8 @@
|
||||
@code {
|
||||
private List<DashboardRow> _dashboardRows = new();
|
||||
private List<ConsolidatedDashboardRow> _consolidatedRows = new();
|
||||
private List<string> _readinessWarnings = new();
|
||||
private bool _consolidatedStale;
|
||||
private bool _loading = true;
|
||||
private bool _anyRunning;
|
||||
private CancellationTokenSource? _pollingCts;
|
||||
@@ -171,6 +192,8 @@
|
||||
var state = await DashboardPageActions.LoadAsync();
|
||||
_dashboardRows = state.DashboardRows;
|
||||
_consolidatedRows = state.ConsolidatedRows;
|
||||
_readinessWarnings = state.ReadinessWarnings;
|
||||
_consolidatedStale = state.IsConsolidatedStale;
|
||||
|
||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
||||
_loading = false;
|
||||
@@ -178,6 +201,12 @@
|
||||
|
||||
private async Task ExportAll()
|
||||
{
|
||||
if (_readinessWarnings.Count > 0)
|
||||
{
|
||||
Snackbar.Add(T("Es gibt aktive Standorte mit fehlender manueller Datei. Bitte Warnung im Dashboard pruefen.",
|
||||
"There are active sites with missing manual files. Please check the dashboard warning."), Severity.Warning);
|
||||
}
|
||||
|
||||
_anyRunning = true;
|
||||
await LoadDataAsync();
|
||||
StartPolling();
|
||||
@@ -260,6 +289,9 @@
|
||||
{
|
||||
await InvokeAsync(() =>
|
||||
Snackbar.Add(string.Format(T("Export gespeichert: {0}", "Export saved: {0}"), result.FilePath), Severity.Success));
|
||||
await InvokeAsync(() =>
|
||||
Snackbar.Add(T("Die zentrale Excel ist danach noch nicht automatisch aktualisiert. Bitte `Zentrale Datei neu erzeugen` starten.",
|
||||
"The consolidated Excel is not automatically updated after this. Please rebuild the consolidated file."), Severity.Info));
|
||||
}
|
||||
else if (result?.Log.Status == "Error" && !string.IsNullOrWhiteSpace(result.Log.ErrorMessage))
|
||||
{
|
||||
|
||||
@@ -12,7 +12,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("Gleiche Berechnungslogik wie FinanceProbe/Testprogramm", "Same calculation logic as FinanceProbe/test program")</MudText>
|
||||
<MudText Typo="Typo.caption">@T("Verbindliche Finance-Sicht aus CentralSalesRecords", "Authoritative finance view from CentralSalesRecords")</MudText>
|
||||
</div>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="@(_hideRowsWithoutActual ? Variant.Filled : Variant.Outlined)"
|
||||
@@ -189,7 +189,9 @@
|
||||
if (row.Key.Equals("ES", StringComparison.OrdinalIgnoreCase))
|
||||
return T("Sage ImporteNeto; REC/Credit Notes negativ; Zuschlaege/Nebenkosten noch pruefen.", "Sage ImporteNeto; REC/credit notes negative; surcharges/charges still to check.");
|
||||
if (row.Key.Equals("IT", StringComparison.OrdinalIgnoreCase))
|
||||
return T("B1 Arbeitsfilter 47005 ohne 4700504 plus provisorischer Kundenausschluss.", "B1 working filter 47005 excluding 4700504 plus provisional customer exclusions.");
|
||||
return T("Bestaetigte IT-Regel: Trafag Italia ausgeschlossen; doppelte Zeilen ohne Supplier country nur einmal.", "Confirmed IT rule: Trafag Italia excluded; duplicate rows without supplier country counted once.");
|
||||
if (row.Key.Equals("DE", StringComparison.OrdinalIgnoreCase))
|
||||
return T("Alphaplan Excel; Kundenlaender/Filter fuer offiziellen DE-Istwert noch fachlich offen.", "Alphaplan Excel; customer countries/filters for official DE actual are still open.");
|
||||
if (row.Key.Equals("FR", StringComparison.OrdinalIgnoreCase) ||
|
||||
row.Key.Equals("IN", StringComparison.OrdinalIgnoreCase) ||
|
||||
row.Key.Equals("US", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -55,6 +55,10 @@
|
||||
<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.")
|
||||
</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.",
|
||||
"This analysis is a plausibility/raw-data view. For the authoritative finance reconciliation, use `Actual/reference comparison` or the `Finance | ...` columns in the final Excel.")
|
||||
</MudAlert>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudSelect T="int" @bind-Value="_selectedCentralYear" Label='@T("Jahr", "Year")' Dense>
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
@page "/manual-imports"
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Models
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject IStandortePageService StandortePageService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IUiTextService UiText
|
||||
|
||||
<PageTitle>@T("Manuelle Importe", "Manual imports")</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">@T("Manuelle Importe", "Manual imports")</MudText>
|
||||
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense Class="mb-4">
|
||||
@T("Diese Seite ist fuer Keyuser: Hier werden Excel-/CSV-Dateien fuer manuelle Laender wie DE, UK und ES hinterlegt und aktiviert. Technische Spaltenmappings bleiben in Admin -> Standorte.",
|
||||
"This page is for key users: Excel/CSV files for manual countries such as DE, UK and ES are maintained and activated here. Technical column mappings remain in Admin -> Sites.")
|
||||
</MudAlert>
|
||||
|
||||
<MudTable Items="_rows" Dense Hover Striped Loading="_loading">
|
||||
<HeaderContent>
|
||||
<MudTh>@T("Land", "Country")</MudTh>
|
||||
<MudTh>TSC</MudTh>
|
||||
<MudTh>@T("Aktiv", "Active")</MudTh>
|
||||
<MudTh>@T("Datei / SharePoint-Ordner", "File / SharePoint folder")</MudTh>
|
||||
<MudTh>@T("Letzter Upload", "Last upload")</MudTh>
|
||||
<MudTh>@T("Aktionen", "Actions")</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Land</MudTd>
|
||||
<MudTd>@context.TSC</MudTd>
|
||||
<MudTd><MudSwitch @bind-Value="context.IsActive" Color="Color.Primary" /></MudTd>
|
||||
<MudTd>
|
||||
<MudTextField @bind-Value="context.ManualImportFilePath"
|
||||
Placeholder="@T("lokaler Pfad, UNC, SharePoint-Datei oder SharePoint-Ordner", "local path, UNC, SharePoint file or SharePoint folder")"
|
||||
Margin="Margin.Dense" />
|
||||
</MudTd>
|
||||
<MudTd>@(context.ManualImportLastUploadedAtUtc?.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") ?? "-")</MudTd>
|
||||
<MudTd>
|
||||
<MudStack Row Spacing="1">
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Info"
|
||||
OnClick="() => ValidatePathAsync(context)" Disabled="_busySiteId == context.Id">
|
||||
@T("Pfad pruefen", "Check path")
|
||||
</MudButton>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
|
||||
OnClick="() => SaveAsync(context)" Disabled="_busySiteId == context.Id">
|
||||
@T("Speichern", "Save")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<InputFile OnChange="args => UploadAsync(context, args)" accept=".xlsx,.csv" />
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText Typo="Typo.caption">@T("Keine manuellen Excel-/CSV-Standorte gefunden.", "No manual Excel/CSV sites found.")</MudText>
|
||||
</NoRecordsContent>
|
||||
</MudTable>
|
||||
|
||||
@code {
|
||||
private List<ManualImportRow> _rows = [];
|
||||
private bool _loading = true;
|
||||
private int? _busySiteId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
_loading = true;
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
var manualSourceCodes = await db.SourceSystemDefinitions
|
||||
.Where(x => x.ConnectionKind == SourceSystemConnectionKinds.ManualExcel)
|
||||
.Select(x => x.Code)
|
||||
.ToListAsync();
|
||||
|
||||
_rows = await db.Sites
|
||||
.Where(site => manualSourceCodes.Contains(site.SourceSystem))
|
||||
.OrderBy(site => site.Land)
|
||||
.ThenBy(site => site.TSC)
|
||||
.Select(site => new ManualImportRow
|
||||
{
|
||||
Id = site.Id,
|
||||
Land = site.Land,
|
||||
TSC = site.TSC,
|
||||
IsActive = site.IsActive,
|
||||
ManualImportFilePath = site.ManualImportFilePath,
|
||||
ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc
|
||||
})
|
||||
.ToListAsync();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task SaveAsync(ManualImportRow row)
|
||||
{
|
||||
_busySiteId = row.Id;
|
||||
try
|
||||
{
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
var site = await db.Sites.FirstAsync(x => x.Id == row.Id);
|
||||
site.IsActive = row.IsActive;
|
||||
site.ManualImportFilePath = row.ManualImportFilePath.Trim();
|
||||
site.ManualImportLastUploadedAtUtc = row.ManualImportLastUploadedAtUtc;
|
||||
await db.SaveChangesAsync();
|
||||
Snackbar.Add(T("Import-Einstellungen gespeichert.", "Import settings saved."), Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"{T("Speichern fehlgeschlagen", "Save failed")}: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busySiteId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidatePathAsync(ManualImportRow row)
|
||||
{
|
||||
_busySiteId = row.Id;
|
||||
try
|
||||
{
|
||||
row.ManualImportLastUploadedAtUtc = await StandortePageService.ValidateManualImportPathAsync(row.ManualImportFilePath);
|
||||
Snackbar.Add(T("Datei oder SharePoint-Referenz ist erreichbar.", "File or SharePoint reference is reachable."), Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"{T("Pfadpruefung fehlgeschlagen", "Path check failed")}: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busySiteId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UploadAsync(ManualImportRow row, InputFileChangeEventArgs args)
|
||||
{
|
||||
var file = args.File;
|
||||
if (file is null)
|
||||
return;
|
||||
|
||||
_busySiteId = row.Id;
|
||||
try
|
||||
{
|
||||
var extension = Path.GetExtension(file.Name);
|
||||
if (!string.Equals(extension, ".xlsx", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(extension, ".csv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(T("Bitte eine .xlsx- oder .csv-Datei auswaehlen.", "Please choose a .xlsx or .csv file."));
|
||||
}
|
||||
|
||||
var uploadDirectory = Path.Combine(AppContext.BaseDirectory, "manual-imports");
|
||||
Directory.CreateDirectory(uploadDirectory);
|
||||
var safeBaseName = string.Concat(Path.GetFileNameWithoutExtension(file.Name)
|
||||
.Select(ch => char.IsLetterOrDigit(ch) || ch == '-' || ch == '_' ? ch : '_'));
|
||||
if (string.IsNullOrWhiteSpace(safeBaseName))
|
||||
safeBaseName = "manual_import";
|
||||
|
||||
var targetPath = Path.Combine(uploadDirectory, $"{safeBaseName}_{Guid.NewGuid():N}{extension}");
|
||||
await using (var sourceStream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024))
|
||||
await using (var targetStream = File.Create(targetPath))
|
||||
{
|
||||
await sourceStream.CopyToAsync(targetStream);
|
||||
}
|
||||
|
||||
row.ManualImportFilePath = targetPath;
|
||||
row.ManualImportLastUploadedAtUtc = DateTime.UtcNow;
|
||||
await SaveAsync(row);
|
||||
Snackbar.Add(T("Datei hochgeladen.", "File uploaded."), Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"{T("Upload fehlgeschlagen", "Upload failed")}: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busySiteId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private string T(string german, string english) => UiText.Text(german, english);
|
||||
|
||||
private sealed class ManualImportRow
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Land { get; set; } = string.Empty;
|
||||
public string TSC { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; }
|
||||
public string ManualImportFilePath { get; set; } = string.Empty;
|
||||
public DateTime? ManualImportLastUploadedAtUtc { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,23 @@
|
||||
|
||||
Stand: 2026-05-20
|
||||
|
||||
## Nachtrag 2026-05-20 Workflow-Fixes nach Review
|
||||
|
||||
Umgesetzt:
|
||||
|
||||
- Dashboard warnt vor aktiven manuellen Standorten ohne Datei.
|
||||
- Nach Einzelstandortexport wird sichtbar, dass die zentrale Excel neu erzeugt werden muss.
|
||||
- Dashboard erkennt eine veraltete zentrale Excel nach neuem Standortexport.
|
||||
- Neuer Menuepunkt `Manuelle Importe` fuer Keyuser.
|
||||
- Zentrale Excel hat ein Blatt `Finance Summary`.
|
||||
- `Management Analyse` ist als Rohdaten-/Plausibilitaetssicht markiert.
|
||||
- `Soll/Ist Vergleich` ist als verbindliche Finance-Sicht markiert.
|
||||
- Export-Live-Status ist nicht mehr pauschal `HANA Abfrage...`.
|
||||
|
||||
Weiterhin offen:
|
||||
|
||||
- DE Alphaplan-Fachabgrenzung: Kundenlaender/Filter muessen von Munir/Finance bestaetigt werden.
|
||||
|
||||
## Nachtrag 2026-05-20 Keyuser Prozess-SVG
|
||||
|
||||
Erstellt:
|
||||
|
||||
@@ -62,13 +62,45 @@ public sealed class DashboardPageService : IDashboardPageService
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
var consolidatedRows = BuildConsolidatedRows(await db.ExportSettings.FirstOrDefaultAsync() ?? new());
|
||||
var latestSuccessfulSiteRun = logs
|
||||
.Where(log => log.Status == "OK")
|
||||
.Select(log => (DateTime?)log.Timestamp)
|
||||
.OrderByDescending(timestamp => timestamp)
|
||||
.FirstOrDefault();
|
||||
var latestConsolidatedRun = consolidatedRows
|
||||
.Select(row => row.LastModified)
|
||||
.OrderByDescending(timestamp => timestamp)
|
||||
.FirstOrDefault();
|
||||
|
||||
return new DashboardPageState
|
||||
{
|
||||
DashboardRows = rows,
|
||||
ConsolidatedRows = BuildConsolidatedRows(await db.ExportSettings.FirstOrDefaultAsync() ?? new())
|
||||
ConsolidatedRows = consolidatedRows,
|
||||
ReadinessWarnings = BuildReadinessWarnings(sites, sourceSystems),
|
||||
IsConsolidatedStale = latestSuccessfulSiteRun.HasValue &&
|
||||
(!latestConsolidatedRun.HasValue || latestSuccessfulSiteRun.Value > latestConsolidatedRun.Value),
|
||||
LatestSuccessfulSiteRun = latestSuccessfulSiteRun,
|
||||
LatestConsolidatedRun = latestConsolidatedRun
|
||||
};
|
||||
}
|
||||
|
||||
private static List<string> BuildReadinessWarnings(List<Site> activeSites, List<SourceSystemDefinition> sourceSystems)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
foreach (var site in activeSites.OrderBy(x => x.Land).ThenBy(x => x.TSC))
|
||||
{
|
||||
var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(site.ManualImportFilePath))
|
||||
warnings.Add($"{site.Land} / {site.TSC}: manuelle Excel-/CSV-Datei fehlt.");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private static string ResolveDashboardSapServiceUrl(Site site, List<SourceSystemDefinition> sourceSystems)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
||||
@@ -114,6 +146,10 @@ public sealed class DashboardPageState
|
||||
{
|
||||
public List<DashboardRow> DashboardRows { get; set; } = [];
|
||||
public List<ConsolidatedDashboardRow> ConsolidatedRows { get; set; } = [];
|
||||
public List<string> ReadinessWarnings { get; set; } = [];
|
||||
public bool IsConsolidatedStale { get; set; }
|
||||
public DateTime? LatestSuccessfulSiteRun { get; set; }
|
||||
public DateTime? LatestConsolidatedRun { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DashboardRow
|
||||
|
||||
@@ -146,11 +146,98 @@ public class ExcelExportService : IExcelExportService
|
||||
|
||||
ws.Columns().AdjustToContents();
|
||||
if (includeFinanceHelpSheet)
|
||||
{
|
||||
AddFinanceSummarySheet(workbook, records);
|
||||
AddFinanceHelpSheet(workbook);
|
||||
}
|
||||
|
||||
workbook.SaveAs(fullPath);
|
||||
}
|
||||
|
||||
private static void AddFinanceSummarySheet(XLWorkbook workbook, List<SalesRecord> records)
|
||||
{
|
||||
var ws = workbook.Worksheets.Add("Finance Summary");
|
||||
ws.Position = 1;
|
||||
ws.Cell(1, 1).Value = "Finance Summary";
|
||||
ws.Cell(1, 1).Style.Font.Bold = true;
|
||||
ws.Cell(1, 1).Style.Font.FontSize = 14;
|
||||
ws.Cell(2, 1).Value = "Diese Summen verwenden dieselbe Finance-Sicht wie die Spalten Finance | ... im Blatt Sales.";
|
||||
|
||||
var headers = new[]
|
||||
{
|
||||
"Year",
|
||||
"Country Key",
|
||||
"Currency",
|
||||
"Included Rows",
|
||||
"Net Sales Actual",
|
||||
"Excluded Rows",
|
||||
"Hinweis"
|
||||
};
|
||||
|
||||
for (var i = 0; i < headers.Length; i++)
|
||||
{
|
||||
ws.Cell(4, i + 1).Value = headers[i];
|
||||
ws.Cell(4, i + 1).Style.Font.Bold = true;
|
||||
}
|
||||
|
||||
var italyBlankSupplierCountryRows = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var summaryRows = records
|
||||
.Select(record =>
|
||||
{
|
||||
var financeDate = ResolveFinanceDate(record);
|
||||
var countryKey = ResolveFinanceCountryKey(record.Land, record.Tsc);
|
||||
var include = ResolveFinanceInclude(record, countryKey, italyBlankSupplierCountryRows) && record.SalesPriceValue != 0m;
|
||||
return new
|
||||
{
|
||||
Year = financeDate.Year,
|
||||
CountryKey = countryKey,
|
||||
Currency = ResolveFinanceCurrency(record),
|
||||
Include = include,
|
||||
Value = include ? record.SalesPriceValue : 0m
|
||||
};
|
||||
})
|
||||
.GroupBy(row => new { row.Year, row.CountryKey, row.Currency })
|
||||
.OrderBy(group => group.Key.Year)
|
||||
.ThenBy(group => group.Key.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(group => group.Key.Currency, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => new
|
||||
{
|
||||
group.Key.Year,
|
||||
group.Key.CountryKey,
|
||||
group.Key.Currency,
|
||||
IncludedRows = group.Count(row => row.Include),
|
||||
NetSalesActual = group.Sum(row => row.Value),
|
||||
ExcludedRows = group.Count(row => !row.Include)
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var rowIndex = 5;
|
||||
foreach (var row in summaryRows)
|
||||
{
|
||||
ws.Cell(rowIndex, 1).Value = row.Year;
|
||||
ws.Cell(rowIndex, 2).Value = row.CountryKey;
|
||||
ws.Cell(rowIndex, 3).Value = row.Currency;
|
||||
ws.Cell(rowIndex, 4).Value = row.IncludedRows;
|
||||
ws.Cell(rowIndex, 5).Value = row.NetSalesActual;
|
||||
ws.Cell(rowIndex, 6).Value = row.ExcludedRows;
|
||||
ws.Cell(rowIndex, 7).Value = BuildFinanceSummaryHint(row.CountryKey);
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
ws.Column(5).Style.NumberFormat.Format = "#,##0.00";
|
||||
ws.Columns().AdjustToContents();
|
||||
}
|
||||
|
||||
private static string BuildFinanceSummaryHint(string countryKey)
|
||||
=> countryKey.ToUpperInvariant() switch
|
||||
{
|
||||
"DE" => "DE Alphaplan ist technisch vorbereitet; Kundenlaender/Filter fachlich noch bestaetigen.",
|
||||
"IT" => "IT: Trafag Italia ausgeschlossen; doppelte Blank-Supplier-Zeilen nur einmal.",
|
||||
"UK" => "UK: Sage/Manual Excel, Credit Notes negativ.",
|
||||
"ES" => "ES: Sage CSV/Manual Excel, REC/Credit Notes negativ.",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
private static void AddFinanceHelpSheet(XLWorkbook workbook)
|
||||
{
|
||||
var ws = workbook.Worksheets.Add("Finance Filter Hilfe");
|
||||
|
||||
@@ -96,7 +96,7 @@ public class ExportOrchestrationService
|
||||
lock (_lock)
|
||||
{
|
||||
if (_runningExports.ContainsKey(site.Id)) return null;
|
||||
_runningExports[site.Id] = "HANA Abfrage...";
|
||||
_runningExports[site.Id] = BuildInitialExportStatus(site);
|
||||
}
|
||||
NotifyChanged();
|
||||
|
||||
@@ -134,6 +134,17 @@ public class ExportOrchestrationService
|
||||
OnExportStatusChanged?.Invoke();
|
||||
}
|
||||
|
||||
private static string BuildInitialExportStatus(Site site)
|
||||
{
|
||||
var sourceSystem = (site.SourceSystem ?? string.Empty).Trim().ToUpperInvariant();
|
||||
return sourceSystem switch
|
||||
{
|
||||
"MANUAL_EXCEL" => "Manuelle Excel/CSV lesen...",
|
||||
"SAP" => "SAP OData lesen...",
|
||||
_ => "Quelldaten lesen..."
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string?> RunConsolidatedExportAsync()
|
||||
{
|
||||
lock (_lock)
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# Last Change 2026-05-04
|
||||
|
||||
## Workflow-Konsistenz fuer Keyuser verbessert 2026-05-20
|
||||
|
||||
Geaendert:
|
||||
|
||||
- Export Dashboard zeigt jetzt Warnungen, wenn aktive Manual-Excel-Standorte noch keine Datei/Pfad hinterlegt haben.
|
||||
- Nach einem Einzelstandortexport wird darauf hingewiesen, dass die zentrale Excel separat neu erzeugt werden muss.
|
||||
- Dashboard markiert, wenn seit der letzten zentralen Excel ein Standortexport gelaufen ist.
|
||||
- Neuer Keyuser-Menuepunkt `Manuelle Importe` fuer DE/UK/ES-artige Excel-/CSV-Quellen:
|
||||
- Pfad/SharePoint-Referenz pflegen
|
||||
- Datei hochladen
|
||||
- Standort aktiv/inaktiv setzen
|
||||
- Pfad pruefen
|
||||
- Live-Status startet nicht mehr pauschal mit `HANA Abfrage...`, sondern quellenneutral bzw. fuer Manual Excel/SAP passender.
|
||||
- Zentrale Excel enthaelt ein neues Blatt `Finance Summary` mit Summen nach Jahr, Land und Waehrung.
|
||||
- `Management Analyse` ist klarer als Rohdaten-/Plausibilitaetssicht markiert.
|
||||
- `Soll/Ist Vergleich` ist klarer als verbindliche Finance-Sicht markiert.
|
||||
|
||||
Bewusst nicht geaendert:
|
||||
|
||||
- DE-Fachregel bleibt offen, bis Munir/Finance bestaetigt, welche Kundenlaender/Filter zum offiziellen DE-Ist gehoeren.
|
||||
|
||||
## Keyuser Prozessdoku SVG 2026-05-20
|
||||
|
||||
Erstellt:
|
||||
|
||||
Reference in New Issue
Block a user