Add SharePoint manual source handling and finance status
This commit is contained in:
@@ -391,7 +391,7 @@
|
||||
Für diesen Standort wird keine SAP- oder HANA-Verbindung verwendet. Es wird die hier hinterlegte Excel-/CSV-Datei gelesen und in `CentralSalesRecords` übernommen.
|
||||
</MudAlert>
|
||||
<MudTextField @bind-Value="_editingSite.ManualImportFilePath" Label="Excel-/CSV-Dateipfad"
|
||||
HelperText="Unterstuetzt lokale Pfade, UNC-Pfade und SharePoint-Referenzen wie https://... oder Shared Documents/Ordner/Datei.xlsx bzw. .csv."
|
||||
HelperText="Unterstuetzt lokale Pfade, UNC-Pfade, SharePoint-Dateien und SharePoint-Ordner. Bei Ordnern wird die neueste passende Excel-/CSV-Datei geladen."
|
||||
Class="mb-2" />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="ValidateManualImportPathAsync"
|
||||
Disabled="_uploadingManualImport" Class="mb-3">
|
||||
|
||||
@@ -2,6 +2,76 @@
|
||||
|
||||
Stand: 2026-05-05
|
||||
|
||||
## Nachtrag 2026-05-08 Manual Excel/CSV / SharePoint-Ordner
|
||||
|
||||
Aktueller Stand fuer manuelle Quellen:
|
||||
|
||||
- `MANUAL_EXCEL` ist fachlich Manual Excel/CSV.
|
||||
- Unterstuetzt werden `.xlsx` und `.csv`; altes `.xls` ist nicht der Zielpfad.
|
||||
- Lokale Datei als Quelle:
|
||||
- App liest die Datei.
|
||||
- App erzeugt eine neue Exportdatei im selben lokalen Ordner.
|
||||
- SharePoint-Datei als Quelle:
|
||||
- App laedt die Datei temporaer herunter.
|
||||
- App erzeugt eine neue Exportdatei und laedt sie in denselben SharePoint-Ordner hoch.
|
||||
- SharePoint-Ordner als Quelle:
|
||||
- App waehlt automatisch die neueste passende `.xlsx`/`.csv` fuer den Standort.
|
||||
- Primaeres Muster: `ddMMyy_TSC.xlsx` oder `ddMMyy_TSC.csv`.
|
||||
- Fallback: SharePoint `LastModifiedDateTime`.
|
||||
|
||||
England / UK:
|
||||
|
||||
- Standort `England`, `TSC = TRUK`, `SourceSystem = MANUAL_EXCEL`.
|
||||
- Quelle ist ein SharePoint-Ordner:
|
||||
|
||||
```text
|
||||
https://trafagag.sharepoint.com/sites/WorldwideBIPlatform/Import/Finance/UK_B1
|
||||
```
|
||||
|
||||
- Beispielauswahl:
|
||||
- `010526_TRUK.xlsx` ist neuer als `010426_TRUK.xlsx`.
|
||||
- Exportdateien werden wieder in `Import/Finance/UK_B1` geschrieben.
|
||||
- Befund am 2026-05-08: England zeigte lokal faelschlich auf die Deutschland-Alphaplan-Datei; lokale DB wurde korrigiert.
|
||||
- `DatabaseSeedService` repariert kuenftig einen leeren England/TRUK-Manual-Pfad auf den UK_B1-Ordner.
|
||||
|
||||
Spanien / Sage:
|
||||
|
||||
- Spanien nutzt `MANUAL_EXCEL` als technischen Importpfad fuer den Sage-Export.
|
||||
- Die Datei `Spain_Sales_2025.csv` konnte gelesen werden (`4'341` Zeilen).
|
||||
- Fehler war danach der Exportpfad: die SharePoint-URL wurde als lokaler Dateipfad interpretiert.
|
||||
- Fix: SharePoint-Manual-Quellen liefern keinen `ReferenceFilePath` mehr, sondern erzeugen eine neue Exportdatei im Quellordner.
|
||||
|
||||
Deutschland / Alphaplan:
|
||||
|
||||
- Deutschland nutzt `MANUAL_EXCEL` als technischen Importpfad fuer Alphaplan-Excel.
|
||||
- Grafisches Mapping ist vorhanden.
|
||||
- Offener Punkt: konkreter Alphaplan-Datei-/SharePoint-Pfad muss im Standort hinterlegt sein, sonst kommt `Standort 'Deutschland' hat keine manuelle Excel-Datei.`
|
||||
|
||||
Verifikation:
|
||||
|
||||
- Tests `55/55` erfolgreich.
|
||||
|
||||
## Nachtrag 2026-05-08 FinanceProbe fuer mehr Laender
|
||||
|
||||
FinanceProbe wurde erweitert:
|
||||
|
||||
- `FinanceReferences` werden vollstaendig angezeigt, nicht nur bei aktivem Standort oder vorhandenen Ist-Daten.
|
||||
- Dadurch sind alle Soll-Laender aus der Finance-Konfiguration im Meeting sichtbar.
|
||||
- Neue Sektion `Datenabdeckung je Standort` zeigt je Standort:
|
||||
- Quelle/System
|
||||
- Manual-/SharePoint-Pfad
|
||||
- Aktivstatus
|
||||
- Anzahl 2025-Zeilen in `CentralSalesRecords`
|
||||
- Summe `SalesPriceValue`
|
||||
- Waehrungen und Datumsbereich
|
||||
- letzter Exportstatus/Fehler
|
||||
- CH/AT-Erkennung im Finance-Service wurde geschaerft, damit `ZSCHWEIZ`-Zeilen mit Land `AT` Oesterreich zugeordnet werden koennen.
|
||||
|
||||
Wichtig:
|
||||
|
||||
- `Keine Daten` bedeutet jetzt nicht zwingend fehlende Referenz, sondern oft: Referenz ist vorhanden, aber Ist-Daten wurden noch nicht exportiert/importiert.
|
||||
- Fuer neue Laender reicht es, `FinanceReferences` zu pflegen und Daten nach `CentralSalesRecords` zu bringen; die Probe zeigt sie dann automatisch.
|
||||
|
||||
## Nachtrag 2026-05-07 Mapper-Konsolidierung / Finance-Konfiguration
|
||||
|
||||
Architekturstand:
|
||||
|
||||
@@ -2,6 +2,47 @@
|
||||
|
||||
Stand: 2026-05-05
|
||||
|
||||
## Nachtrag 2026-05-08 Manual Excel/CSV SharePoint-Automatik
|
||||
|
||||
Erledigt:
|
||||
|
||||
- SharePoint-Ordner koennen bei Manual Excel/CSV als Quelle hinterlegt werden.
|
||||
- Bei Ordnern wird automatisch die neueste passende `.xlsx`/`.csv` ausgewaehlt.
|
||||
- Dateinamenmuster fuer bevorzugte Auswahl: `ddMMyy_TSC.xlsx` bzw. `ddMMyy_TSC.csv`.
|
||||
- Manual-Export schreibt die erzeugte Exportdatei in den Quellordner zurueck:
|
||||
- lokal: gleicher lokaler Ordner
|
||||
- SharePoint: gleicher SharePoint-Ordner
|
||||
- England/TRUK ist lokal auf den SharePoint-Ordner `Import/Finance/UK_B1` korrigiert.
|
||||
- Spanien-Fehler nach erfolgreichem Einlesen der SharePoint-CSV ist behoben.
|
||||
|
||||
Naechste konkrete Schritte:
|
||||
|
||||
1. App neu starten, damit die Seed-/Repair-Logik aktiv ist.
|
||||
2. England/TRUK exportieren und pruefen, ob die App `010526_TRUK.xlsx` statt `010426_TRUK.xlsx` auswaehlt.
|
||||
3. Im SharePoint-Ordner `Import/Finance/UK_B1` pruefen, ob die neue Exportdatei dort wieder abgelegt wird.
|
||||
4. Deutschland/Alphaplan: im Standort den korrekten Alphaplan-Excel- oder SharePoint-Pfad hinterlegen.
|
||||
5. Deutschland exportieren und Mapping gegen die Alphaplan-Datei validieren.
|
||||
6. Falls UK-Dateinamen spaeter ein anderes Muster bekommen, Auswahlregel erweitern.
|
||||
|
||||
## Nachtrag 2026-05-08 FinanceProbe
|
||||
|
||||
Erledigt:
|
||||
|
||||
- FinanceProbe zeigt alle Finance-Referenzen 2025.
|
||||
- Datenabdeckung je Standort wurde ergaenzt.
|
||||
- CH/AT-Zuordnung wurde fuer `ZSCHWEIZ` geschaerft.
|
||||
|
||||
Naechste fachliche Schritte:
|
||||
|
||||
1. Nach Export von England, Schweiz/Oesterreich, Spanien und Deutschland die FinanceProbe neu laden.
|
||||
2. In der Sektion `Datenabdeckung je Standort` pruefen, ob Zeilen 2025 und Periode plausibel sind.
|
||||
3. Fuer Laender mit `Keine Daten` entscheiden:
|
||||
- Datenquelle fehlt
|
||||
- Standort deaktiviert
|
||||
- Mapping/Export noch nicht gelaufen
|
||||
- Referenz ist nur zukuenftig relevant
|
||||
4. Fuer AT/CH nach `ZSCHWEIZ`-Export pruefen, ob `LAND1` korrekt `AT` bzw. `CH` liefert.
|
||||
|
||||
## Nachtrag 2026-05-07 nach Mapper-/Finance-Aufraeumung
|
||||
|
||||
Erledigt:
|
||||
|
||||
@@ -11,4 +11,10 @@ public sealed class DataSourceFetchResult
|
||||
/// SiteExportService erzeugt dann keine neue Excel-Datei.
|
||||
/// </summary>
|
||||
public string? ReferenceFilePath { get; init; }
|
||||
|
||||
public string? LocalOutputDirectoryOverride { get; init; }
|
||||
|
||||
public string? SharePointUploadFolderOverride { get; init; }
|
||||
|
||||
public string? SharePointUploadLandOverride { get; init; }
|
||||
}
|
||||
|
||||
@@ -29,12 +29,15 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
||||
|
||||
var manualImportPath = site.ManualImportFilePath.Trim();
|
||||
string filePath;
|
||||
string? localOutputDirectory = null;
|
||||
string? sharePointUploadFolder = null;
|
||||
string? tempManualImportPath = null;
|
||||
try
|
||||
{
|
||||
if (File.Exists(manualImportPath))
|
||||
{
|
||||
filePath = manualImportPath;
|
||||
localOutputDirectory = Path.GetDirectoryName(Path.GetFullPath(manualImportPath));
|
||||
}
|
||||
else if (LooksLikeSharePointReference(manualImportPath))
|
||||
{
|
||||
@@ -55,10 +58,22 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
||||
await _appEventLogService.WriteAsync("Export", "Manuelle Excel von SharePoint laden",
|
||||
siteId: site.Id, land: site.Land, details: manualImportPath);
|
||||
|
||||
var sharePointFileReference = manualImportPath;
|
||||
if (LooksLikeSharePointFolderReference(manualImportPath))
|
||||
{
|
||||
var latestFile = await _sharePointService.ResolveLatestFileInFolderAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret,
|
||||
spConfig.SiteUrl, manualImportPath, site.TSC);
|
||||
sharePointFileReference = latestFile.FileReference;
|
||||
await _appEventLogService.WriteAsync("Export", "Neueste SharePoint-Datei ausgewaehlt",
|
||||
siteId: site.Id, land: site.Land, details: sharePointFileReference);
|
||||
}
|
||||
|
||||
tempManualImportPath = await _sharePointService.DownloadToTempFileAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret,
|
||||
spConfig.SiteUrl, manualImportPath);
|
||||
filePath = manualImportPath;
|
||||
spConfig.SiteUrl, sharePointFileReference);
|
||||
filePath = sharePointFileReference;
|
||||
sharePointUploadFolder = ResolveSharePointParentFolder(sharePointFileReference, spConfig.SiteUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -75,7 +90,9 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
||||
return new DataSourceFetchResult
|
||||
{
|
||||
Records = records,
|
||||
ReferenceFilePath = filePath
|
||||
LocalOutputDirectoryOverride = localOutputDirectory,
|
||||
SharePointUploadFolderOverride = sharePointUploadFolder,
|
||||
SharePointUploadLandOverride = sharePointUploadFolder is null ? null : string.Empty
|
||||
};
|
||||
}
|
||||
finally
|
||||
@@ -90,4 +107,25 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
||||
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith("/Shared Documents/", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith("Shared Documents/", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool LooksLikeSharePointFolderReference(string path)
|
||||
=> LooksLikeSharePointReference(path) &&
|
||||
string.IsNullOrWhiteSpace(Path.GetExtension(path.TrimEnd('/')));
|
||||
|
||||
private static string ResolveSharePointParentFolder(string fileReference, string siteUrl)
|
||||
{
|
||||
var remotePath = fileReference.Trim('/').Trim();
|
||||
if (Uri.TryCreate(fileReference, UriKind.Absolute, out var fileUri) &&
|
||||
Uri.TryCreate(siteUrl, UriKind.Absolute, out var siteUri))
|
||||
{
|
||||
var absolutePath = Uri.UnescapeDataString(fileUri.AbsolutePath);
|
||||
var sitePath = siteUri.AbsolutePath.TrimEnd('/');
|
||||
if (absolutePath.StartsWith(sitePath, StringComparison.OrdinalIgnoreCase))
|
||||
absolutePath = absolutePath[sitePath.Length..];
|
||||
remotePath = absolutePath.Trim('/').Trim();
|
||||
}
|
||||
|
||||
var lastSlash = remotePath.LastIndexOf('/');
|
||||
return lastSlash <= 0 ? string.Empty : remotePath[..lastSlash];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
EnsureSourceSystemDefinitions(db);
|
||||
EnsureCentralHanaServerRecords(db);
|
||||
EnsureSpainManualExcelSite(db);
|
||||
EnsureUkManualExcelFolder(db);
|
||||
EnsureSapODataDachSite(db);
|
||||
EnsureFinanceReferenceDefaults(db);
|
||||
EnsureBudgetExchangeRateDefaults(db);
|
||||
@@ -287,6 +288,36 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsureUkManualExcelFolder(AppDbContext db)
|
||||
{
|
||||
var existing = db.Sites
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefault(x =>
|
||||
x.TSC == "TRUK" ||
|
||||
x.Land == "England" ||
|
||||
x.Land == "UK");
|
||||
|
||||
if (existing is null)
|
||||
return;
|
||||
|
||||
var changed = false;
|
||||
if (string.IsNullOrWhiteSpace(existing.SourceSystem))
|
||||
{
|
||||
existing.SourceSystem = "MANUAL_EXCEL";
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (string.Equals(existing.SourceSystem, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase) &&
|
||||
string.IsNullOrWhiteSpace(existing.ManualImportFilePath))
|
||||
{
|
||||
existing.ManualImportFilePath = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform/Import/Finance/UK_B1";
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsureSapODataDachSite(AppDbContext db)
|
||||
{
|
||||
if (db.Sites.Count() <= 1)
|
||||
|
||||
@@ -60,16 +60,7 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
rows => BuildNetSalesActual(rows, budgetRatesToChf, intercompanyRules),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var activeSiteKeys = (await db.Sites
|
||||
.AsNoTracking()
|
||||
.Where(s => s.IsActive)
|
||||
.Select(s => new { s.Land, s.TSC })
|
||||
.ToListAsync())
|
||||
.Select(s => ResolveReferenceKey(s.Land, s.TSC))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return financeReferences
|
||||
.Where(reference => activeSiteKeys.Contains(reference.Key) || groupedActuals.ContainsKey(reference.Key))
|
||||
.Select(reference => BuildReferenceRow(reference, groupedActuals))
|
||||
.OrderBy(row => row.Label, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
@@ -282,6 +273,8 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
var normalizedLand = (land ?? string.Empty).Trim().ToUpperInvariant();
|
||||
var normalizedTsc = (tsc ?? string.Empty).Trim().ToUpperInvariant();
|
||||
|
||||
if (normalizedLand is "AT" or "AUT" || normalizedLand.Contains("OESTER") || normalizedLand.Contains("OSTER") || normalizedLand.Contains("AUSTRIA")) return "AT";
|
||||
if (normalizedLand is "CH" or "CHE" || normalizedLand.Contains("SCHWE") || normalizedLand.Contains("SWITZER")) return "CH";
|
||||
if (normalizedLand.Contains("FRANK") || normalizedTsc.Contains("FR")) return "FR";
|
||||
if (normalizedLand.Contains("IND") || normalizedTsc.Contains("IN")) return "IN";
|
||||
if (normalizedLand.Contains("ITAL") || normalizedTsc.Contains("IT")) return "IT";
|
||||
@@ -289,7 +282,6 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
if (normalizedLand.Contains("USA") || normalizedLand.Contains("UNITED STATES") || normalizedTsc.Contains("US")) return "US";
|
||||
if (normalizedLand.Contains("DEUT") || normalizedTsc.Contains("DE")) return "DE";
|
||||
if (normalizedLand.Contains("SPAN") || normalizedTsc is "SE" or "ES") return "ES";
|
||||
if (normalizedLand.Contains("SCHWE") || normalizedTsc.Contains("CH")) return "CH";
|
||||
|
||||
return normalizedTsc.Replace("TR", string.Empty);
|
||||
}
|
||||
|
||||
@@ -4,5 +4,8 @@ public interface ISharePointUploadService
|
||||
{
|
||||
Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath);
|
||||
Task<string> DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference);
|
||||
Task<SharePointFileReference> ResolveLatestFileInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc);
|
||||
Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl);
|
||||
}
|
||||
|
||||
public sealed record SharePointFileReference(string FileReference, DateTimeOffset? LastModifiedUtc);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using Azure.Core;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Graph;
|
||||
using Microsoft.Graph.Models;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
@@ -82,6 +85,64 @@ public class SharePointUploadService : ISharePointUploadService
|
||||
return tempPath;
|
||||
}
|
||||
|
||||
public async Task<SharePointFileReference> ResolveLatestFileInFolderAsync(
|
||||
string tenantId,
|
||||
string clientId,
|
||||
string clientSecret,
|
||||
string siteUrl,
|
||||
string folderReference,
|
||||
string siteTsc)
|
||||
{
|
||||
var normalizedTenantId = Normalize(tenantId);
|
||||
var normalizedClientId = Normalize(clientId);
|
||||
var normalizedClientSecret = Normalize(clientSecret);
|
||||
var normalizedSiteUrl = Normalize(siteUrl);
|
||||
var normalizedReference = Normalize(folderReference);
|
||||
var normalizedTsc = Normalize(siteTsc).ToUpperInvariant();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(normalizedReference))
|
||||
throw new InvalidOperationException("SharePoint-Ordnerreferenz fehlt.");
|
||||
|
||||
var credential = new ClientSecretCredential(normalizedTenantId, normalizedClientId, normalizedClientSecret);
|
||||
var graphClient = new GraphServiceClient(credential, ["https://graph.microsoft.com/.default"]);
|
||||
|
||||
var siteUri = new Uri(normalizedSiteUrl);
|
||||
var sitePath = siteUri.AbsolutePath.TrimEnd('/');
|
||||
var site = await graphClient.Sites[$"{siteUri.Host}:{sitePath}"].GetAsync();
|
||||
|
||||
if (site?.Id is null)
|
||||
throw new InvalidOperationException("SharePoint Site konnte nicht gefunden werden.");
|
||||
|
||||
var drive = await graphClient.Sites[site.Id].Drive.GetAsync();
|
||||
if (drive?.Id is null)
|
||||
throw new InvalidOperationException("SharePoint Dokumentenbibliothek konnte nicht gefunden werden.");
|
||||
|
||||
var folderPath = ResolveRemotePath(normalizedReference, siteUri);
|
||||
var children = await graphClient.Drives[drive.Id].Root.ItemWithPath(folderPath).Children.GetAsync();
|
||||
var candidates = children?.Value?
|
||||
.Where(item => item.File is not null)
|
||||
.Where(item => IsSupportedManualImportFile(item.Name))
|
||||
.Where(item => MatchesTsc(item.Name, normalizedTsc))
|
||||
.Select(item => new
|
||||
{
|
||||
Item = item,
|
||||
FileDate = TryParseDatedSiteFileName(item.Name, normalizedTsc, out var fileDate) ? fileDate : (DateTime?)null
|
||||
})
|
||||
.OrderByDescending(x => x.FileDate ?? x.Item.LastModifiedDateTime?.UtcDateTime ?? DateTime.MinValue)
|
||||
.ThenByDescending(x => x.Item.LastModifiedDateTime?.UtcDateTime ?? DateTime.MinValue)
|
||||
.ToList() ?? [];
|
||||
|
||||
var selected = candidates.FirstOrDefault()
|
||||
?? throw new InvalidOperationException(
|
||||
string.IsNullOrWhiteSpace(normalizedTsc)
|
||||
? $"Im SharePoint-Ordner '{folderPath}' wurde keine Excel-/CSV-Datei gefunden."
|
||||
: $"Im SharePoint-Ordner '{folderPath}' wurde keine Excel-/CSV-Datei fuer '{normalizedTsc}' gefunden.");
|
||||
|
||||
return new SharePointFileReference(
|
||||
string.Join("/", folderPath.Trim('/'), selected.Item.Name).Trim('/'),
|
||||
selected.Item.LastModifiedDateTime);
|
||||
}
|
||||
|
||||
public async Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl)
|
||||
{
|
||||
var normalizedTenantId = Normalize(tenantId);
|
||||
@@ -143,6 +204,41 @@ public class SharePointUploadService : ISharePointUploadService
|
||||
return fileReference.Trim('/').Trim();
|
||||
}
|
||||
|
||||
private static bool IsSupportedManualImportFile(string? fileName)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName ?? string.Empty);
|
||||
return extension.Equals(".xlsx", StringComparison.OrdinalIgnoreCase) ||
|
||||
extension.Equals(".csv", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool MatchesTsc(string? fileName, string normalizedTsc)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(normalizedTsc))
|
||||
return true;
|
||||
|
||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName ?? string.Empty);
|
||||
return nameWithoutExtension.EndsWith($"_{normalizedTsc}", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool TryParseDatedSiteFileName(string? fileName, string normalizedTsc, out DateTime fileDate)
|
||||
{
|
||||
fileDate = default;
|
||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName ?? string.Empty);
|
||||
var pattern = string.IsNullOrWhiteSpace(normalizedTsc)
|
||||
? @"^(?<date>\d{6})_[A-Z0-9]+$"
|
||||
: $"^(?<date>\\d{{6}})_{Regex.Escape(normalizedTsc)}$";
|
||||
var match = Regex.Match(nameWithoutExtension, pattern, RegexOptions.IgnoreCase);
|
||||
if (!match.Success)
|
||||
return false;
|
||||
|
||||
return DateTime.TryParseExact(
|
||||
match.Groups["date"].Value,
|
||||
"ddMMyy",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out fileDate);
|
||||
}
|
||||
|
||||
private static string BuildInputPreview(string tenantId, string clientId, string clientSecret, string siteUrl)
|
||||
{
|
||||
var maskedSecret = string.IsNullOrEmpty(clientSecret)
|
||||
|
||||
@@ -56,8 +56,6 @@ public class SiteExportService : ISiteExportService
|
||||
details: $"Quelle={sourceSystem} | TSC={site.TSC}");
|
||||
|
||||
var (settings, spConfig, sourceDefinition, rules) = await LoadExportConfigAsync(site, sourceSystem);
|
||||
var outputDir = ResolveSiteOutputDirectory(settings, site);
|
||||
|
||||
var adapter = _dataSourceResolver.Resolve(sourceDefinition.ConnectionKind);
|
||||
var fetchResult = await adapter.FetchAsync(new DataSourceFetchContext
|
||||
{
|
||||
@@ -69,6 +67,7 @@ public class SiteExportService : ISiteExportService
|
||||
});
|
||||
|
||||
var records = fetchResult.Records;
|
||||
var outputDir = fetchResult.LocalOutputDirectoryOverride ?? ResolveSiteOutputDirectory(settings, site);
|
||||
|
||||
updateStatus?.Invoke("Transformationen anwenden...");
|
||||
await _appEventLogService.WriteAsync("Export", "Transformationen anwenden",
|
||||
@@ -94,7 +93,7 @@ public class SiteExportService : ISiteExportService
|
||||
details: $"Records={records.Count}");
|
||||
await _centralSalesRecordService.ReplaceForSiteAsync(site, records, updateStatus);
|
||||
|
||||
await UploadToSharePointIfConfiguredAsync(site, spConfig, filePath, updateStatus);
|
||||
await UploadToSharePointIfConfiguredAsync(site, spConfig, filePath, updateStatus, fetchResult);
|
||||
|
||||
sw.Stop();
|
||||
log.Status = "OK";
|
||||
@@ -156,7 +155,11 @@ public class SiteExportService : ISiteExportService
|
||||
}
|
||||
|
||||
private async Task UploadToSharePointIfConfiguredAsync(
|
||||
Site site, SharePointConfig? spConfig, string filePath, Action<string>? updateStatus)
|
||||
Site site,
|
||||
SharePointConfig? spConfig,
|
||||
string filePath,
|
||||
Action<string>? updateStatus,
|
||||
DataSourceFetchResult fetchResult)
|
||||
{
|
||||
if (spConfig is null ||
|
||||
string.IsNullOrWhiteSpace(spConfig.TenantId) ||
|
||||
@@ -165,12 +168,16 @@ public class SiteExportService : ISiteExportService
|
||||
return;
|
||||
|
||||
updateStatus?.Invoke("SharePoint Upload...");
|
||||
var uploadFolder = string.IsNullOrWhiteSpace(fetchResult.SharePointUploadFolderOverride)
|
||||
? spConfig.ExportFolder
|
||||
: fetchResult.SharePointUploadFolderOverride;
|
||||
var uploadLand = fetchResult.SharePointUploadLandOverride ?? site.Land;
|
||||
await _appEventLogService.WriteAsync("Export", "SharePoint Upload gestartet",
|
||||
siteId: site.Id, land: site.Land,
|
||||
details: $"{spConfig.SiteUrl} | {spConfig.ExportFolder}");
|
||||
details: $"{spConfig.SiteUrl} | {uploadFolder}");
|
||||
await _sharePointService.UploadAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret,
|
||||
spConfig.SiteUrl, spConfig.ExportFolder, site.Land, filePath);
|
||||
spConfig.SiteUrl, uploadFolder, uploadLand, filePath);
|
||||
}
|
||||
|
||||
private static string NormalizeSourceSystem(string? sourceSystem)
|
||||
|
||||
@@ -409,13 +409,14 @@ public sealed class StandortePageService : IStandortePageService
|
||||
var trimmedPath = manualImportFilePath.Trim();
|
||||
if (string.IsNullOrWhiteSpace(trimmedPath))
|
||||
throw new InvalidOperationException("Bitte zuerst einen Dateipfad eintragen.");
|
||||
if (!IsSupportedManualImportFile(trimmedPath))
|
||||
var isSharePointReference = LooksLikeSharePointReference(trimmedPath);
|
||||
if (!isSharePointReference && !IsSupportedManualImportFile(trimmedPath))
|
||||
throw new InvalidOperationException("Bitte eine Excel- oder CSV-Datei mit Endung .xlsx oder .csv angeben.");
|
||||
|
||||
if (File.Exists(trimmedPath))
|
||||
return File.GetLastWriteTimeUtc(trimmedPath);
|
||||
|
||||
if (!LooksLikeSharePointReference(trimmedPath))
|
||||
if (!isSharePointReference)
|
||||
throw new InvalidOperationException($"Datei nicht gefunden oder nicht erreichbar: {trimmedPath}");
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
@@ -429,8 +430,16 @@ public sealed class StandortePageService : IStandortePageService
|
||||
throw new InvalidOperationException("Fuer SharePoint-Pruefung fehlt eine vollstaendige SharePoint-Konfiguration in Settings.");
|
||||
}
|
||||
|
||||
var sharePointFileReference = trimmedPath;
|
||||
if (!IsSupportedManualImportFile(trimmedPath))
|
||||
{
|
||||
var latestFile = await _sharePointService.ResolveLatestFileInFolderAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath, string.Empty);
|
||||
sharePointFileReference = latestFile.FileReference;
|
||||
}
|
||||
|
||||
var tempPath = await _sharePointService.DownloadToTempFileAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath);
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, sharePointFileReference);
|
||||
try
|
||||
{
|
||||
return File.GetLastWriteTimeUtc(tempPath);
|
||||
@@ -448,7 +457,7 @@ public sealed class StandortePageService : IStandortePageService
|
||||
var deleteAfterRead = !string.Equals(filePath, manualImportFilePath?.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
return string.Equals(Path.GetExtension(manualImportFilePath?.Trim()), ".csv", StringComparison.OrdinalIgnoreCase)
|
||||
return string.Equals(Path.GetExtension(filePath), ".csv", StringComparison.OrdinalIgnoreCase)
|
||||
? LoadCsvHeaders(filePath)
|
||||
: LoadExcelHeaders(filePath);
|
||||
}
|
||||
@@ -482,8 +491,16 @@ public sealed class StandortePageService : IStandortePageService
|
||||
throw new InvalidOperationException("Fuer SharePoint-Pruefung fehlt eine vollstaendige SharePoint-Konfiguration in Settings.");
|
||||
}
|
||||
|
||||
var sharePointFileReference = trimmedPath;
|
||||
if (!IsSupportedManualImportFile(trimmedPath))
|
||||
{
|
||||
var latestFile = await _sharePointService.ResolveLatestFileInFolderAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath, string.Empty);
|
||||
sharePointFileReference = latestFile.FileReference;
|
||||
}
|
||||
|
||||
return await _sharePointService.DownloadToTempFileAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath);
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, sharePointFileReference);
|
||||
}
|
||||
|
||||
private static void ApplyServer(HanaServer target, HanaServer source)
|
||||
|
||||
@@ -16,13 +16,14 @@ builder.Services.AddSingleton<IFinanceReconciliationService, FinanceReconciliati
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/", () => Results.Redirect("/finance"));
|
||||
app.MapGet("/finance", async (IFinanceReconciliationService finance) =>
|
||||
app.MapGet("/finance", async (IFinanceReconciliationService finance, IDbContextFactory<AppDbContext> dbFactory) =>
|
||||
{
|
||||
var rows = await finance.BuildNetSalesReferenceRowsAsync(2025);
|
||||
var excelReferences = LoadCheckedExcelReferences(ResolveCheckedExcelPath());
|
||||
var spainCsv = LoadSpainSalesCsvProbe(ResolveSpainSalesCsvPath());
|
||||
var germanySample = LoadGermanyExcelProbe(ResolveGermanySamplePath());
|
||||
return Results.Content(BuildPage(rows, databasePath, excelReferences, spainCsv, germanySample), "text/html; charset=utf-8");
|
||||
var coverage = await LoadSiteCoverageAsync(dbFactory, 2025);
|
||||
return Results.Content(BuildPage(rows, databasePath, excelReferences, spainCsv, germanySample, coverage), "text/html; charset=utf-8");
|
||||
});
|
||||
|
||||
app.Run();
|
||||
@@ -215,6 +216,79 @@ static GermanyExcelProbe? LoadGermanyExcelProbe(string? path)
|
||||
};
|
||||
}
|
||||
|
||||
static async Task<List<SiteCoverageRow>> LoadSiteCoverageAsync(IDbContextFactory<AppDbContext> dbFactory, int year)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
var sites = await db.Sites
|
||||
.AsNoTracking()
|
||||
.OrderBy(s => s.Land)
|
||||
.ThenBy(s => s.TSC)
|
||||
.Select(s => new
|
||||
{
|
||||
s.Id,
|
||||
s.Land,
|
||||
s.TSC,
|
||||
s.SourceSystem,
|
||||
s.ManualImportFilePath,
|
||||
s.IsActive
|
||||
})
|
||||
.ToListAsync();
|
||||
var sourceSystems = await db.SourceSystemDefinitions
|
||||
.AsNoTracking()
|
||||
.ToDictionaryAsync(s => s.Code, StringComparer.OrdinalIgnoreCase);
|
||||
var centralBaseRows = await db.CentralSalesRecords
|
||||
.AsNoTracking()
|
||||
.Where(r => (r.InvoiceDate ?? r.ExtractionDate).Year == year)
|
||||
.Select(r => new
|
||||
{
|
||||
r.SiteId,
|
||||
r.SalesPriceValue,
|
||||
Date = r.InvoiceDate ?? r.ExtractionDate,
|
||||
Currency = string.IsNullOrWhiteSpace(r.CompanyCurrency) ? r.SalesCurrency : r.CompanyCurrency
|
||||
})
|
||||
.ToListAsync();
|
||||
var centralRows = centralBaseRows
|
||||
.GroupBy(r => r.SiteId)
|
||||
.ToDictionary(g => g.Key, g => new
|
||||
{
|
||||
Rows = g.Count(),
|
||||
Sales = g.Sum(r => r.SalesPriceValue),
|
||||
MinDate = g.Min(r => r.Date),
|
||||
MaxDate = g.Max(r => r.Date),
|
||||
Currencies = g.Select(r => r.Currency).Distinct(StringComparer.OrdinalIgnoreCase).ToList()
|
||||
});
|
||||
var latestLogs = await db.ExportLogs
|
||||
.AsNoTracking()
|
||||
.GroupBy(l => l.SiteId)
|
||||
.Select(g => g.OrderByDescending(l => l.Id).First())
|
||||
.ToDictionaryAsync(l => l.SiteId);
|
||||
|
||||
return sites.Select(site =>
|
||||
{
|
||||
sourceSystems.TryGetValue(site.SourceSystem, out var sourceSystem);
|
||||
centralRows.TryGetValue(site.Id, out var central);
|
||||
latestLogs.TryGetValue(site.Id, out var latestLog);
|
||||
return new SiteCoverageRow
|
||||
{
|
||||
Land = site.Land,
|
||||
Tsc = site.TSC,
|
||||
SourceSystem = site.SourceSystem,
|
||||
SourceDisplayName = sourceSystem?.DisplayName ?? site.SourceSystem,
|
||||
ConnectionKind = sourceSystem?.ConnectionKind ?? string.Empty,
|
||||
IsActive = site.IsActive,
|
||||
ManualImportPath = site.ManualImportFilePath,
|
||||
RowCount = central?.Rows ?? 0,
|
||||
SalesPriceValue = central?.Sales,
|
||||
MinDate = central?.MinDate,
|
||||
MaxDate = central?.MaxDate,
|
||||
Currencies = central is null ? string.Empty : string.Join(", ", central.Currencies.Where(x => !string.IsNullOrWhiteSpace(x)).OrderBy(x => x, StringComparer.OrdinalIgnoreCase)),
|
||||
LastExportStatus = latestLog?.Status ?? string.Empty,
|
||||
LastExportAt = latestLog?.Timestamp,
|
||||
LastExportError = latestLog?.ErrorMessage ?? string.Empty
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
static decimal ReadProbeDecimal(IXLCell cell)
|
||||
{
|
||||
if (cell.TryGetValue<decimal>(out var decimalValue))
|
||||
@@ -336,7 +410,8 @@ static string BuildPage(
|
||||
string databasePath,
|
||||
IReadOnlyDictionary<string, CheckedExcelReference> excelReferences,
|
||||
SpainSalesCsvProbe? spainCsv,
|
||||
GermanyExcelProbe? germanySample)
|
||||
GermanyExcelProbe? germanySample,
|
||||
IReadOnlyList<SiteCoverageRow> coverage)
|
||||
{
|
||||
var generatedAt = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.GetCultureInfo("de-CH"));
|
||||
var okCount = rows.Count(r => r.Status == "OK");
|
||||
@@ -345,6 +420,7 @@ static string BuildPage(
|
||||
var excelCount = excelReferences.Count;
|
||||
var executiveBriefing = BuildExecutiveBriefing(rows, excelReferences, spainCsv, germanySample);
|
||||
var detailRows = BuildDetailRows(rows, excelReferences, spainCsv);
|
||||
var coverageRows = BuildCoverageRows(coverage);
|
||||
var spainCsvSection = BuildSpainCsvSection(spainCsv);
|
||||
var germanySampleSection = BuildGermanySampleSection(germanySample, excelReferences);
|
||||
|
||||
@@ -540,6 +616,7 @@ static string BuildPage(
|
||||
<nav aria-label="Finance Probe Navigation">
|
||||
<a href="#briefing">Meeting Ampel</a>
|
||||
<a href="#all-sites">Detail alle Laender</a>
|
||||
<a href="#coverage">Datenabdeckung</a>
|
||||
<a href="#germany-sample">Germany Excel</a>
|
||||
<a href="#spain-csv">Spain CSV</a>
|
||||
</nav>
|
||||
@@ -551,6 +628,7 @@ static string BuildPage(
|
||||
<div class="metric"><strong>{{okCount}}</strong><span>OK</span></div>
|
||||
<div class="metric"><strong>{{checkCount}}</strong><span>Pruefen</span></div>
|
||||
<div class="metric"><strong>{{missingCount}}</strong><span>Keine Daten</span></div>
|
||||
<div class="metric"><strong>{{coverage.Count}}</strong><span>Konfigurierte Standorte</span></div>
|
||||
</section>
|
||||
<div id="all-sites" class="table-wrap">
|
||||
<table>
|
||||
@@ -579,6 +657,7 @@ static string BuildPage(
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{coverageRows}}
|
||||
{{germanySampleSection}}
|
||||
{{spainCsvSection}}
|
||||
</main>
|
||||
@@ -587,6 +666,85 @@ static string BuildPage(
|
||||
""";
|
||||
}
|
||||
|
||||
static string BuildCoverageRows(IReadOnlyList<SiteCoverageRow> coverage)
|
||||
{
|
||||
if (coverage.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var rows = string.Join(Environment.NewLine, coverage.Select(row =>
|
||||
{
|
||||
var sourceDetail = row.ConnectionKind switch
|
||||
{
|
||||
"MANUAL_EXCEL" when !string.IsNullOrWhiteSpace(row.ManualImportPath) => row.ManualImportPath,
|
||||
"MANUAL_EXCEL" => "Kein Manual-Dateipfad hinterlegt",
|
||||
_ => row.SourceDisplayName
|
||||
};
|
||||
var period = row.RowCount == 0
|
||||
? "-"
|
||||
: $"{row.MinDate:dd.MM.yyyy} - {row.MaxDate:dd.MM.yyyy}";
|
||||
var lastExport = row.LastExportAt.HasValue
|
||||
? $"{row.LastExportAt:dd.MM.yyyy HH:mm} / {row.LastExportStatus}"
|
||||
: "-";
|
||||
var issue = BuildCoverageIssue(row);
|
||||
|
||||
return $$"""
|
||||
<tr>
|
||||
<td><strong>{{Html(row.Land)}}</strong><div class="small">{{Html(row.Tsc)}}</div></td>
|
||||
<td>{{Html(row.SourceSystem)}}<div class="small">{{Html(row.ConnectionKind)}}</div></td>
|
||||
<td class="wrap">{{Html(sourceDetail)}}</td>
|
||||
<td>{{(row.IsActive ? "Ja" : "Nein")}}</td>
|
||||
<td class="num">{{row.RowCount}}</td>
|
||||
<td class="num">{{Amount(row.SalesPriceValue)}}</td>
|
||||
<td>{{Html(row.Currencies)}}</td>
|
||||
<td>{{Html(period)}}</td>
|
||||
<td>{{Html(lastExport)}}</td>
|
||||
<td class="wrap">{{Html(issue)}}</td>
|
||||
</tr>
|
||||
""";
|
||||
}));
|
||||
|
||||
return $$"""
|
||||
<section id="coverage" style="margin-top:18px;">
|
||||
<h2 style="font-size:18px;margin:0 0 8px;">Datenabdeckung je Standort</h2>
|
||||
<p class="briefing-note">Diese Tabelle zeigt, welche Standorte in der App konfiguriert sind, welche Quelle sie nutzen und ob fuer 2025 bereits Daten in `CentralSalesRecords` liegen.</p>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Standort</th>
|
||||
<th>System</th>
|
||||
<th>Quelle / Pfad</th>
|
||||
<th>Aktiv</th>
|
||||
<th class="num">Zeilen 2025</th>
|
||||
<th class="num">SalesPriceValue</th>
|
||||
<th>Waehrung</th>
|
||||
<th>Periode</th>
|
||||
<th>Letzter Export</th>
|
||||
<th>Hinweis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{{rows}}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
""";
|
||||
}
|
||||
|
||||
static string BuildCoverageIssue(SiteCoverageRow row)
|
||||
{
|
||||
if (!row.IsActive)
|
||||
return "Standort ist deaktiviert.";
|
||||
if (row.ConnectionKind == "MANUAL_EXCEL" && string.IsNullOrWhiteSpace(row.ManualImportPath))
|
||||
return "Manual Excel/CSV-Pfad fehlt.";
|
||||
if (!string.IsNullOrWhiteSpace(row.LastExportError))
|
||||
return row.LastExportError;
|
||||
if (row.RowCount == 0)
|
||||
return "Keine 2025-Daten in CentralSalesRecords. Export pruefen.";
|
||||
if (!string.IsNullOrWhiteSpace(row.LastExportStatus) && !row.LastExportStatus.Equals("OK", StringComparison.OrdinalIgnoreCase))
|
||||
return $"Letzter Exportstatus: {row.LastExportStatus}.";
|
||||
return "Daten vorhanden.";
|
||||
}
|
||||
|
||||
static string BuildDetailRows(
|
||||
IReadOnlyList<NetSalesReferenceRow> rows,
|
||||
IReadOnlyDictionary<string, CheckedExcelReference> excelReferences,
|
||||
@@ -989,3 +1147,22 @@ sealed class GermanyExcelProbe
|
||||
public decimal SalesPriceValueIn2025 { get; set; }
|
||||
public string Currencies { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
sealed class SiteCoverageRow
|
||||
{
|
||||
public string Land { get; set; } = string.Empty;
|
||||
public string Tsc { get; set; } = string.Empty;
|
||||
public string SourceSystem { get; set; } = string.Empty;
|
||||
public string SourceDisplayName { get; set; } = string.Empty;
|
||||
public string ConnectionKind { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; }
|
||||
public string ManualImportPath { get; set; } = string.Empty;
|
||||
public int RowCount { get; set; }
|
||||
public decimal? SalesPriceValue { get; set; }
|
||||
public DateTime? MinDate { get; set; }
|
||||
public DateTime? MaxDate { get; set; }
|
||||
public string Currencies { get; set; } = string.Empty;
|
||||
public string LastExportStatus { get; set; } = string.Empty;
|
||||
public DateTime? LastExportAt { get; set; }
|
||||
public string LastExportError { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
using TrafagSalesExporter.Services;
|
||||
using TrafagSalesExporter.Services.DataSources;
|
||||
|
||||
namespace TrafagSalesExporter.Tests;
|
||||
|
||||
public class ManualExcelDataSourceAdapterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FetchAsync_Uses_Local_File_Directory_As_OutputDirectory()
|
||||
{
|
||||
var filePath = CreateSpainCsv();
|
||||
try
|
||||
{
|
||||
var adapter = new ManualExcelDataSourceAdapter(
|
||||
new FakeSharePointUploadService(filePath),
|
||||
new ManualExcelImportService(),
|
||||
new NoopAppEventLogService());
|
||||
|
||||
var result = await adapter.FetchAsync(CreateContext(filePath));
|
||||
|
||||
Assert.Single(result.Records);
|
||||
Assert.Null(result.ReferenceFilePath);
|
||||
Assert.Equal(Path.GetDirectoryName(Path.GetFullPath(filePath)), result.LocalOutputDirectoryOverride);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchAsync_Uses_SharePoint_Source_Folder_As_UploadFolder()
|
||||
{
|
||||
var filePath = CreateSpainCsv();
|
||||
try
|
||||
{
|
||||
var adapter = new ManualExcelDataSourceAdapter(
|
||||
new FakeSharePointUploadService(filePath),
|
||||
new ManualExcelImportService(),
|
||||
new NoopAppEventLogService());
|
||||
|
||||
var result = await adapter.FetchAsync(CreateContext("https://trafagag.sharepoint.com/sites/WorldwideBIPlatform/Import/Finance/Spanien/Spain_Sales_2025.csv"));
|
||||
|
||||
Assert.Single(result.Records);
|
||||
Assert.Null(result.ReferenceFilePath);
|
||||
Assert.Equal("Import/Finance/Spanien", result.SharePointUploadFolderOverride);
|
||||
Assert.Equal(string.Empty, result.SharePointUploadLandOverride);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchAsync_Uses_Latest_SharePoint_File_When_Path_Is_Folder()
|
||||
{
|
||||
var filePath = CreateSpainCsv();
|
||||
var sharePointService = new FakeSharePointUploadService(
|
||||
filePath,
|
||||
latestFileReference: "Import/Finance/UK_B1/010526_TRUK.xlsx");
|
||||
try
|
||||
{
|
||||
var adapter = new ManualExcelDataSourceAdapter(
|
||||
sharePointService,
|
||||
new ManualExcelImportService(),
|
||||
new NoopAppEventLogService());
|
||||
|
||||
var result = await adapter.FetchAsync(CreateContext("https://trafagag.sharepoint.com/sites/WorldwideBIPlatform/Import/Finance/UK_B1", "TRUK", "England"));
|
||||
|
||||
Assert.Single(result.Records);
|
||||
Assert.Equal("Import/Finance/UK_B1", result.SharePointUploadFolderOverride);
|
||||
Assert.Equal("Import/Finance/UK_B1/010526_TRUK.xlsx", sharePointService.LastDownloadedReference);
|
||||
Assert.Equal("TRUK", sharePointService.LastResolvedTsc);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static DataSourceFetchContext CreateContext(string manualImportPath, string tsc = "TRES", string land = "Spanien") => new()
|
||||
{
|
||||
Site = new Site
|
||||
{
|
||||
Id = 7,
|
||||
TSC = tsc,
|
||||
Land = land,
|
||||
ManualImportFilePath = manualImportPath
|
||||
},
|
||||
SourceDefinition = new SourceSystemDefinition
|
||||
{
|
||||
Code = "MANUAL_EXCEL",
|
||||
ConnectionKind = SourceSystemConnectionKinds.ManualExcel
|
||||
},
|
||||
Settings = new ExportSettings(),
|
||||
SharePointConfig = new SharePointConfig
|
||||
{
|
||||
TenantId = "tenant",
|
||||
ClientId = "client",
|
||||
ClientSecret = "secret",
|
||||
SiteUrl = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform"
|
||||
}
|
||||
};
|
||||
|
||||
private static string CreateSpainCsv()
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.csv");
|
||||
var csv = string.Join(Environment.NewLine,
|
||||
"\"TSC\";\"Land\";\"InvoiceNumber\";\"PositionOnInvoice\";\"Material\";\"Name\";\"ProductGroup\";\"Quantity\";\"CustomerNumber\";\"CustomerName\";\"CustomerCountry\";\"StandardCost\";\"StandardCostCurrency\";\"PurchaseOrderNumber\";\"SalesPriceValue\";\"SalesCurrency\";\"DocumentCurrency\";\"CompanyCurrency\";\"Incoterms2020\";\"SalesResponsibleEmployee\";\"InvoiceDate\";\"DocumentType\"",
|
||||
"\"TRES\";\"Spanien\";\"20241332\";\"20\";\"52871\";\"ECL1.0AP\";\"TRANS\";\"1.000000\";\"302208\";\"INTRONIK AUTOMATIZACION E INST. SL\";\"ESPANA\";\"160.760000\";\"EUR\";\"PC240330\";\"265.000000\";\"EUR\";\"EUR\";\"EUR\";\"EXW\";\"1\";\"2025-01-02 00:00:00\";\"Invoice\"");
|
||||
File.WriteAllText(filePath, csv);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private sealed class FakeSharePointUploadService : ISharePointUploadService
|
||||
{
|
||||
private readonly string _sourceFilePath;
|
||||
private readonly string _latestFileReference;
|
||||
|
||||
public FakeSharePointUploadService(string sourceFilePath, string? latestFileReference = null)
|
||||
{
|
||||
_sourceFilePath = sourceFilePath;
|
||||
_latestFileReference = latestFileReference ?? "Import/Finance/Spanien/Spain_Sales_2025.csv";
|
||||
}
|
||||
|
||||
public string LastDownloadedReference { get; private set; } = string.Empty;
|
||||
|
||||
public string LastResolvedTsc { get; private set; } = string.Empty;
|
||||
|
||||
public Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task<string> DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference)
|
||||
{
|
||||
LastDownloadedReference = fileReference;
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.csv");
|
||||
File.Copy(_sourceFilePath, tempPath);
|
||||
return Task.FromResult(tempPath);
|
||||
}
|
||||
|
||||
public Task<SharePointFileReference> ResolveLatestFileInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc)
|
||||
{
|
||||
LastResolvedTsc = siteTsc;
|
||||
return Task.FromResult(new SharePointFileReference(_latestFileReference, new DateTimeOffset(2026, 5, 1, 0, 0, 0, TimeSpan.Zero)));
|
||||
}
|
||||
|
||||
public Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -43,3 +43,14 @@ Wichtige Praezisierung aus dem Code:
|
||||
## Einsatz
|
||||
|
||||
Die SVG-Dateien koennen direkt im Browser geoeffnet, in Markdown verlinkt oder in Praesentationen eingefuegt werden.
|
||||
|
||||
## Nachtrag Manual Excel/CSV 2026-05-08
|
||||
|
||||
Die Diagramme zeigen Manual Excel/CSV als Quelle. Die aktuelle Detailregel dazu ist:
|
||||
|
||||
- Eine konkrete lokale Datei wird gelesen; die erzeugte Exportdatei wird im gleichen lokalen Ordner abgelegt.
|
||||
- Eine konkrete SharePoint-Datei wird gelesen; die erzeugte Exportdatei wird im gleichen SharePoint-Ordner abgelegt.
|
||||
- Eine SharePoint-Ordnerreferenz wird als dynamische Quelle behandelt.
|
||||
- Bei SharePoint-Ordnern wird die neueste passende `.xlsx`/`.csv` gesucht.
|
||||
- Fuer England/TRUK gilt das Dateimuster `ddMMyy_TRUK.xlsx`, z. B. `010526_TRUK.xlsx`.
|
||||
- Die Ordnerlogik ist generisch fuer Manual-Quellen, nicht hart nur fuer England implementiert.
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1780" height="1240" viewBox="0 0 1780 1240" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Finance-Abgleich 2025: Was passt, was ist offen</title>
|
||||
<desc id="desc">Statusuebersicht zum Net-Sales-Abgleich 2025 gegen check.xlsx auf Basis der Financechef-Entscheide.</desc>
|
||||
<defs>
|
||||
<style>
|
||||
.bg { fill: #f6f7f9; }
|
||||
.panel { fill: #ffffff; stroke: #c9d1dc; stroke-width: 1.2; }
|
||||
.title { font: 700 34px Arial, sans-serif; fill: #172033; }
|
||||
.subtitle { font: 400 17px Arial, sans-serif; fill: #536074; }
|
||||
.h2 { font: 700 20px Arial, sans-serif; fill: #172033; }
|
||||
.h3 { font: 700 15px Arial, sans-serif; fill: #172033; }
|
||||
.txt { font: 400 13.5px Arial, sans-serif; fill: #344054; }
|
||||
.small { font: 400 12px Arial, sans-serif; fill: #667085; }
|
||||
.table-head { fill: #22324a; }
|
||||
.th { font: 700 13px Arial, sans-serif; fill: #ffffff; }
|
||||
.td { font: 400 12.5px Arial, sans-serif; fill: #263241; }
|
||||
.num { font: 400 12.5px Arial, sans-serif; fill: #263241; }
|
||||
.line { stroke: #d8dee8; stroke-width: 1; }
|
||||
.ok { fill: #e7f5ec; stroke: #168a48; stroke-width: 1.5; }
|
||||
.warn { fill: #fff6e6; stroke: #d99020; stroke-width: 1.5; }
|
||||
.miss { fill: #f0f2f5; stroke: #98a2b3; stroke-width: 1.5; }
|
||||
.bad { fill: #fff0f0; stroke: #c93d3d; stroke-width: 1.5; }
|
||||
.pill-ok { fill: #168a48; }
|
||||
.pill-warn { fill: #d99020; }
|
||||
.pill-miss { fill: #667085; }
|
||||
.pill-bad { fill: #c93d3d; }
|
||||
.pill-text { font: 700 11px Arial, sans-serif; fill: #ffffff; }
|
||||
.rule { fill: #eef3f8; stroke: #c3cedb; stroke-width: 1; }
|
||||
.arrow { stroke: #637083; stroke-width: 2; fill: none; marker-end: url(#arrow); }
|
||||
</style>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#637083"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect class="bg" width="1780" height="1240"/>
|
||||
<text class="title" x="54" y="62">Finance-Abgleich 2025 - was passt, was noch nicht</text>
|
||||
<text class="subtitle" x="54" y="92">Stand: letzter lokaler FinanceProbe-Abgleich gegen check.xlsx von Rhino, plus fachliche Entscheide vom Financechef.</text>
|
||||
|
||||
<g transform="translate(54,124)">
|
||||
<rect class="panel" width="1672" height="144" rx="8"/>
|
||||
<text class="h2" x="24" y="34">Verbindliche Finance-Regeln</text>
|
||||
<g transform="translate(24,54)">
|
||||
<rect class="rule" width="250" height="58" rx="6"/>
|
||||
<text class="h3" x="14" y="22">Fuehrende Waehrung</text>
|
||||
<text class="txt" x="14" y="43">Immer Hauswaehrung</text>
|
||||
</g>
|
||||
<g transform="translate(294,54)">
|
||||
<rect class="rule" width="250" height="58" rx="6"/>
|
||||
<text class="h3" x="14" y="22">CHF-Ausweis</text>
|
||||
<text class="txt" x="14" y="43">Budgetkurse, keine Tageskurse</text>
|
||||
</g>
|
||||
<g transform="translate(564,54)">
|
||||
<rect class="rule" width="250" height="58" rx="6"/>
|
||||
<text class="h3" x="14" y="22">Summierung</text>
|
||||
<text class="txt" x="14" y="43">Pro Artikel / Belegposition</text>
|
||||
</g>
|
||||
<g transform="translate(834,54)">
|
||||
<rect class="rule" width="250" height="58" rx="6"/>
|
||||
<text class="h3" x="14" y="22">Net Sales Basis</text>
|
||||
<text class="txt" x="14" y="43">Nettofakturawert</text>
|
||||
</g>
|
||||
<g transform="translate(1104,54)">
|
||||
<rect class="rule" width="250" height="58" rx="6"/>
|
||||
<text class="h3" x="14" y="22">Periode 2025</text>
|
||||
<text class="txt" x="14" y="43">Buchungsdatum</text>
|
||||
</g>
|
||||
<g transform="translate(1374,54)">
|
||||
<rect class="rule" width="250" height="58" rx="6"/>
|
||||
<text class="h3" x="14" y="22">IC / Gutschriften</text>
|
||||
<text class="txt" x="14" y="43">Separat sichtbar ausweisen</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g transform="translate(54,294)">
|
||||
<rect class="panel" width="1672" height="132" rx="8"/>
|
||||
<text class="h2" x="24" y="34">Kurzfazit</text>
|
||||
<g transform="translate(24,58)">
|
||||
<rect class="ok" width="260" height="48" rx="7"/>
|
||||
<text class="h3" x="16" y="20">Passt rechnerisch</text>
|
||||
<text class="txt" x="16" y="39">Indien, aber Waehrungsfeld pruefen</text>
|
||||
</g>
|
||||
<g transform="translate(312,58)">
|
||||
<rect class="warn" width="430" height="48" rx="7"/>
|
||||
<text class="h3" x="16" y="20">Daten vorhanden, aber noch nicht fachlich sauber</text>
|
||||
<text class="txt" x="16" y="39">ES, FR, IT, US brauchen Regel-/Mapping-Klaerung</text>
|
||||
</g>
|
||||
<g transform="translate(770,58)">
|
||||
<rect class="miss" width="410" height="48" rx="7"/>
|
||||
<text class="h3" x="16" y="20">Noch keine belastbaren Ist-Daten</text>
|
||||
<text class="txt" x="16" y="39">AT, CH, DE, UK und weitere Referenzlaender</text>
|
||||
</g>
|
||||
<g transform="translate(1208,58)">
|
||||
<rect class="bad" width="390" height="48" rx="7"/>
|
||||
<text class="h3" x="16" y="20">Technische Anpassung offen</text>
|
||||
<text class="txt" x="16" y="39">FinanceProbe/Export muss strikt Finance-Regeln zeigen</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g transform="translate(54,454)">
|
||||
<rect class="panel" width="1672" height="530" rx="8"/>
|
||||
<text class="h2" x="24" y="34">Laenderstatus gegen Rhino check.xlsx</text>
|
||||
|
||||
<rect class="table-head" x="24" y="56" width="1624" height="34" rx="4"/>
|
||||
<text class="th" x="42" y="78">Status</text>
|
||||
<text class="th" x="156" y="78">Land</text>
|
||||
<text class="th" x="302" y="78">Ist aktuell</text>
|
||||
<text class="th" x="432" y="78">Soll Rhino</text>
|
||||
<text class="th" x="562" y="78">Differenz</text>
|
||||
<text class="th" x="700" y="78">Was passt</text>
|
||||
<text class="th" x="1020" y="78">Was noch nicht passt / naechster Schritt</text>
|
||||
|
||||
<g transform="translate(24,90)">
|
||||
<line class="line" x1="0" y1="48" x2="1624" y2="48"/>
|
||||
<line class="line" x1="0" y1="96" x2="1624" y2="96"/>
|
||||
<line class="line" x1="0" y1="144" x2="1624" y2="144"/>
|
||||
<line class="line" x1="0" y1="192" x2="1624" y2="192"/>
|
||||
<line class="line" x1="0" y1="240" x2="1624" y2="240"/>
|
||||
<line class="line" x1="0" y1="288" x2="1624" y2="288"/>
|
||||
<line class="line" x1="0" y1="336" x2="1624" y2="336"/>
|
||||
<line class="line" x1="0" y1="384" x2="1624" y2="384"/>
|
||||
|
||||
<g transform="translate(0,0)">
|
||||
<rect class="pill-ok" x="16" y="12" width="78" height="24" rx="12"/>
|
||||
<text class="pill-text" x="55" y="28" text-anchor="middle">PASST</text>
|
||||
<text class="td" x="132" y="20">IN</text>
|
||||
<text class="num" x="278" y="20">750'936'591.38</text>
|
||||
<text class="num" x="408" y="20">750'936'591.00</text>
|
||||
<text class="num" x="538" y="20">0.38</text>
|
||||
<text class="td" x="676" y="20">Zahl passt fast exakt gegen Soll.</text>
|
||||
<text class="td" x="996" y="20">Anzeige/Mapping muss INR Hauswaehrung zeigen; aktuell gemischte Waehrungen sichtbar.</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(0,48)">
|
||||
<rect class="pill-warn" x="16" y="12" width="78" height="24" rx="12"/>
|
||||
<text class="pill-text" x="55" y="28" text-anchor="middle">PRUEFEN</text>
|
||||
<text class="td" x="132" y="20">ES</text>
|
||||
<text class="num" x="278" y="20">3'082'320.18</text>
|
||||
<text class="num" x="408" y="20">3'102'334.00</text>
|
||||
<text class="num" x="538" y="20">-20'013.82</text>
|
||||
<text class="td" x="676" y="20">Sage-Datei ist lesbar, 4'341 Zeilen.</text>
|
||||
<text class="td" x="996" y="20">Datumsabgrenzung, Serien REG/LAT/PRO/REC und Gutschriften klaeren.</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(0,96)">
|
||||
<rect class="pill-warn" x="16" y="12" width="78" height="24" rx="12"/>
|
||||
<text class="pill-text" x="55" y="28" text-anchor="middle">PRUEFEN</text>
|
||||
<text class="td" x="132" y="20">FR</text>
|
||||
<text class="num" x="278" y="20">1'414'138.88</text>
|
||||
<text class="num" x="408" y="20">1'471'218.00</text>
|
||||
<text class="num" x="538" y="20">-57'079.12</text>
|
||||
<text class="td" x="676" y="20">B1/HANA-Daten vorhanden.</text>
|
||||
<text class="td" x="996" y="20">Muss auf Nettofakturawert Hauswaehrung + Buchungsdatum ausgerichtet werden.</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(0,144)">
|
||||
<rect class="pill-warn" x="16" y="12" width="78" height="24" rx="12"/>
|
||||
<text class="pill-text" x="55" y="28" text-anchor="middle">PRUEFEN</text>
|
||||
<text class="td" x="132" y="20">IT</text>
|
||||
<text class="num" x="278" y="20">11'866'896.53</text>
|
||||
<text class="num" x="408" y="20">7'669'840.00</text>
|
||||
<text class="num" x="538" y="20">4'197'056.53</text>
|
||||
<text class="td" x="676" y="20">Hauswaehrung/EUR-Daten vorhanden.</text>
|
||||
<text class="td" x="996" y="20">IC/2nd-party separat markieren; nicht still abziehen. Abgrenzung weiter klaeren.</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(0,192)">
|
||||
<rect class="pill-warn" x="16" y="12" width="78" height="24" rx="12"/>
|
||||
<text class="pill-text" x="55" y="28" text-anchor="middle">PRUEFEN</text>
|
||||
<text class="td" x="132" y="20">US</text>
|
||||
<text class="num" x="278" y="20">3'795'763.33</text>
|
||||
<text class="num" x="408" y="20">3'749'865.00</text>
|
||||
<text class="num" x="538" y="20">45'898.33</text>
|
||||
<text class="td" x="676" y="20">B1/HANA-Daten vorhanden.</text>
|
||||
<text class="td" x="996" y="20">Hauswaehrung, Buchungsdatum und Nettofakturawert gegen Quelle pruefen.</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(0,240)">
|
||||
<rect class="pill-miss" x="16" y="12" width="78" height="24" rx="12"/>
|
||||
<text class="pill-text" x="55" y="28" text-anchor="middle">FEHLT</text>
|
||||
<text class="td" x="132" y="20">UK</text>
|
||||
<text class="num" x="278" y="20">-</text>
|
||||
<text class="num" x="408" y="20">3'749'865.00</text>
|
||||
<text class="num" x="538" y="20">-</text>
|
||||
<text class="td" x="676" y="20">UK_B1 SharePoint-Ordner ist konfiguriert.</text>
|
||||
<text class="td" x="996" y="20">Noch keine 2025-Zeilen in CentralSalesRecords; Export/Import neu laufen lassen.</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(0,288)">
|
||||
<rect class="pill-miss" x="16" y="12" width="78" height="24" rx="12"/>
|
||||
<text class="pill-text" x="55" y="28" text-anchor="middle">FEHLT</text>
|
||||
<text class="td" x="132" y="20">DE</text>
|
||||
<text class="num" x="278" y="20">-</text>
|
||||
<text class="num" x="408" y="20">3'635'923.00</text>
|
||||
<text class="num" x="538" y="20">-</text>
|
||||
<text class="td" x="676" y="20">Alphaplan-Mapping ist vorhanden.</text>
|
||||
<text class="td" x="996" y="20">Finalen Alphaplan-Exportpfad/Import pruefen; Sample ist nicht Jahresdatei.</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(0,336)">
|
||||
<rect class="pill-miss" x="16" y="12" width="78" height="24" rx="12"/>
|
||||
<text class="pill-text" x="55" y="28" text-anchor="middle">FEHLT</text>
|
||||
<text class="td" x="132" y="20">AT / CH</text>
|
||||
<text class="num" x="278" y="20">-</text>
|
||||
<text class="num" x="408" y="20">AT 3'443'863</text>
|
||||
<text class="num" x="538" y="20">-</text>
|
||||
<text class="td" x="676" y="20">ZSCHWEIZ OData-Pfad ist vorbereitet.</text>
|
||||
<text class="td" x="996" y="20">Export muss AT/CH nach LAND1 trennen; danach FinanceProbe erneut pruefen.</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(0,384)">
|
||||
<rect class="pill-miss" x="16" y="12" width="78" height="24" rx="12"/>
|
||||
<text class="pill-text" x="55" y="28" text-anchor="middle">FEHLT</text>
|
||||
<text class="td" x="132" y="20">CN/CZ/GFS/JP/MS/MSA/PL/RU</text>
|
||||
<text class="num" x="278" y="20">-</text>
|
||||
<text class="num" x="408" y="20">teils vorhanden</text>
|
||||
<text class="num" x="538" y="20">-</text>
|
||||
<text class="td" x="676" y="20">Sollwerte/Referenzen sichtbar.</text>
|
||||
<text class="td" x="996" y="20">Noch keine Ist-Daten; Quelle, Standort oder Importprozess festlegen.</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g transform="translate(54,1010)">
|
||||
<rect class="panel" width="1672" height="160" rx="8"/>
|
||||
<text class="h2" x="24" y="34">Konkrete Luecken zur Finance-Regel</text>
|
||||
<path class="arrow" d="M 38 62 H 250"/>
|
||||
<text class="h3" x="270" y="66">Ist-Wert muss konsequent aus Nettofakturawert Hauswaehrung kommen</text>
|
||||
<text class="txt" x="270" y="88">Aktuell zeigt die Probe bei einigen Laendern noch alternative technische Kandidaten oder gemischte Waehrungen.</text>
|
||||
<path class="arrow" d="M 38 108 H 250"/>
|
||||
<text class="h3" x="270" y="112">Buchungsdatum und Belegpositionslogik muessen in Quelle/Mapping sichtbar sein</text>
|
||||
<text class="txt" x="270" y="134">Gutschriften separat, IC/2nd-party als Klassifikation; 3rd-party ist Default. Keine stille Bereinigung im Hintergrund.</text>
|
||||
</g>
|
||||
|
||||
<text class="small" x="54" y="1210">Hinweis: Diese SVG ist eine fachliche Statusdoku, keine Codeaenderung. Zahlen stammen aus dem letzten lokalen Abgleich gegen Rhino check.xlsx.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -1,5 +1,74 @@
|
||||
# Last Change 2026-05-04
|
||||
|
||||
## Manual Excel/CSV SharePoint-Ordner und Quellordner-Export 2026-05-08
|
||||
|
||||
Umgesetzte Anpassungen:
|
||||
|
||||
- Manual Excel/CSV Quellen erzeugen nun immer eine neue Exportdatei; die Quelldatei wird nicht als Exportdatei weitergereicht.
|
||||
- Lokale Manual-Dateien schreiben die neue Exportdatei in denselben lokalen Ordner wie die Quelldatei.
|
||||
- SharePoint-Manual-Dateien schreiben die neue Exportdatei in denselben SharePoint-Ordner wie die Quelldatei.
|
||||
- SharePoint-Referenzen ohne Dateiendung werden als Ordner behandelt.
|
||||
- Bei SharePoint-Ordnern sucht die App die neueste passende Excel-/CSV-Datei fuer den Standort.
|
||||
- Fuer datierte Dateien wird das Muster `ddMMyy_TSC.xlsx` bzw. `ddMMyy_TSC.csv` ausgewertet.
|
||||
- Beispiel England/UK:
|
||||
- Ordner: `https://trafagag.sharepoint.com/sites/WorldwideBIPlatform/Import/Finance/UK_B1`
|
||||
- `010526_TRUK.xlsx` wird vor `010426_TRUK.xlsx` gewaehlt.
|
||||
- Falls kein Datum aus dem Dateinamen gelesen werden kann, faellt die Auswahl auf das SharePoint-Aenderungsdatum zurueck.
|
||||
|
||||
Technischer Befund aus den Logs:
|
||||
|
||||
- Spanien konnte die SharePoint-Datei lesen (`4'341` Zeilen), fiel danach aber auf einen ungueltigen lokalen Pfad, weil die URL als lokale Exportdatei behandelt wurde.
|
||||
- Fehlerpfad war sinngemaess `...\https:\trafagag.sharepoint.com\...\Spain_Sales_2025.csv`.
|
||||
- Deutschland hatte keinen manuellen Dateipfad hinterlegt.
|
||||
- England/TRUK zeigte lokal versehentlich auf die Deutschland-Alphaplan-Datei; die lokale DB wurde auf den UK_B1-Ordner korrigiert.
|
||||
|
||||
Codeaenderungen:
|
||||
|
||||
- `DataSourceFetchResult` enthaelt optionale Overrides fuer lokalen Output-Ordner und SharePoint-Zielordner.
|
||||
- `ManualExcelDataSourceAdapter` erkennt SharePoint-Dateien vs. SharePoint-Ordner und waehlt bei Ordnern die neueste passende Datei.
|
||||
- `SharePointUploadService` kann den neuesten passenden Datei-Eintrag in einem SharePoint-Ordner aufloesen.
|
||||
- `SiteExportService` nutzt fuer Manual-Quellen den Quellordner als Zielordner.
|
||||
- `StandortePageService` erlaubt fuer Manual-Importe nun auch SharePoint-Ordnerreferenzen.
|
||||
- Standort-UI-Hilfetext wurde entsprechend angepasst.
|
||||
- `DatabaseSeedService` repariert England/TRUK auf den UK_B1-Ordner, wenn der Manual-Pfad leer ist.
|
||||
|
||||
Letzte technische Verifikation:
|
||||
|
||||
```text
|
||||
dotnet test .\TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj --no-restore --verbosity minimal
|
||||
```
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- Tests erfolgreich, `55/55`
|
||||
- Bekannte MudBlazor-Analyzerwarnungen zu `Dense` bleiben bestehen.
|
||||
|
||||
## FinanceProbe erweitert fuer alle Finance-Referenzen 2026-05-08
|
||||
|
||||
Umgesetzte Anpassungen:
|
||||
|
||||
- FinanceProbe zeigt nun alle aktiven `FinanceReferences` fuer 2025, auch wenn noch kein aktiver/importierter Standort dazu Daten liefert.
|
||||
- Damit werden auch Laender wie AT, CH, CN, CZ, GFS, JP, MS, MSA, PL und RU sichtbar als `Keine Daten`, bis Ist-Daten vorhanden sind.
|
||||
- Zusaetzliche Sektion `Datenabdeckung je Standort`:
|
||||
- Standort / TSC
|
||||
- Quellsystem und Anschlussart
|
||||
- Manual-Datei- oder SharePoint-Pfad
|
||||
- Aktivstatus
|
||||
- Anzahl 2025-Zeilen in `CentralSalesRecords`
|
||||
- Summe `SalesPriceValue`
|
||||
- Waehrungen
|
||||
- importierte Periode
|
||||
- letzter Exportstatus und Hinweis
|
||||
- Referenzschluessel-Erkennung wurde fuer CH/AT praezisiert:
|
||||
- `AT`, `AUT`, `Oesterreich`/`Austria` -> `AT`
|
||||
- `CH`, `CHE`, `Schweiz`/`Switzerland` -> `CH`
|
||||
- Damit koennen Zeilen aus `ZSCHWEIZ` mit `LAND1 = AT` fachlich Oesterreich zugeordnet werden.
|
||||
|
||||
Verifikation:
|
||||
|
||||
- `Tools/FinanceProbe` Build erfolgreich.
|
||||
- Haupttests wurden mit separatem Output/Obj-Pfad ausgefuehrt, damit die laufende App nicht stoert.
|
||||
|
||||
## Mapper-/Finance-Konfiguration konsolidiert 2026-05-07
|
||||
|
||||
Umgesetzte Aufraeumarbeiten:
|
||||
|
||||
Reference in New Issue
Block a user