zentraler export
This commit is contained in:
@@ -81,6 +81,8 @@ public class ConfigTransferService : IConfigTransferService
|
||||
UsernameOverride = includeSecrets ? site.UsernameOverride : null,
|
||||
PasswordOverride = includeSecrets ? site.PasswordOverride : null,
|
||||
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
||||
ManualImportFilePath = site.ManualImportFilePath,
|
||||
ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc,
|
||||
SapServiceUrl = site.SapServiceUrl,
|
||||
SapEntitySet = site.SapEntitySet,
|
||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||
@@ -242,6 +244,8 @@ public class ConfigTransferService : IConfigTransferService
|
||||
UsernameOverride = package.IncludesSecrets ? site.UsernameOverride ?? string.Empty : preserved.UsernameOverride ?? string.Empty,
|
||||
PasswordOverride = package.IncludesSecrets ? site.PasswordOverride ?? string.Empty : preserved.PasswordOverride ?? string.Empty,
|
||||
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
||||
ManualImportFilePath = site.ManualImportFilePath,
|
||||
ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc,
|
||||
SapServiceUrl = site.SapServiceUrl,
|
||||
SapEntitySet = site.SapEntitySet,
|
||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||
|
||||
@@ -54,6 +54,8 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
AddColumnIfMissing(db, "Sites", "UsernameOverride", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "PasswordOverride", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "LocalExportFolderOverride", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "ManualImportFilePath", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "ManualImportLastUploadedAtUtc", "TEXT NULL");
|
||||
AddColumnIfMissing(db, "Sites", "SapServiceUrl", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySet", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySetsCache", "TEXT NOT NULL DEFAULT ''");
|
||||
@@ -128,6 +130,8 @@ CREATE TABLE Sites (
|
||||
UsernameOverride TEXT NOT NULL DEFAULT '',
|
||||
PasswordOverride TEXT NOT NULL DEFAULT '',
|
||||
LocalExportFolderOverride TEXT NOT NULL DEFAULT '',
|
||||
ManualImportFilePath TEXT NOT NULL DEFAULT '',
|
||||
ManualImportLastUploadedAtUtc TEXT NULL,
|
||||
SapServiceUrl TEXT NOT NULL DEFAULT '',
|
||||
SapEntitySet TEXT NOT NULL DEFAULT '',
|
||||
SapEntitySetsCache TEXT NOT NULL DEFAULT '',
|
||||
@@ -145,7 +149,7 @@ CREATE TABLE Sites (
|
||||
INSERT INTO Sites (
|
||||
Id, HanaServerId, Schema, TSC, Land, SourceSystem,
|
||||
UsernameOverride, PasswordOverride, LocalExportFolderOverride, SapServiceUrl, SapEntitySet,
|
||||
SapEntitySetsCache, SapEntitySetsRefreshedAtUtc, IsActive
|
||||
ManualImportFilePath, ManualImportLastUploadedAtUtc, SapEntitySetsCache, SapEntitySetsRefreshedAtUtc, IsActive
|
||||
)
|
||||
SELECT
|
||||
Id, HanaServerId, Schema, TSC, Land,
|
||||
@@ -153,6 +157,8 @@ SELECT
|
||||
COALESCE(UsernameOverride, ''),
|
||||
COALESCE(PasswordOverride, ''),
|
||||
COALESCE(LocalExportFolderOverride, ''),
|
||||
COALESCE(ManualImportFilePath, ''),
|
||||
ManualImportLastUploadedAtUtc,
|
||||
COALESCE(SapServiceUrl, ''),
|
||||
COALESCE(SapEntitySet, ''),
|
||||
COALESCE(SapEntitySetsCache, ''),
|
||||
|
||||
@@ -14,6 +14,8 @@ public class ExportOrchestrationService
|
||||
public event Action? OnExportStatusChanged;
|
||||
|
||||
private readonly Dictionary<int, string> _runningExports = new();
|
||||
private bool _consolidatedExportRunning;
|
||||
private string _consolidatedExportStatus = string.Empty;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public ExportOrchestrationService(
|
||||
@@ -44,6 +46,22 @@ public class ExportOrchestrationService
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsConsolidatedExporting()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _consolidatedExportRunning;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetConsolidatedExportStatus()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _consolidatedExportStatus;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExportAllAsync()
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
@@ -57,7 +75,12 @@ public class ExportOrchestrationService
|
||||
consolidatedRecords.AddRange(result.Records);
|
||||
}
|
||||
|
||||
await _consolidatedExportService.ExportAsync(consolidatedRecords);
|
||||
await RunConsolidatedExportAsync(consolidatedRecords);
|
||||
}
|
||||
|
||||
public async Task<string?> ExportConsolidatedOnlyAsync()
|
||||
{
|
||||
return await RunConsolidatedExportAsync(null);
|
||||
}
|
||||
|
||||
public async Task<SiteExportResult?> ExportSiteByIdAsync(int siteId)
|
||||
@@ -112,4 +135,31 @@ public class ExportOrchestrationService
|
||||
{
|
||||
OnExportStatusChanged?.Invoke();
|
||||
}
|
||||
|
||||
private async Task<string?> RunConsolidatedExportAsync(List<SalesRecord>? records)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_consolidatedExportRunning)
|
||||
return null;
|
||||
|
||||
_consolidatedExportRunning = true;
|
||||
_consolidatedExportStatus = "Zentrale Datei erzeugen...";
|
||||
}
|
||||
NotifyChanged();
|
||||
|
||||
try
|
||||
{
|
||||
return await _consolidatedExportService.ExportAsync(records ?? []);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_consolidatedExportRunning = false;
|
||||
_consolidatedExportStatus = string.Empty;
|
||||
}
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IManualExcelImportService
|
||||
{
|
||||
Task<List<SalesRecord>> ReadSalesRecordsAsync(string filePath, Site site);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
using System.Globalization;
|
||||
using ClosedXML.Excel;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class ManualExcelImportService : IManualExcelImportService
|
||||
{
|
||||
private static readonly Dictionary<string, string> HeaderMap = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["extractiondate"] = nameof(SalesRecord.ExtractionDate),
|
||||
["tsc"] = nameof(SalesRecord.Tsc),
|
||||
["invoicenumber"] = nameof(SalesRecord.InvoiceNumber),
|
||||
["positiononinvoice"] = nameof(SalesRecord.PositionOnInvoice),
|
||||
["material"] = nameof(SalesRecord.Material),
|
||||
["name"] = nameof(SalesRecord.Name),
|
||||
["productgroup"] = nameof(SalesRecord.ProductGroup),
|
||||
["quantity"] = nameof(SalesRecord.Quantity),
|
||||
["suppliernumber"] = nameof(SalesRecord.SupplierNumber),
|
||||
["suppliername"] = nameof(SalesRecord.SupplierName),
|
||||
["suppliercountry"] = nameof(SalesRecord.SupplierCountry),
|
||||
["customernumber"] = nameof(SalesRecord.CustomerNumber),
|
||||
["customername"] = nameof(SalesRecord.CustomerName),
|
||||
["customercountry"] = nameof(SalesRecord.CustomerCountry),
|
||||
["customerindustry"] = nameof(SalesRecord.CustomerIndustry),
|
||||
["standardcost"] = nameof(SalesRecord.StandardCost),
|
||||
["standardcostcurrency"] = nameof(SalesRecord.StandardCostCurrency),
|
||||
["purchaseordernumber"] = nameof(SalesRecord.PurchaseOrderNumber),
|
||||
["salespricevalue"] = nameof(SalesRecord.SalesPriceValue),
|
||||
["salescurrency"] = nameof(SalesRecord.SalesCurrency),
|
||||
["incoterms2020"] = nameof(SalesRecord.Incoterms2020),
|
||||
["salesresponsibleemployee"] = nameof(SalesRecord.SalesResponsibleEmployee),
|
||||
["invoicedate"] = nameof(SalesRecord.InvoiceDate),
|
||||
["orderdate"] = nameof(SalesRecord.OrderDate),
|
||||
["land"] = nameof(SalesRecord.Land),
|
||||
["documenttype"] = nameof(SalesRecord.DocumentType)
|
||||
};
|
||||
|
||||
public Task<List<SalesRecord>> ReadSalesRecordsAsync(string filePath, Site site)
|
||||
{
|
||||
using var workbook = new XLWorkbook(filePath);
|
||||
var worksheet = workbook.Worksheets.FirstOrDefault()
|
||||
?? throw new InvalidOperationException("Die Excel-Datei enthält kein Arbeitsblatt.");
|
||||
var usedRange = worksheet.RangeUsed()
|
||||
?? throw new InvalidOperationException("Die Excel-Datei enthält keine Daten.");
|
||||
|
||||
var headerRow = usedRange.FirstRow();
|
||||
var headerIndexes = BuildHeaderIndexMap(headerRow);
|
||||
var rows = new List<SalesRecord>();
|
||||
|
||||
foreach (var row in usedRange.RowsUsed().Skip(1))
|
||||
{
|
||||
if (IsRowEmpty(row))
|
||||
continue;
|
||||
|
||||
rows.Add(new SalesRecord
|
||||
{
|
||||
ExtractionDate = ReadDate(headerIndexes, row, nameof(SalesRecord.ExtractionDate)) ?? DateTime.UtcNow,
|
||||
Tsc = ReadString(headerIndexes, row, nameof(SalesRecord.Tsc), site.TSC),
|
||||
InvoiceNumber = ReadString(headerIndexes, row, nameof(SalesRecord.InvoiceNumber)),
|
||||
PositionOnInvoice = (int)Math.Round(ReadDecimal(headerIndexes, row, nameof(SalesRecord.PositionOnInvoice))),
|
||||
Material = ReadString(headerIndexes, row, nameof(SalesRecord.Material)),
|
||||
Name = ReadString(headerIndexes, row, nameof(SalesRecord.Name)),
|
||||
ProductGroup = ReadString(headerIndexes, row, nameof(SalesRecord.ProductGroup)),
|
||||
Quantity = ReadDecimal(headerIndexes, row, nameof(SalesRecord.Quantity)),
|
||||
SupplierNumber = ReadString(headerIndexes, row, nameof(SalesRecord.SupplierNumber)),
|
||||
SupplierName = ReadString(headerIndexes, row, nameof(SalesRecord.SupplierName)),
|
||||
SupplierCountry = ReadString(headerIndexes, row, nameof(SalesRecord.SupplierCountry)),
|
||||
CustomerNumber = ReadString(headerIndexes, row, nameof(SalesRecord.CustomerNumber)),
|
||||
CustomerName = ReadString(headerIndexes, row, nameof(SalesRecord.CustomerName)),
|
||||
CustomerCountry = ReadString(headerIndexes, row, nameof(SalesRecord.CustomerCountry)),
|
||||
CustomerIndustry = ReadString(headerIndexes, row, nameof(SalesRecord.CustomerIndustry)),
|
||||
StandardCost = ReadDecimal(headerIndexes, row, nameof(SalesRecord.StandardCost)),
|
||||
StandardCostCurrency = ReadString(headerIndexes, row, nameof(SalesRecord.StandardCostCurrency)),
|
||||
PurchaseOrderNumber = ReadString(headerIndexes, row, nameof(SalesRecord.PurchaseOrderNumber)),
|
||||
SalesPriceValue = ReadDecimal(headerIndexes, row, nameof(SalesRecord.SalesPriceValue)),
|
||||
SalesCurrency = ReadString(headerIndexes, row, nameof(SalesRecord.SalesCurrency)),
|
||||
Incoterms2020 = ReadString(headerIndexes, row, nameof(SalesRecord.Incoterms2020)),
|
||||
SalesResponsibleEmployee = ReadString(headerIndexes, row, nameof(SalesRecord.SalesResponsibleEmployee)),
|
||||
InvoiceDate = ReadDate(headerIndexes, row, nameof(SalesRecord.InvoiceDate)),
|
||||
OrderDate = ReadDate(headerIndexes, row, nameof(SalesRecord.OrderDate)),
|
||||
Land = ReadString(headerIndexes, row, nameof(SalesRecord.Land), site.Land),
|
||||
DocumentType = ReadString(headerIndexes, row, nameof(SalesRecord.DocumentType))
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(rows);
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> BuildHeaderIndexMap(IXLRangeRow headerRow)
|
||||
{
|
||||
var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var cell in headerRow.CellsUsed())
|
||||
{
|
||||
var normalizedHeader = NormalizeHeader(cell.GetString());
|
||||
if (string.IsNullOrWhiteSpace(normalizedHeader))
|
||||
continue;
|
||||
|
||||
if (HeaderMap.TryGetValue(normalizedHeader, out var targetField))
|
||||
result[targetField] = cell.Address.ColumnNumber;
|
||||
}
|
||||
|
||||
if (!result.ContainsKey(nameof(SalesRecord.InvoiceNumber)))
|
||||
throw new InvalidOperationException("Die Excel-Datei hat nicht das erwartete Exportformat. Spalte 'Invoice Number' fehlt.");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsRowEmpty(IXLRangeRow row)
|
||||
=> row.CellsUsed().All(cell => string.IsNullOrWhiteSpace(cell.GetFormattedString()));
|
||||
|
||||
private static string ReadString(Dictionary<string, int> headerIndexes, IXLRangeRow row, string fieldName, string fallback = "")
|
||||
{
|
||||
if (!headerIndexes.TryGetValue(fieldName, out var index))
|
||||
return fallback;
|
||||
|
||||
var value = row.Cell(index).GetFormattedString().Trim();
|
||||
return string.IsNullOrWhiteSpace(value) ? fallback : value;
|
||||
}
|
||||
|
||||
private static decimal ReadDecimal(Dictionary<string, int> headerIndexes, IXLRangeRow row, string fieldName)
|
||||
{
|
||||
if (!headerIndexes.TryGetValue(fieldName, out var index))
|
||||
return 0m;
|
||||
|
||||
var cell = row.Cell(index);
|
||||
if (cell.TryGetValue<decimal>(out var decimalValue))
|
||||
return decimalValue;
|
||||
if (cell.TryGetValue<double>(out var doubleValue))
|
||||
return Convert.ToDecimal(doubleValue, CultureInfo.InvariantCulture);
|
||||
|
||||
var text = cell.GetFormattedString().Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return 0m;
|
||||
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out decimalValue))
|
||||
return decimalValue;
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("de-CH"), out decimalValue))
|
||||
return decimalValue;
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("de-DE"), out decimalValue))
|
||||
return decimalValue;
|
||||
|
||||
return 0m;
|
||||
}
|
||||
|
||||
private static DateTime? ReadDate(Dictionary<string, int> headerIndexes, IXLRangeRow row, string fieldName)
|
||||
{
|
||||
if (!headerIndexes.TryGetValue(fieldName, out var index))
|
||||
return null;
|
||||
|
||||
var cell = row.Cell(index);
|
||||
if (cell.TryGetValue<DateTime>(out var dateValue))
|
||||
return dateValue;
|
||||
|
||||
var text = cell.GetFormattedString().Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return null;
|
||||
|
||||
var formats = new[]
|
||||
{
|
||||
"dd.MM.yyyy HH:mm:ss",
|
||||
"dd.MM.yyyy",
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
"yyyy-MM-dd",
|
||||
"O"
|
||||
};
|
||||
|
||||
if (DateTime.TryParseExact(text, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out dateValue))
|
||||
return dateValue;
|
||||
if (DateTime.TryParse(text, CultureInfo.GetCultureInfo("de-CH"), DateTimeStyles.AssumeLocal, out dateValue))
|
||||
return dateValue;
|
||||
if (DateTime.TryParse(text, CultureInfo.GetCultureInfo("de-DE"), DateTimeStyles.AssumeLocal, out dateValue))
|
||||
return dateValue;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NormalizeHeader(string value)
|
||||
{
|
||||
var chars = value
|
||||
.Where(char.IsLetterOrDigit)
|
||||
.Select(char.ToLowerInvariant)
|
||||
.ToArray();
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public class SiteExportService : ISiteExportService
|
||||
private readonly ISharePointUploadService _sharePointService;
|
||||
private readonly IRecordTransformationService _transformationService;
|
||||
private readonly ICentralSalesRecordService _centralSalesRecordService;
|
||||
private readonly IManualExcelImportService _manualExcelImportService;
|
||||
private readonly IAppEventLogService _appEventLogService;
|
||||
private readonly ILogger<SiteExportService> _logger;
|
||||
|
||||
@@ -27,6 +28,7 @@ public class SiteExportService : ISiteExportService
|
||||
ISharePointUploadService sharePointService,
|
||||
IRecordTransformationService transformationService,
|
||||
ICentralSalesRecordService centralSalesRecordService,
|
||||
IManualExcelImportService manualExcelImportService,
|
||||
IAppEventLogService appEventLogService,
|
||||
ILogger<SiteExportService> logger)
|
||||
{
|
||||
@@ -38,6 +40,7 @@ public class SiteExportService : ISiteExportService
|
||||
_sharePointService = sharePointService;
|
||||
_transformationService = transformationService;
|
||||
_centralSalesRecordService = centralSalesRecordService;
|
||||
_manualExcelImportService = manualExcelImportService;
|
||||
_appEventLogService = appEventLogService;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -96,6 +99,30 @@ public class SiteExportService : ISiteExportService
|
||||
filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
||||
log.RowCount = records.Count;
|
||||
}
|
||||
else if (sourceSystem == "MANUAL_EXCEL")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(site.ManualImportFilePath))
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keine manuelle Excel-Datei.");
|
||||
if (!File.Exists(site.ManualImportFilePath))
|
||||
throw new InvalidOperationException($"Die manuelle Excel-Datei wurde nicht gefunden: {site.ManualImportFilePath}");
|
||||
|
||||
updateStatus?.Invoke("Manuelle Excel lesen...");
|
||||
await _appEventLogService.WriteAsync("Export", "Manuelle Excel lesen", siteId: site.Id, land: site.Land,
|
||||
details: site.ManualImportFilePath);
|
||||
records = await _manualExcelImportService.ReadSalesRecordsAsync(site.ManualImportFilePath, site);
|
||||
|
||||
updateStatus?.Invoke("Transformationen anwenden...");
|
||||
await _appEventLogService.WriteAsync("Export", "Transformationen anwenden", siteId: site.Id, land: site.Land,
|
||||
details: $"Records vor Transformation={records.Count}");
|
||||
var rules = await db.FieldTransformationRules
|
||||
.Where(r => r.IsActive && r.SourceSystem == sourceSystem)
|
||||
.OrderBy(r => r.SortOrder)
|
||||
.ToListAsync();
|
||||
_transformationService.Apply(records, rules);
|
||||
|
||||
filePath = site.ManualImportFilePath;
|
||||
log.RowCount = records.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
var exportServer = BuildEffectiveServer(site, settings, sourceSystem);
|
||||
|
||||
Reference in New Issue
Block a user