Support Spain sales delta folder sync
This commit is contained in:
@@ -5,6 +5,7 @@ public class SalesRecord
|
|||||||
public DateTime ExtractionDate { get; set; }
|
public DateTime ExtractionDate { get; set; }
|
||||||
public string SourceSystem { get; set; } = string.Empty;
|
public string SourceSystem { get; set; } = string.Empty;
|
||||||
public string Tsc { get; set; } = string.Empty;
|
public string Tsc { get; set; } = string.Empty;
|
||||||
|
public string SourceLineId { get; set; } = string.Empty;
|
||||||
public int DocumentEntry { get; set; }
|
public int DocumentEntry { get; set; }
|
||||||
public string InvoiceNumber { get; set; } = string.Empty;
|
public string InvoiceNumber { get; set; } = string.Empty;
|
||||||
public int PositionOnInvoice { get; set; }
|
public int PositionOnInvoice { get; set; }
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
|||||||
string filePath;
|
string filePath;
|
||||||
string? localOutputDirectory = null;
|
string? localOutputDirectory = null;
|
||||||
string? sharePointUploadFolder = null;
|
string? sharePointUploadFolder = null;
|
||||||
|
var localManualImportPaths = new List<string>();
|
||||||
var tempManualImportPaths = new List<string>();
|
var tempManualImportPaths = new List<string>();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -39,6 +40,12 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
|||||||
filePath = manualImportPath;
|
filePath = manualImportPath;
|
||||||
localOutputDirectory = Path.GetDirectoryName(Path.GetFullPath(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))
|
else if (LooksLikeSharePointReference(manualImportPath))
|
||||||
{
|
{
|
||||||
var spConfig = context.SharePointConfig
|
var spConfig = context.SharePointConfig
|
||||||
@@ -95,9 +102,15 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
|||||||
siteId: site.Id, land: site.Land, details: filePath);
|
siteId: site.Id, land: site.Land, details: filePath);
|
||||||
|
|
||||||
var records = new List<SalesRecord>();
|
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)
|
foreach (var readPath in readPaths)
|
||||||
records.AddRange(await _manualExcelImportService.ReadSalesRecordsAsync(readPath, site));
|
records.AddRange(await _manualExcelImportService.ReadSalesRecordsAsync(readPath, site));
|
||||||
|
if (IsSpainSite(site))
|
||||||
|
records = DeduplicateSpainSalesRecords(records);
|
||||||
return new DataSourceFetchResult
|
return new DataSourceFetchResult
|
||||||
{
|
{
|
||||||
Records = records,
|
Records = records,
|
||||||
@@ -126,6 +139,85 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
|||||||
=> LooksLikeSharePointReference(path) &&
|
=> LooksLikeSharePointReference(path) &&
|
||||||
string.IsNullOrWhiteSpace(Path.GetExtension(path.TrimEnd('/')));
|
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)
|
private static string ResolveSharePointParentFolder(string fileReference, string siteUrl)
|
||||||
{
|
{
|
||||||
var remotePath = fileReference.Trim('/').Trim();
|
var remotePath = fileReference.Trim('/').Trim();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class ManualExcelImportService : IManualExcelImportService
|
|||||||
{
|
{
|
||||||
["extractiondate"] = nameof(SalesRecord.ExtractionDate),
|
["extractiondate"] = nameof(SalesRecord.ExtractionDate),
|
||||||
["tsc"] = nameof(SalesRecord.Tsc),
|
["tsc"] = nameof(SalesRecord.Tsc),
|
||||||
|
["sourcelineid"] = nameof(SalesRecord.SourceLineId),
|
||||||
["documententry"] = nameof(SalesRecord.DocumentEntry),
|
["documententry"] = nameof(SalesRecord.DocumentEntry),
|
||||||
["invoicenumber"] = nameof(SalesRecord.InvoiceNumber),
|
["invoicenumber"] = nameof(SalesRecord.InvoiceNumber),
|
||||||
["positiononinvoice"] = nameof(SalesRecord.PositionOnInvoice),
|
["positiononinvoice"] = nameof(SalesRecord.PositionOnInvoice),
|
||||||
@@ -163,6 +164,7 @@ public class ManualExcelImportService : IManualExcelImportService
|
|||||||
{
|
{
|
||||||
ExtractionDate = ReadDate(headerIndexes, fields, nameof(SalesRecord.ExtractionDate)) ?? DateTime.UtcNow,
|
ExtractionDate = ReadDate(headerIndexes, fields, nameof(SalesRecord.ExtractionDate)) ?? DateTime.UtcNow,
|
||||||
Tsc = ReadString(headerIndexes, fields, nameof(SalesRecord.Tsc), site.TSC),
|
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))),
|
DocumentEntry = (int)Math.Round(ReadDecimal(headerIndexes, fields, nameof(SalesRecord.DocumentEntry))),
|
||||||
InvoiceNumber = ReadString(headerIndexes, fields, nameof(SalesRecord.InvoiceNumber)),
|
InvoiceNumber = ReadString(headerIndexes, fields, nameof(SalesRecord.InvoiceNumber)),
|
||||||
PositionOnInvoice = (int)Math.Round(ReadDecimal(headerIndexes, fields, nameof(SalesRecord.PositionOnInvoice))),
|
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,
|
ExtractionDate = ReadDate(headerIndexes, row, nameof(SalesRecord.ExtractionDate)) ?? DateTime.UtcNow,
|
||||||
Tsc = ReadString(headerIndexes, row, nameof(SalesRecord.Tsc), site.TSC),
|
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))),
|
DocumentEntry = (int)Math.Round(ReadDecimal(headerIndexes, row, nameof(SalesRecord.DocumentEntry))),
|
||||||
InvoiceNumber = ReadString(headerIndexes, row, nameof(SalesRecord.InvoiceNumber)),
|
InvoiceNumber = ReadString(headerIndexes, row, nameof(SalesRecord.InvoiceNumber)),
|
||||||
PositionOnInvoice = (int)Math.Round(ReadDecimal(headerIndexes, row, nameof(SalesRecord.PositionOnInvoice))),
|
PositionOnInvoice = (int)Math.Round(ReadDecimal(headerIndexes, row, nameof(SalesRecord.PositionOnInvoice))),
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ public class SharePointUploadService : ISharePointUploadService
|
|||||||
var normalizedSiteUrl = Normalize(siteUrl);
|
var normalizedSiteUrl = Normalize(siteUrl);
|
||||||
var normalizedReference = Normalize(folderReference);
|
var normalizedReference = Normalize(folderReference);
|
||||||
var normalizedTsc = Normalize(siteTsc).ToUpperInvariant();
|
var normalizedTsc = Normalize(siteTsc).ToUpperInvariant();
|
||||||
|
var isSpainImport = IsSpainManualImport(normalizedTsc, normalizedReference);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(normalizedReference))
|
if (string.IsNullOrWhiteSpace(normalizedReference))
|
||||||
throw new InvalidOperationException("SharePoint-Ordnerreferenz fehlt.");
|
throw new InvalidOperationException("SharePoint-Ordnerreferenz fehlt.");
|
||||||
@@ -136,16 +137,41 @@ public class SharePointUploadService : ISharePointUploadService
|
|||||||
var allCandidates = children?.Value?
|
var allCandidates = children?.Value?
|
||||||
.Where(item => item.File is not null)
|
.Where(item => item.File is not null)
|
||||||
.Where(item => IsSupportedManualImportFile(item.Name))
|
.Where(item => IsSupportedManualImportFile(item.Name))
|
||||||
.Where(item => MatchesTsc(item.Name, normalizedTsc))
|
.Where(item => isSpainImport ? IsSpainSalesFile(item.Name) : MatchesTsc(item.Name, normalizedTsc))
|
||||||
.Select(item => new
|
.Select(item =>
|
||||||
|
{
|
||||||
|
var hasSpainRange = TryParseSpainSalesRangeFileName(item.Name, out var rangeStart, out var rangeEnd);
|
||||||
|
return new
|
||||||
{
|
{
|
||||||
Item = item,
|
Item = item,
|
||||||
FileDate = TryParseDatedSiteFileName(item.Name, normalizedTsc, out var fileDate) ? fileDate : (DateTime?)null,
|
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,
|
AnnualYear = TryParseAnnualSiteFileName(item.Name, normalizedTsc, out var annualYear) ? annualYear : (int?)null,
|
||||||
SnapshotDate = TryParseSnapshotDate(item.Name, out var snapshotDate) ? snapshotDate : (DateTime?)null
|
SnapshotDate = TryParseSnapshotDate(item.Name, out var snapshotDate) ? snapshotDate : (DateTime?)null
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.ToList() ?? [];
|
.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)
|
if (preferredYear is not null)
|
||||||
{
|
{
|
||||||
var annual = allCandidates
|
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);
|
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)
|
private static bool TryParseDatedSiteFileName(string? fileName, string normalizedTsc, out DateTime fileDate)
|
||||||
{
|
{
|
||||||
fileDate = default;
|
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()
|
private static DataSourceFetchContext CreateContext(string manualImportPath, string tsc = "TRES", string land = "Spanien") => new()
|
||||||
{
|
{
|
||||||
Site = new Site
|
Site = new Site
|
||||||
@@ -107,13 +138,21 @@ public class ManualExcelDataSourceAdapterTests
|
|||||||
private static string CreateSpainCsv()
|
private static string CreateSpainCsv()
|
||||||
{
|
{
|
||||||
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.csv");
|
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.csv");
|
||||||
var csv = string.Join(Environment.NewLine,
|
WriteSpainCsv(filePath, ("line-a", "20241332", 20, 265m));
|
||||||
"\"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;
|
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 sealed class FakeSharePointUploadService : ISharePointUploadService
|
||||||
{
|
{
|
||||||
private readonly string _sourceFilePath;
|
private readonly string _sourceFilePath;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Manual-Import und Delta-Stand
|
# 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.
|
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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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:
|
Aktueller Implementierungsstand:
|
||||||
|
|
||||||
- Datei/Ordner kann ueber SharePoint oder lokal hinterlegt werden.
|
- Datei/Ordner kann ueber SharePoint oder lokal hinterlegt werden.
|
||||||
- Bei SharePoint-Ordnern wird die neueste passende Datei nach TSC/Datum ausgewaehlt.
|
- Bei Spanien-Ordnern werden alle `Spain_Sales*.csv` gelesen, auch wenn der Dateiname kein `TRES` enthaelt.
|
||||||
- Spanien muss immer den kompletten relevanten Datenstand liefern.
|
- Basis-/Vollfiles werden vor Range-Dateien gelesen.
|
||||||
- Delta-Dateien sind fuer Spanien nicht vorgesehen.
|
- Range-Dateien wie `Spain_Sales_range_20260528_to_20260603.csv` werden nach Range-Start/Ende sortiert.
|
||||||
- Praktisch gilt Spanien deshalb als Vollfile-Import.
|
- Die App dedupliziert die gelesenen Zeilen vor dem Speichern:
|
||||||
- Beim Standortexport ersetzt die App den bisherigen Spanien-Stand in `CentralSalesRecords`.
|
- primaer ueber `SourceLineId`.
|
||||||
- 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.
|
- Fallback ueber `TSC + InvoiceNumber + PositionOnInvoice + Material`.
|
||||||
- Es gibt aktuell keine explizite Sperre, die eine Spanien-Delta-Datei erkennt und ablehnt.
|
- 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:
|
Finance-Logik:
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
# RAG Manual Import
|
# RAG Manual Import
|
||||||
|
|
||||||
Stand: 2026-05-27
|
Stand: 2026-06-05
|
||||||
|
|
||||||
## Kurzstand
|
## Kurzstand
|
||||||
|
|
||||||
- Manual-Importe ersetzen pro Standort den aktuellen Stand in `CentralSalesRecords`.
|
- Manual-Importe ersetzen pro Standort den aktuellen Stand in `CentralSalesRecords`.
|
||||||
- Delta-Dateien muessen zusammen mit der passenden Basisdatei gelesen werden.
|
- Delta-Dateien muessen zusammen mit der passenden Basisdatei gelesen werden.
|
||||||
- Das ist aktuell nur fuer UK vorgesehen.
|
- UK liest Jahresdatei plus spaetere Deltas.
|
||||||
- ES und DE muessen Vollfiles liefern.
|
- 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
|
## Laender
|
||||||
|
|
||||||
| Standort | Quelle | Delta | Finance-Wert |
|
| Standort | Quelle | Delta | Finance-Wert |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| UK / `TRUK` | SharePoint `Import/Finance/UK_B1`, Sage Excel | ja | `[Sales Price/Value] * [Quantity]`, Credit Notes negativ, GBP |
|
| 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 |
|
| DE / `TRDE` | Alphaplan Excel | nein | `NettoPreisGesamtX`, GS negativ, Ausschlussregeln |
|
||||||
|
|
||||||
## Bedienreihenfolge
|
## Bedienreihenfolge
|
||||||
@@ -25,8 +27,20 @@ Stand: 2026-05-27
|
|||||||
4. Zentrale Datei neu erzeugen.
|
4. Zentrale Datei neu erzeugen.
|
||||||
5. `Finance Summary` und `Finance Details` pruefen.
|
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
|
## Rohquellen Nur Bei Bedarf
|
||||||
|
|
||||||
- Detailstand: `docs/MANUAL_IMPORT_DELTA_STAND_2026-05-21.md`
|
- Detailstand: `docs/MANUAL_IMPORT_DELTA_STAND_2026-05-21.md`
|
||||||
- Workflow-Historie: `NEXT_STEPS_2026-04-15.md`
|
- Workflow-Historie: `NEXT_STEPS_2026-04-15.md`
|
||||||
|
|
||||||
|
|||||||
@@ -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: `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: 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: 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.
|
- Fuer normale Weiterarbeit diese Datei plus den passenden Themen-RAG laden.
|
||||||
|
|
||||||
## Aktive Themen
|
## Aktive Themen
|
||||||
|
|||||||
@@ -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-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`.
|
- 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.
|
- 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 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: ES-Referenz 2025 auf `3'082'320.18 EUR` korrigiert; alter Sollwert `3'102'333.61 EUR` als Referenz-/Excel-Fehler dokumentiert.
|
||||||
- Neu umgesetzt: `FinanceProbe` nutzt dieselbe korrigierte ES-Referenz.
|
- Neu umgesetzt: `FinanceProbe` nutzt dieselbe korrigierte ES-Referenz.
|
||||||
@@ -133,6 +135,48 @@ Commits:
|
|||||||
- `af097ca Fix Spain all-in-one rclone upload`
|
- `af097ca Fix Spain all-in-one rclone upload`
|
||||||
- `3fd19a8 Detect nested Spain rclone executable`
|
- `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
|
## Nachtrag 2026-06-04 Finance Schnelluebersicht / Experten / 3D Datenanalyse
|
||||||
|
|
||||||
Ziel:
|
Ziel:
|
||||||
|
|||||||
Reference in New Issue
Block a user