Support Spain sales delta folder sync

This commit is contained in:
2026-06-05 06:40:31 +02:00
parent 195b430836
commit 825e8063a0
9 changed files with 279 additions and 25 deletions
@@ -5,6 +5,7 @@ public class SalesRecord
public DateTime ExtractionDate { get; set; }
public string SourceSystem { get; set; } = string.Empty;
public string Tsc { get; set; } = string.Empty;
public string SourceLineId { get; set; } = string.Empty;
public int DocumentEntry { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public int PositionOnInvoice { get; set; }
@@ -31,6 +31,7 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
string filePath;
string? localOutputDirectory = null;
string? sharePointUploadFolder = null;
var localManualImportPaths = new List<string>();
var tempManualImportPaths = new List<string>();
try
{
@@ -39,6 +40,12 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
filePath = manualImportPath;
localOutputDirectory = Path.GetDirectoryName(Path.GetFullPath(manualImportPath));
}
else if (Directory.Exists(manualImportPath))
{
localManualImportPaths.AddRange(ResolveLocalManualImportFilesInFolder(manualImportPath, site));
filePath = manualImportPath;
localOutputDirectory = Path.GetFullPath(manualImportPath);
}
else if (LooksLikeSharePointReference(manualImportPath))
{
var spConfig = context.SharePointConfig
@@ -95,9 +102,15 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
siteId: site.Id, land: site.Land, details: filePath);
var records = new List<SalesRecord>();
var readPaths = tempManualImportPaths.Count > 0 ? tempManualImportPaths : [filePath];
var readPaths = tempManualImportPaths.Count > 0
? tempManualImportPaths
: localManualImportPaths.Count > 0
? localManualImportPaths
: [filePath];
foreach (var readPath in readPaths)
records.AddRange(await _manualExcelImportService.ReadSalesRecordsAsync(readPath, site));
if (IsSpainSite(site))
records = DeduplicateSpainSalesRecords(records);
return new DataSourceFetchResult
{
Records = records,
@@ -126,6 +139,85 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
=> LooksLikeSharePointReference(path) &&
string.IsNullOrWhiteSpace(Path.GetExtension(path.TrimEnd('/')));
private static List<string> ResolveLocalManualImportFilesInFolder(string folderPath, Site site)
{
var files = Directory.EnumerateFiles(folderPath)
.Where(IsSupportedManualImportFile)
.Where(path => !IsSpainSite(site) || IsSpainSalesFile(path))
.OrderBy(GetManualImportFileSortKey, StringComparer.OrdinalIgnoreCase)
.ThenBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToList();
if (files.Count == 0)
{
var expected = IsSpainSite(site) ? "Spain_Sales*.csv" : "*.xlsx/*.csv";
throw new InvalidOperationException($"Im Ordner '{folderPath}' wurde keine passende Importdatei gefunden ({expected}).");
}
return files;
}
private static bool IsSupportedManualImportFile(string path)
{
var extension = Path.GetExtension(path);
return extension.Equals(".xlsx", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".csv", StringComparison.OrdinalIgnoreCase);
}
private static bool IsSpainSite(Site site)
=> string.Equals(site.TSC, "TRES", StringComparison.OrdinalIgnoreCase) ||
string.Equals(site.TSC, "TRSE", StringComparison.OrdinalIgnoreCase) ||
string.Equals(site.Land, "Spanien", StringComparison.OrdinalIgnoreCase) ||
string.Equals(site.Land, "Spain", StringComparison.OrdinalIgnoreCase);
private static bool IsSpainSalesFile(string path)
=> Path.GetFileName(path).StartsWith("Spain_Sales", StringComparison.OrdinalIgnoreCase) &&
Path.GetExtension(path).Equals(".csv", StringComparison.OrdinalIgnoreCase);
private static string GetManualImportFileSortKey(string path)
{
var name = Path.GetFileNameWithoutExtension(path);
var rangeIndex = name.IndexOf("_range_", StringComparison.OrdinalIgnoreCase);
if (rangeIndex >= 0)
return "1_" + name[(rangeIndex + "_range_".Length)..];
return "0_" + name;
}
private static List<SalesRecord> DeduplicateSpainSalesRecords(IEnumerable<SalesRecord> records)
{
var ordered = records.ToList();
var keyed = new Dictionary<string, SalesRecord>(StringComparer.OrdinalIgnoreCase);
var unkeyed = new List<SalesRecord>();
foreach (var record in ordered)
{
var key = BuildSpainSalesRecordKey(record);
if (string.IsNullOrWhiteSpace(key))
unkeyed.Add(record);
else
keyed[key] = record;
}
return keyed.Values.Concat(unkeyed).ToList();
}
private static string BuildSpainSalesRecordKey(SalesRecord record)
{
if (!string.IsNullOrWhiteSpace(record.SourceLineId))
return $"source:{record.SourceLineId.Trim()}";
if (!string.IsNullOrWhiteSpace(record.InvoiceNumber))
return string.Join("|",
"invoice",
record.Tsc?.Trim() ?? string.Empty,
record.InvoiceNumber.Trim(),
record.PositionOnInvoice.ToString(System.Globalization.CultureInfo.InvariantCulture),
record.Material?.Trim() ?? string.Empty);
return string.Empty;
}
private static string ResolveSharePointParentFolder(string fileReference, string siteUrl)
{
var remotePath = fileReference.Trim('/').Trim();
@@ -18,6 +18,7 @@ public class ManualExcelImportService : IManualExcelImportService
{
["extractiondate"] = nameof(SalesRecord.ExtractionDate),
["tsc"] = nameof(SalesRecord.Tsc),
["sourcelineid"] = nameof(SalesRecord.SourceLineId),
["documententry"] = nameof(SalesRecord.DocumentEntry),
["invoicenumber"] = nameof(SalesRecord.InvoiceNumber),
["positiononinvoice"] = nameof(SalesRecord.PositionOnInvoice),
@@ -163,6 +164,7 @@ public class ManualExcelImportService : IManualExcelImportService
{
ExtractionDate = ReadDate(headerIndexes, fields, nameof(SalesRecord.ExtractionDate)) ?? DateTime.UtcNow,
Tsc = ReadString(headerIndexes, fields, nameof(SalesRecord.Tsc), site.TSC),
SourceLineId = ReadString(headerIndexes, fields, nameof(SalesRecord.SourceLineId)),
DocumentEntry = (int)Math.Round(ReadDecimal(headerIndexes, fields, nameof(SalesRecord.DocumentEntry))),
InvoiceNumber = ReadString(headerIndexes, fields, nameof(SalesRecord.InvoiceNumber)),
PositionOnInvoice = (int)Math.Round(ReadDecimal(headerIndexes, fields, nameof(SalesRecord.PositionOnInvoice))),
@@ -281,6 +283,7 @@ public class ManualExcelImportService : IManualExcelImportService
{
ExtractionDate = ReadDate(headerIndexes, row, nameof(SalesRecord.ExtractionDate)) ?? DateTime.UtcNow,
Tsc = ReadString(headerIndexes, row, nameof(SalesRecord.Tsc), site.TSC),
SourceLineId = ReadString(headerIndexes, row, nameof(SalesRecord.SourceLineId)),
DocumentEntry = (int)Math.Round(ReadDecimal(headerIndexes, row, nameof(SalesRecord.DocumentEntry))),
InvoiceNumber = ReadString(headerIndexes, row, nameof(SalesRecord.InvoiceNumber)),
PositionOnInvoice = (int)Math.Round(ReadDecimal(headerIndexes, row, nameof(SalesRecord.PositionOnInvoice))),
@@ -113,6 +113,7 @@ public class SharePointUploadService : ISharePointUploadService
var normalizedSiteUrl = Normalize(siteUrl);
var normalizedReference = Normalize(folderReference);
var normalizedTsc = Normalize(siteTsc).ToUpperInvariant();
var isSpainImport = IsSpainManualImport(normalizedTsc, normalizedReference);
if (string.IsNullOrWhiteSpace(normalizedReference))
throw new InvalidOperationException("SharePoint-Ordnerreferenz fehlt.");
@@ -136,16 +137,41 @@ public class SharePointUploadService : ISharePointUploadService
var allCandidates = children?.Value?
.Where(item => item.File is not null)
.Where(item => IsSupportedManualImportFile(item.Name))
.Where(item => MatchesTsc(item.Name, normalizedTsc))
.Select(item => new
.Where(item => isSpainImport ? IsSpainSalesFile(item.Name) : MatchesTsc(item.Name, normalizedTsc))
.Select(item =>
{
var hasSpainRange = TryParseSpainSalesRangeFileName(item.Name, out var rangeStart, out var rangeEnd);
return new
{
Item = item,
FileDate = TryParseDatedSiteFileName(item.Name, normalizedTsc, out var fileDate) ? fileDate : (DateTime?)null,
SpainRangeStart = hasSpainRange ? rangeStart : (DateTime?)null,
SpainRangeEnd = hasSpainRange ? rangeEnd : (DateTime?)null,
AnnualYear = TryParseAnnualSiteFileName(item.Name, normalizedTsc, out var annualYear) ? annualYear : (int?)null,
SnapshotDate = TryParseSnapshotDate(item.Name, out var snapshotDate) ? snapshotDate : (DateTime?)null
};
})
.ToList() ?? [];
if (isSpainImport)
{
var spainCandidates = allCandidates
.OrderBy(x => x.SpainRangeStart is null ? 0 : 1)
.ThenBy(x => x.SpainRangeStart ?? DateTime.MinValue)
.ThenBy(x => x.SpainRangeEnd ?? DateTime.MinValue)
.ThenBy(x => x.Item.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
if (spainCandidates.Count == 0)
throw new InvalidOperationException($"Im SharePoint-Ordner '{folderPath}' wurde keine Spain_Sales*.csv gefunden.");
return spainCandidates
.Select(x => new SharePointFileReference(
string.Join("/", folderPath.Trim('/'), x.Item.Name).Trim('/'),
x.Item.LastModifiedDateTime))
.ToList();
}
if (preferredYear is not null)
{
var annual = allCandidates
@@ -315,6 +341,39 @@ public class SharePointUploadService : ISharePointUploadService
Regex.IsMatch(nameWithoutExtension, $@"(^|[^A-Z0-9]){Regex.Escape(normalizedTsc)}([^A-Z0-9]|$)", RegexOptions.IgnoreCase);
}
private static bool IsSpainManualImport(string normalizedTsc, string folderReference)
=> string.Equals(normalizedTsc, "TRES", StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedTsc, "TRSE", StringComparison.OrdinalIgnoreCase) ||
folderReference.Contains("Spanien", StringComparison.OrdinalIgnoreCase) ||
folderReference.Contains("Spain", StringComparison.OrdinalIgnoreCase);
private static bool IsSpainSalesFile(string? fileName)
=> Path.GetFileName(fileName ?? string.Empty).StartsWith("Spain_Sales", StringComparison.OrdinalIgnoreCase) &&
Path.GetExtension(fileName ?? string.Empty).Equals(".csv", StringComparison.OrdinalIgnoreCase);
private static bool TryParseSpainSalesRangeFileName(string? fileName, out DateTime rangeStart, out DateTime rangeEnd)
{
rangeStart = default;
rangeEnd = default;
var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName ?? string.Empty);
var match = Regex.Match(nameWithoutExtension, @"^Spain_Sales_range_(?<from>\d{8})_to_(?<to>\d{8})$", RegexOptions.IgnoreCase);
if (!match.Success)
return false;
return DateTime.TryParseExact(
match.Groups["from"].Value,
"yyyyMMdd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out rangeStart) &&
DateTime.TryParseExact(
match.Groups["to"].Value,
"yyyyMMdd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out rangeEnd);
}
private static bool TryParseDatedSiteFileName(string? fileName, string normalizedTsc, out DateTime fileDate)
{
fileDate = default;
@@ -80,6 +80,37 @@ public class ManualExcelDataSourceAdapterTests
}
}
[Fact]
public async Task FetchAsync_Reads_Local_Spain_Folder_And_Deduplicates_DeltaRows()
{
var folder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(folder);
try
{
WriteSpainCsv(Path.Combine(folder, "Spain_Sales_2025.csv"),
("line-a", "1001", 10, 100m));
WriteSpainCsv(Path.Combine(folder, "Spain_Sales_range_20260528_to_20260603.csv"),
("line-a", "1001", 10, 125m),
("line-b", "1002", 20, 50m));
var adapter = new ManualExcelDataSourceAdapter(
new FakeSharePointUploadService(Path.Combine(folder, "Spain_Sales_2025.csv")),
new ManualExcelImportService(),
new NoopAppEventLogService());
var result = await adapter.FetchAsync(CreateContext(folder));
Assert.Equal(2, result.Records.Count);
Assert.Equal(125m, Assert.Single(result.Records, r => r.SourceLineId == "line-a").SalesPriceValue);
Assert.Equal(50m, Assert.Single(result.Records, r => r.SourceLineId == "line-b").SalesPriceValue);
Assert.Equal(folder, result.LocalOutputDirectoryOverride);
}
finally
{
Directory.Delete(folder, recursive: true);
}
}
private static DataSourceFetchContext CreateContext(string manualImportPath, string tsc = "TRES", string land = "Spanien") => new()
{
Site = new Site
@@ -107,13 +138,21 @@ public class ManualExcelDataSourceAdapterTests
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);
WriteSpainCsv(filePath, ("line-a", "20241332", 20, 265m));
return filePath;
}
private static void WriteSpainCsv(string filePath, params (string SourceLineId, string InvoiceNumber, int Position, decimal SalesPriceValue)[] rows)
{
var csv = string.Join(Environment.NewLine,
new[]
{
"\"TSC\";\"Land\";\"SourceLineId\";\"InvoiceNumber\";\"PositionOnInvoice\";\"Material\";\"Name\";\"ProductGroup\";\"Quantity\";\"CustomerNumber\";\"CustomerName\";\"CustomerCountry\";\"StandardCost\";\"StandardCostCurrency\";\"PurchaseOrderNumber\";\"SalesPriceValue\";\"SalesCurrency\";\"DocumentCurrency\";\"CompanyCurrency\";\"Incoterms2020\";\"SalesResponsibleEmployee\";\"InvoiceDate\";\"DocumentType\""
}.Concat(rows.Select(row =>
$"\"TRES\";\"Spanien\";\"{row.SourceLineId}\";\"{row.InvoiceNumber}\";\"{row.Position}\";\"52871\";\"ECL1.0AP\";\"TRANS\";\"1.000000\";\"302208\";\"INTRONIK AUTOMATIZACION E INST. SL\";\"ESPANA\";\"160.760000\";\"EUR\";\"PC240330\";\"{row.SalesPriceValue.ToString(System.Globalization.CultureInfo.InvariantCulture)}\";\"EUR\";\"EUR\";\"EUR\";\"EXW\";\"1\";\"2025-01-02 00:00:00\";\"Invoice\"")));
File.WriteAllText(filePath, csv);
}
private sealed class FakeSharePointUploadService : ISharePointUploadService
{
private readonly string _sourceFilePath;
@@ -1,6 +1,6 @@
# Manual-Import und Delta-Stand
Stand: 2026-05-21
Stand: 2026-06-05
Diese Datei beschreibt, wie manuelle Excel-/CSV-Importe aktuell behandelt werden und wie neue Eintraege bzw. Delta-Dateien verarbeitet werden.
@@ -9,7 +9,7 @@ Diese Datei beschreibt, wie manuelle Excel-/CSV-Importe aktuell behandelt werden
| Land / Standort | Quelle aktuell | Dateityp | Neue Eintraege / Deltas | Wie die App auswaehlt | Was beim Standortexport passiert | Finance-Wert |
| --- | --- | --- | --- | --- | --- | --- |
| UK / England `TRUK` | SharePoint-Ordner `Import/Finance/UK_B1` | Sage Excel `.xlsx` | Delta-faehig | Bei Jahreslauf: Jahresdatei fuer `TRUK` plus spaetere datierte Dateien `ddMMyy_TRUK.xlsx` oder `.csv` | Alle gefundenen Dateien werden gelesen und zusammen in `CentralSalesRecords` fuer `TRUK` ersetzt | `Sales Price/Value * Quantity`, Credit Notes negativ, GBP |
| Spanien `TRSE` / `TRES` | SharePoint-Datei oder Ordner / Sage CSV | `.csv` | Vollfile erforderlich, keine Deltas | Wenn Ordner: neueste passende Vollfile-Datei nach TSC/Datum | Datei wird gelesen, Standortdaten werden ersetzt | Sage `ImporteNeto`, REC/Abono/Credit negativ, EUR |
| Spanien `TRSE` / `TRES` | SharePoint-Ordner `Import/Finance/Spanien` / Sage CSV | `.csv` | Delta-faehig mit Basis + `Spain_Sales_range_YYYYMMDD_to_YYYYMMDD.csv` | Wenn Ordner: alle `Spain_Sales*.csv`, Basis zuerst, danach Range-Dateien nach Datum | Alle Dateien werden gelesen, nach `SourceLineId` bzw. Invoice/Position/Material dedupliziert, danach ersetzen die deduplizierten Spanien-Zeilen den bisherigen Spanien-Stand | Sage `SalesPriceValue`/`ImporteNeto`, REC/Abono/Credit negativ, EUR |
| Deutschland `TRDE` | Alphaplan Excel | `.xlsx` | Vollfile/Jahresfile erforderlich, keine Deltas | Pfad/Datei am Standort hinterlegt | Datei wird gelesen, DE-Zeilen ersetzen bisherigen DE-Stand | `NettoPreisGesamtX`, Finance-Regeln: Ausschluesse, GS negativ, 2025-Zwang |
| CH/AT `ZSCHWEIZ` | SAP OData | OData | Kein manueller Delta-Excel-Prozess | App liest SAP-Service | ZSCHWEIZ-Zeilen ersetzen bisherigen Stand | `NetwrHc`, CHF/EUR nach Land |
| FR / IT / US | HANA / SAP B1 | direkte DB | Kein manueller Delta-Excel-Prozess | App liest HANA nach Datum/Schema | Standortdaten werden neu aus HANA aufgebaut | B1 Positions-Netto, Credit Notes negativ |
@@ -44,13 +44,14 @@ Spanien nutzt technisch ebenfalls `MANUAL_EXCEL`, fachlich aber Sage CSV.
Aktueller Implementierungsstand:
- Datei/Ordner kann ueber SharePoint oder lokal hinterlegt werden.
- Bei SharePoint-Ordnern wird die neueste passende Datei nach TSC/Datum ausgewaehlt.
- Spanien muss immer den kompletten relevanten Datenstand liefern.
- Delta-Dateien sind fuer Spanien nicht vorgesehen.
- Praktisch gilt Spanien deshalb als Vollfile-Import.
- Beim Standortexport ersetzt die App den bisherigen Spanien-Stand in `CentralSalesRecords`.
- Wenn versehentlich nur eine Delta-Datei als neueste Datei im Ordner liegt oder direkt als Pfad hinterlegt wird, wuerde die App technisch nur dieses Delta lesen und damit den bisherigen Spanien-Stand ersetzen.
- Es gibt aktuell keine explizite Sperre, die eine Spanien-Delta-Datei erkennt und ablehnt.
- Bei Spanien-Ordnern werden alle `Spain_Sales*.csv` gelesen, auch wenn der Dateiname kein `TRES` enthaelt.
- Basis-/Vollfiles werden vor Range-Dateien gelesen.
- Range-Dateien wie `Spain_Sales_range_20260528_to_20260603.csv` werden nach Range-Start/Ende sortiert.
- Die App dedupliziert die gelesenen Zeilen vor dem Speichern:
- 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.
- 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:
+19 -5
View File
@@ -1,20 +1,22 @@
# RAG Manual Import
Stand: 2026-05-27
Stand: 2026-06-05
## Kurzstand
- Manual-Importe ersetzen pro Standort den aktuellen Stand in `CentralSalesRecords`.
- Delta-Dateien muessen zusammen mit der passenden Basisdatei gelesen werden.
- Das ist aktuell nur fuer UK vorgesehen.
- ES und DE muessen Vollfiles liefern.
- UK liest Jahresdatei plus spaetere Deltas.
- 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.
## Laender
| Standort | Quelle | Delta | Finance-Wert |
| --- | --- | --- | --- |
| UK / `TRUK` | SharePoint `Import/Finance/UK_B1`, Sage Excel | ja | `[Sales Price/Value] * [Quantity]`, Credit Notes negativ, GBP |
| ES / `TRSE`/`TRES` | Sage CSV | nein | `ImporteNeto`, REC/Credit negativ, EUR |
| ES / `TRSE`/`TRES` | Sage CSV `Spain_Sales*.csv` | ja, wenn Ordner mit Basis + Deltas | `SalesPriceValue`/`ImporteNeto`, REC/Credit negativ, EUR |
| DE / `TRDE` | Alphaplan Excel | nein | `NettoPreisGesamtX`, GS negativ, Ausschlussregeln |
## Bedienreihenfolge
@@ -25,8 +27,20 @@ Stand: 2026-05-27
4. Zentrale Datei neu erzeugen.
5. `Finance Summary` und `Finance Details` pruefen.
## Spanien Delta-Sync
- SharePoint-Ordner: `Import/Finance/Spanien`.
- Dateimuster:
- Basis/Vollfile: z. B. `Spain_Sales_2025.csv`.
- Delta/Range: `Spain_Sales_range_20260528_to_20260603.csv`.
- Die App liest bei Spanien-Ordnern alle `Spain_Sales*.csv`, nicht nur die neueste Datei.
- Reihenfolge: Basisdateien zuerst, danach Range-Dateien nach Datum.
- Deduplizierung:
- primaer `SourceLineId`.
- Fallback `TSC + InvoiceNumber + PositionOnInvoice + Material`.
- Danach ersetzt die App den Spanien-Stand in `CentralSalesRecords` mit diesem deduplizierten Gesamtstand.
## Rohquellen Nur Bei Bedarf
- Detailstand: `docs/MANUAL_IMPORT_DELTA_STAND_2026-05-21.md`
- Workflow-Historie: `NEXT_STEPS_2026-04-15.md`
+1
View File
@@ -13,6 +13,7 @@ Stand: 2026-06-05
- Spanien: `Run-SpainRangeExportAndUpload-AllInOne.ps1` exportiert Sage-Range direkt und laedt CSV/Summary via rclone nach SharePoint `trafag-bi:Import/Finance/Spanien`.
- Spanien: Default-Range ist heute minus 7 Tage bis heute; `ToDate` ist exklusiv.
- Spanien: rclone-Fehler `Can't set -v and --log-level` im All-in-one-Script behoben; aktuelle Datei enthaelt kein `--verbose` im Upload.
- Spanien-Import: Ordner mit `Spain_Sales*.csv` werden komplett gelesen; Basis + taegliche Range-Dateien werden nach `SourceLineId` bzw. Invoice/Position/Material dedupliziert.
- Fuer normale Weiterarbeit diese Datei plus den passenden Themen-RAG laden.
## Aktive Themen
+44
View File
@@ -21,6 +21,8 @@ Diese Datei ist fuer tokenarme RAG-Nutzung komprimiert.
- Neu fuer Spanien: rclone-Uploadfehler `Can't set -v and --log-level` behoben; `--verbose` wurde aus dem All-in-one-Upload entfernt.
- Neu fuer Spanien: rclone wird automatisch an mehreren Standardpfaden gesucht, inkl. `C:\Tools\rclone.exe`, `C:\Tools\rclone\rclone.exe`, `C:\Tools\rclone\rclone\rclone.exe` und `PATH`.
- Wichtig fuer Spanien: Nur das All-in-one-Script benoetigt keine separate `Export-SageSpainSalesCsv.ps1`; der alte Wrapper `Run-SpainExportAndUpload.ps1` braucht weiterhin das Export-Script daneben.
- Neu fuer Spanien-Import: SharePoint-/lokale Ordner mit `Spain_Sales*.csv` werden komplett gelesen; Basisdateien und taegliche Range-/Delta-Dateien werden zu einem deduplizierten Gesamtstand zusammengefuehrt.
- Spanien-Dedupe-Regel: primaer `SourceLineId`, Fallback `TSC + InvoiceNumber + PositionOnInvoice + Material`.
- Neu dokumentiert: Spanien-rclone-Anleitung und Package-README auf den All-in-one-Workflow aktualisiert.
- Neu umgesetzt: ES-Referenz 2025 auf `3'082'320.18 EUR` korrigiert; alter Sollwert `3'102'333.61 EUR` als Referenz-/Excel-Fehler dokumentiert.
- Neu umgesetzt: `FinanceProbe` nutzt dieselbe korrigierte ES-Referenz.
@@ -133,6 +135,48 @@ Commits:
- `af097ca Fix Spain all-in-one rclone upload`
- `3fd19a8 Detect nested Spain rclone executable`
## Nachtrag 2026-06-05 Spanien Delta-Sync im Dashboard-Import
Problem:
- Der Sage-Server laedt per rclone taeglich neue Delta-Dateien in den SharePoint-Ordner.
- Dateinamen sind z. B. `Spain_Sales_range_20260528_to_20260603.csv`.
- Bisher haette ein einzelnes Delta beim Standortexport den kompletten Spanienbestand ersetzt, wenn nur dieses Delta gelesen wird.
Umgesetzt:
- `ManualExcelDataSourceAdapter` erkennt Spanien-Ordner lokal und in SharePoint.
- Fuer Spanien werden alle `Spain_Sales*.csv` gelesen, nicht nur die neueste Datei.
- SharePoint-Auswahl akzeptiert Spanien-Dateien ohne `TRES` im Namen.
- Sortierung:
- Basis-/Vollfiles zuerst.
- danach `Spain_Sales_range_YYYYMMDD_to_YYYYMMDD.csv` nach Datumsbereich.
- `ManualExcelImportService` liest `SourceLineId` aus dem CSV.
- Vor dem Speichern wird Spanien dedupliziert:
- primaer `SourceLineId`.
- Fallback `TSC + InvoiceNumber + PositionOnInvoice + Material`.
- `CentralSalesRecords` werden weiterhin pro Standort ersetzt, aber mit dem zusammengesetzten und deduplizierten Gesamtstand aus Basis + Deltas.
Wichtige Bedienregel:
- Fuer Delta-Sync muss im Standort/Manuellen Import der Ordner hinterlegt sein, nicht eine einzelne Delta-Datei.
- Beispielordner lokal/testweise: `SageSpainExportPackage`.
- Beispiel SharePoint: `Import/Finance/Spanien`.
Validierung:
```text
dotnet test TrafagSalesExporter.sln --verbosity minimal --filter ManualExcel
```
Ergebnis: `12/12` Tests gruen.
```text
dotnet test TrafagSalesExporter.sln --verbosity minimal
```
Ergebnis: `83/83` Tests gruen.
## Nachtrag 2026-06-04 Finance Schnelluebersicht / Experten / 3D Datenanalyse
Ziel: