Add manual Excel column mapping

This commit is contained in:
2026-05-04 16:08:56 +02:00
parent 749a3209d9
commit c862a559f6
23 changed files with 1523 additions and 182 deletions
@@ -7,45 +7,61 @@ namespace TrafagSalesExporter.Services;
public class AppEventLogService : IAppEventLogService
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private readonly ILogger<AppEventLogService> _logger;
public AppEventLogService(IDbContextFactory<AppDbContext> dbFactory)
public AppEventLogService(IDbContextFactory<AppDbContext> dbFactory, ILogger<AppEventLogService> logger)
{
_dbFactory = dbFactory;
_logger = logger;
}
public async Task WriteAsync(string category, string message, string level = "Info", int? siteId = null, string? land = null, string? details = null)
{
using var db = await _dbFactory.CreateDbContextAsync();
db.AppEventLogs.Add(new AppEventLog
try
{
Timestamp = DateTime.Now,
Level = string.IsNullOrWhiteSpace(level) ? "Info" : level.Trim(),
Category = category?.Trim() ?? string.Empty,
SiteId = siteId,
Land = land?.Trim() ?? string.Empty,
Message = message?.Trim() ?? string.Empty,
Details = details?.Trim() ?? string.Empty
});
await db.SaveChangesAsync();
using var db = await _dbFactory.CreateDbContextAsync();
db.AppEventLogs.Add(new AppEventLog
{
Timestamp = DateTime.Now,
Level = string.IsNullOrWhiteSpace(level) ? "Info" : level.Trim(),
Category = category?.Trim() ?? string.Empty,
SiteId = siteId,
Land = land?.Trim() ?? string.Empty,
Message = message?.Trim() ?? string.Empty,
Details = details?.Trim() ?? string.Empty
});
await db.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "AppEventLog konnte nicht gespeichert werden: {Category} - {Message}", category, message);
}
}
public async Task WriteDebugAsync(string category, string message, int? siteId = null, string? land = null, string? details = null)
{
using var db = await _dbFactory.CreateDbContextAsync();
var settings = await db.ExportSettings.FirstOrDefaultAsync();
if (settings is null || !settings.DebugLoggingEnabled)
return;
db.AppEventLogs.Add(new AppEventLog
try
{
Timestamp = DateTime.Now,
Level = "Debug",
Category = category?.Trim() ?? string.Empty,
SiteId = siteId,
Land = land?.Trim() ?? string.Empty,
Message = message?.Trim() ?? string.Empty,
Details = details?.Trim() ?? string.Empty
});
await db.SaveChangesAsync();
using var db = await _dbFactory.CreateDbContextAsync();
var settings = await db.ExportSettings.FirstOrDefaultAsync();
if (settings is null || !settings.DebugLoggingEnabled)
return;
db.AppEventLogs.Add(new AppEventLog
{
Timestamp = DateTime.Now,
Level = "Debug",
Category = category?.Trim() ?? string.Empty,
SiteId = siteId,
Land = land?.Trim() ?? string.Empty,
Message = message?.Trim() ?? string.Empty,
Details = details?.Trim() ?? string.Empty
});
await db.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Debug-AppEventLog konnte nicht gespeichert werden: {Category} - {Message}", category, message);
}
}
}
@@ -64,6 +64,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
{
ExtractionDate = r.ExtractionDate,
Tsc = r.Tsc,
DocumentEntry = r.DocumentEntry,
InvoiceNumber = r.InvoiceNumber,
PositionOnInvoice = r.PositionOnInvoice,
Material = r.Material,
@@ -161,7 +162,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
command.Transaction = transaction;
command.CommandText = """
INSERT INTO CentralSalesRecords (
StoredAtUtc, SiteId, SourceSystem, ExtractionDate, Tsc, InvoiceNumber, PositionOnInvoice,
StoredAtUtc, SiteId, SourceSystem, ExtractionDate, Tsc, DocumentEntry, InvoiceNumber, PositionOnInvoice,
Material, Name, ProductGroup, Quantity, SupplierNumber, SupplierName, SupplierCountry,
CustomerNumber, CustomerName, CustomerCountry, CustomerIndustry, StandardCost,
StandardCostCurrency, PurchaseOrderNumber, SalesPriceValue, SalesCurrency, Incoterms2020,
@@ -169,7 +170,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
VatSumLocalCurrency, DocumentRate, CompanyCurrency, SalesResponsibleEmployee, InvoiceDate, OrderDate, Land, DocumentType
)
VALUES (
$storedAtUtc, $siteId, $sourceSystem, $extractionDate, $tsc, $invoiceNumber, $positionOnInvoice,
$storedAtUtc, $siteId, $sourceSystem, $extractionDate, $tsc, $documentEntry, $invoiceNumber, $positionOnInvoice,
$material, $name, $productGroup, $quantity, $supplierNumber, $supplierName, $supplierCountry,
$customerNumber, $customerName, $customerCountry, $customerIndustry, $standardCost,
$standardCostCurrency, $purchaseOrderNumber, $salesPriceValue, $salesCurrency, $incoterms2020,
@@ -183,6 +184,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
command.Parameters.Add("$sourceSystem", SqliteType.Text);
command.Parameters.Add("$extractionDate", SqliteType.Text);
command.Parameters.Add("$tsc", SqliteType.Text);
command.Parameters.Add("$documentEntry", SqliteType.Integer);
command.Parameters.Add("$invoiceNumber", SqliteType.Text);
command.Parameters.Add("$positionOnInvoice", SqliteType.Integer);
command.Parameters.Add("$material", SqliteType.Text);
@@ -225,6 +227,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
command.Parameters["$sourceSystem"].Value = sourceSystem;
command.Parameters["$extractionDate"].Value = record.ExtractionDate.ToString("O");
command.Parameters["$tsc"].Value = record.Tsc ?? string.Empty;
command.Parameters["$documentEntry"].Value = record.DocumentEntry;
command.Parameters["$invoiceNumber"].Value = record.InvoiceNumber ?? string.Empty;
command.Parameters["$positionOnInvoice"].Value = record.PositionOnInvoice;
command.Parameters["$material"].Value = record.Material ?? string.Empty;
@@ -32,6 +32,7 @@ public class ConfigTransferService : IConfigTransferService
var sapSources = await db.SapSourceDefinitions.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync();
var sapJoins = await db.SapJoinDefinitions.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync();
var sapMappings = await db.SapFieldMappings.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync();
var manualExcelMappings = await db.ManualExcelColumnMappings.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync();
var serverKeyMap = hanaServers.ToDictionary(x => x.Id, _ => Guid.NewGuid().ToString("N"));
var siteKeyMap = sites.ToDictionary(x => x.Id, _ => Guid.NewGuid().ToString("N"));
@@ -148,6 +149,15 @@ public class ConfigTransferService : IConfigTransferService
IsRequired = m.IsRequired,
IsActive = m.IsActive,
SortOrder = m.SortOrder
}).ToList(),
ManualExcelColumnMappings = manualExcelMappings.Select(m => new ConfigTransferManualExcelColumnMapping
{
SiteKey = siteKeyMap[m.SiteId],
TargetField = m.TargetField,
SourceHeader = m.SourceHeader,
IsRequired = m.IsRequired,
IsActive = m.IsActive,
SortOrder = m.SortOrder
}).ToList()
};
@@ -173,6 +183,7 @@ public class ConfigTransferService : IConfigTransferService
var existingSapSources = await db.SapSourceDefinitions.ToListAsync();
var existingSapJoins = await db.SapJoinDefinitions.ToListAsync();
var existingSapMappings = await db.SapFieldMappings.ToListAsync();
var existingManualExcelMappings = await db.ManualExcelColumnMappings.ToListAsync();
var preservedSharePointSecret = existingSharePoint?.ClientSecret ?? string.Empty;
var preservedSourceSystemSecrets = existingSourceSystems.ToDictionary(
@@ -187,6 +198,7 @@ public class ConfigTransferService : IConfigTransferService
x => BuildSiteSignature(x.Land, x.TSC, x.Schema, x.SourceSystem));
if (existingSapMappings.Count > 0) db.SapFieldMappings.RemoveRange(existingSapMappings);
if (existingManualExcelMappings.Count > 0) db.ManualExcelColumnMappings.RemoveRange(existingManualExcelMappings);
if (existingSapJoins.Count > 0) db.SapJoinDefinitions.RemoveRange(existingSapJoins);
if (existingSapSources.Count > 0) db.SapSourceDefinitions.RemoveRange(existingSapSources);
if (existingRules.Count > 0) db.FieldTransformationRules.RemoveRange(existingRules);
@@ -314,6 +326,7 @@ public class ConfigTransferService : IConfigTransferService
SourceSystem = record.SourceSystem,
ExtractionDate = record.ExtractionDate,
Tsc = record.Tsc,
DocumentEntry = record.DocumentEntry,
InvoiceNumber = record.InvoiceNumber,
PositionOnInvoice = record.PositionOnInvoice,
Material = record.Material,
@@ -414,6 +427,21 @@ public class ConfigTransferService : IConfigTransferService
}));
}
if (package.ManualExcelColumnMappings.Count > 0)
{
db.ManualExcelColumnMappings.AddRange(package.ManualExcelColumnMappings
.Where(x => siteIdMap.ContainsKey(x.SiteKey))
.Select(x => new ManualExcelColumnMapping
{
SiteId = siteIdMap[x.SiteKey],
TargetField = x.TargetField,
SourceHeader = x.SourceHeader,
IsRequired = x.IsRequired,
IsActive = x.IsActive,
SortOrder = x.SortOrder
}));
}
await db.SaveChangesAsync();
await transaction.CommitAsync();
}
@@ -85,6 +85,7 @@ CREATE TABLE CentralSalesRecords (
SourceSystem TEXT NOT NULL,
ExtractionDate TEXT NOT NULL,
Tsc TEXT NOT NULL,
DocumentEntry INTEGER NOT NULL DEFAULT 0,
InvoiceNumber TEXT NOT NULL,
PositionOnInvoice INTEGER NOT NULL,
Material TEXT NOT NULL,
@@ -156,4 +157,16 @@ CREATE TABLE SapFieldMappings (
SortOrder INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
);";
internal static string GetManualExcelColumnMappingsCreateSql() => @"
CREATE TABLE ManualExcelColumnMappings (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
SiteId INTEGER NOT NULL,
TargetField TEXT NOT NULL,
SourceHeader TEXT NOT NULL,
IsRequired INTEGER NOT NULL DEFAULT 0,
IsActive INTEGER NOT NULL DEFAULT 1,
SortOrder INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
);";
}
@@ -39,7 +39,9 @@ public class DatabaseSchemaMaintenanceService : IDatabaseSchemaMaintenanceServic
EnsureSapSourceTable(db);
EnsureSapJoinTable(db);
EnsureSapFieldMappingTable(db);
EnsureManualExcelColumnMappingTable(db);
EnsureCentralSalesRecordTable(db);
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentEntry", "INTEGER NOT NULL DEFAULT 0");
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentCurrency", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentTotalForeignCurrency", "TEXT NOT NULL DEFAULT '0'");
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentTotalLocalCurrency", "TEXT NOT NULL DEFAULT '0'");
@@ -191,16 +193,19 @@ FROM Sites_old;";
("CentralSalesRecords", DatabaseSchemaSql.GetCentralSalesRecordsCreateSql()),
("SapSourceDefinitions", DatabaseSchemaSql.GetSapSourceDefinitionsCreateSql()),
("SapJoinDefinitions", DatabaseSchemaSql.GetSapJoinDefinitionsCreateSql()),
("SapFieldMappings", DatabaseSchemaSql.GetSapFieldMappingsCreateSql())
("SapFieldMappings", DatabaseSchemaSql.GetSapFieldMappingsCreateSql()),
("ManualExcelColumnMappings", DatabaseSchemaSql.GetManualExcelColumnMappingsCreateSql())
};
foreach (var (tableName, createSql) in siteDependentTables)
{
if (DatabaseSchemaTools.TableReferences(conn, tableName, "Sites_old"))
if (DatabaseSchemaTools.TableReferences(conn, tableName, "Sites_old") ||
DatabaseSchemaTools.TableReferencesObsoleteTable(conn, tableName, "Sites"))
DatabaseSchemaTools.RebuildTable(conn, tableName, createSql);
}
if (DatabaseSchemaTools.TableReferences(conn, "Sites", "HanaServers_repair_old"))
if (DatabaseSchemaTools.TableReferences(conn, "Sites", "HanaServers_repair_old") ||
DatabaseSchemaTools.TableReferencesObsoleteTable(conn, "Sites", "HanaServers"))
DatabaseSchemaTools.RebuildTable(conn, "Sites", DatabaseSchemaSql.GetSitesCreateSql());
}
@@ -309,6 +314,17 @@ CREATE TABLE IF NOT EXISTS CurrencyExchangeRates (
cmd.ExecuteNonQuery();
}
private static void EnsureManualExcelColumnMappingTable(AppDbContext db)
{
var conn = db.Database.GetDbConnection();
if (conn.State != System.Data.ConnectionState.Open)
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = DatabaseSchemaSql.GetManualExcelColumnMappingsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
cmd.ExecuteNonQuery();
}
private static void EnsureCentralSalesRecordTable(AppDbContext db)
{
var conn = db.Database.GetDbConnection();
@@ -369,6 +385,25 @@ internal static class DatabaseSchemaTools
return sql.Contains(referencedTableName, StringComparison.OrdinalIgnoreCase);
}
internal static bool TableReferencesObsoleteTable(System.Data.Common.DbConnection connection, string tableName, string currentTableName)
{
using var command = connection.CreateCommand();
command.CommandText = "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = $tableName;";
var parameter = command.CreateParameter();
parameter.ParameterName = "$tableName";
parameter.Value = tableName;
command.Parameters.Add(parameter);
var sql = command.ExecuteScalar()?.ToString() ?? string.Empty;
var obsoletePrefix = $"{currentTableName}_";
return sql.Contains($"REFERENCES {obsoletePrefix}", StringComparison.OrdinalIgnoreCase) ||
sql.Contains($"REFERENCES \"{obsoletePrefix}", StringComparison.OrdinalIgnoreCase) ||
sql.Contains($"REFERENCES [{obsoletePrefix}", StringComparison.OrdinalIgnoreCase) ||
sql.Contains($"REFERENCES `{obsoletePrefix}", StringComparison.OrdinalIgnoreCase);
}
internal static void RebuildTable(System.Data.Common.DbConnection connection, string tableName, string createSql)
{
using var disableFk = connection.CreateCommand();
@@ -42,6 +42,7 @@ public class ExcelExportService : IExcelExportService
{
"extraction date",
"TSC",
"Document Entry",
"Invoice Number",
"Position on invoice",
"Material",
@@ -86,37 +87,38 @@ public class ExcelExportService : IExcelExportService
{
ws.Cell(row, 1).Value = record.ExtractionDate.ToString("dd.MM.yyyy HH:mm:ss");
ws.Cell(row, 2).Value = record.Tsc;
ws.Cell(row, 3).Value = record.InvoiceNumber;
ws.Cell(row, 4).Value = record.PositionOnInvoice;
ws.Cell(row, 5).Value = record.Material;
ws.Cell(row, 6).Value = record.Name;
ws.Cell(row, 7).Value = record.ProductGroup;
ws.Cell(row, 8).Value = record.Quantity;
ws.Cell(row, 9).Value = record.SupplierNumber;
ws.Cell(row, 10).Value = record.SupplierName;
ws.Cell(row, 11).Value = record.SupplierCountry;
ws.Cell(row, 12).Value = record.CustomerNumber;
ws.Cell(row, 13).Value = record.CustomerName;
ws.Cell(row, 14).Value = record.CustomerCountry;
ws.Cell(row, 15).Value = record.CustomerIndustry;
ws.Cell(row, 16).Value = record.StandardCost;
ws.Cell(row, 17).Value = record.StandardCostCurrency;
ws.Cell(row, 18).Value = record.PurchaseOrderNumber;
ws.Cell(row, 19).Value = record.SalesPriceValue;
ws.Cell(row, 20).Value = record.SalesCurrency;
ws.Cell(row, 21).Value = record.DocumentCurrency;
ws.Cell(row, 22).Value = record.DocumentTotalForeignCurrency;
ws.Cell(row, 23).Value = record.DocumentTotalLocalCurrency;
ws.Cell(row, 24).Value = record.VatSumForeignCurrency;
ws.Cell(row, 25).Value = record.VatSumLocalCurrency;
ws.Cell(row, 26).Value = record.DocumentRate;
ws.Cell(row, 27).Value = record.CompanyCurrency;
ws.Cell(row, 28).Value = record.Incoterms2020;
ws.Cell(row, 29).Value = record.SalesResponsibleEmployee;
ws.Cell(row, 30).Value = record.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty;
ws.Cell(row, 31).Value = record.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty;
ws.Cell(row, 32).Value = record.Land;
ws.Cell(row, 33).Value = record.DocumentType;
ws.Cell(row, 3).Value = record.DocumentEntry;
ws.Cell(row, 4).Value = record.InvoiceNumber;
ws.Cell(row, 5).Value = record.PositionOnInvoice;
ws.Cell(row, 6).Value = record.Material;
ws.Cell(row, 7).Value = record.Name;
ws.Cell(row, 8).Value = record.ProductGroup;
ws.Cell(row, 9).Value = record.Quantity;
ws.Cell(row, 10).Value = record.SupplierNumber;
ws.Cell(row, 11).Value = record.SupplierName;
ws.Cell(row, 12).Value = record.SupplierCountry;
ws.Cell(row, 13).Value = record.CustomerNumber;
ws.Cell(row, 14).Value = record.CustomerName;
ws.Cell(row, 15).Value = record.CustomerCountry;
ws.Cell(row, 16).Value = record.CustomerIndustry;
ws.Cell(row, 17).Value = record.StandardCost;
ws.Cell(row, 18).Value = record.StandardCostCurrency;
ws.Cell(row, 19).Value = record.PurchaseOrderNumber;
ws.Cell(row, 20).Value = record.SalesPriceValue;
ws.Cell(row, 21).Value = record.SalesCurrency;
ws.Cell(row, 22).Value = record.DocumentCurrency;
ws.Cell(row, 23).Value = record.DocumentTotalForeignCurrency;
ws.Cell(row, 24).Value = record.DocumentTotalLocalCurrency;
ws.Cell(row, 25).Value = record.VatSumForeignCurrency;
ws.Cell(row, 26).Value = record.VatSumLocalCurrency;
ws.Cell(row, 27).Value = record.DocumentRate;
ws.Cell(row, 28).Value = record.CompanyCurrency;
ws.Cell(row, 29).Value = record.Incoterms2020;
ws.Cell(row, 30).Value = record.SalesResponsibleEmployee;
ws.Cell(row, 31).Value = record.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty;
ws.Cell(row, 32).Value = record.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty;
ws.Cell(row, 33).Value = record.Land;
ws.Cell(row, 34).Value = record.DocumentType;
row++;
}
@@ -7,16 +7,25 @@ namespace TrafagSalesExporter.Services;
public class ExportLogService : IExportLogService
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private readonly ILogger<ExportLogService> _logger;
public ExportLogService(IDbContextFactory<AppDbContext> dbFactory)
public ExportLogService(IDbContextFactory<AppDbContext> dbFactory, ILogger<ExportLogService> logger)
{
_dbFactory = dbFactory;
_logger = logger;
}
public async Task WriteAsync(ExportLog log)
{
using var db = await _dbFactory.CreateDbContextAsync();
db.ExportLogs.Add(log);
await db.SaveChangesAsync();
try
{
using var db = await _dbFactory.CreateDbContextAsync();
db.ExportLogs.Add(log);
await db.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "ExportLog konnte nicht gespeichert werden: {Land} ({TSC})", log.Land, log.TSC);
}
}
}
@@ -10,6 +10,7 @@ public class ExportOrchestrationService
private readonly ISiteExportService _siteExportService;
private readonly IConsolidatedExportService _consolidatedExportService;
private readonly IExportLogService _exportLogService;
private readonly IAppEventLogService _appEventLogService;
public event Action? OnExportStatusChanged;
@@ -22,12 +23,14 @@ public class ExportOrchestrationService
IDbContextFactory<AppDbContext> dbFactory,
ISiteExportService siteExportService,
IConsolidatedExportService consolidatedExportService,
IExportLogService exportLogService)
IExportLogService exportLogService,
IAppEventLogService appEventLogService)
{
_dbFactory = dbFactory;
_siteExportService = siteExportService;
_consolidatedExportService = consolidatedExportService;
_exportLogService = exportLogService;
_appEventLogService = appEventLogService;
}
public bool IsExporting(int siteId)
@@ -152,6 +155,11 @@ public class ExportOrchestrationService
{
return await _consolidatedExportService.ExportAsync(records ?? []);
}
catch (Exception ex)
{
await _appEventLogService.WriteAsync("Export", "Zentrale Datei fehlgeschlagen", "Error", details: ex.ToString());
return null;
}
finally
{
lock (_lock)
@@ -158,6 +158,7 @@ public class HanaQueryService : IHanaQueryService
{
ExtractionDate = reader.GetDateTime(reader.GetOrdinal("extraction_date")),
Tsc = reader.GetString(reader.GetOrdinal("tsc")),
DocumentEntry = Convert.ToInt32(reader["document_entry"]),
InvoiceNumber = reader["invoice_number"]?.ToString() ?? string.Empty,
PositionOnInvoice = Convert.ToInt32(reader["invoice_position"]),
InvoiceDate = reader.IsDBNull(reader.GetOrdinal("invoice_date")) ? null : reader.GetDateTime(reader.GetOrdinal("invoice_date")),
@@ -204,11 +205,12 @@ public class HanaQueryService : IHanaQueryService
private static string GetInvoiceQuery(string schema)
{
var quotedSchema = QuoteIdentifier(schema);
var schemaPrefix = BuildSchemaPrefix(schema);
return $@"
SELECT
CURRENT_TIMESTAMP AS extraction_date,
:{TscParameterName} AS tsc,
h.""DocEntry"" AS document_entry,
h.""DocNum"" AS invoice_number,
p.""LineNum"" AS invoice_position,
h.""DocDate"" AS invoice_date,
@@ -240,35 +242,36 @@ SELECT
'' AS incoterms_2020,
COALESCE(emp.""SlpName"", '') AS sales_responsible,
CASE WHEN p.""BaseType"" = 17
THEN (SELECT o.""DocDate"" FROM {quotedSchema}.""ORDR"" o
THEN (SELECT o.""DocDate"" FROM {schemaPrefix}""ORDR"" o
WHERE o.""DocEntry"" = p.""BaseEntry"")
ELSE NULL END AS order_date,
'INV' AS doc_type
FROM {quotedSchema}.""OINV"" h
INNER JOIN {quotedSchema}.""INV1"" p ON h.""DocEntry"" = p.""DocEntry""
CROSS JOIN {quotedSchema}.""OADM"" adm
LEFT JOIN {quotedSchema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode""
LEFT JOIN {quotedSchema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod""
LEFT JOIN {quotedSchema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode""
LEFT JOIN {quotedSchema}.""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode""
FROM {schemaPrefix}""OINV"" h
INNER JOIN {schemaPrefix}""INV1"" p ON h.""DocEntry"" = p.""DocEntry""
CROSS JOIN {schemaPrefix}""OADM"" adm
LEFT JOIN {schemaPrefix}""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode""
LEFT JOIN {schemaPrefix}""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod""
LEFT JOIN {schemaPrefix}""OCRD"" cust ON h.""CardCode"" = cust.""CardCode""
LEFT JOIN {schemaPrefix}""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode""
AND cust_adr.""AdresType"" = 'B' AND cust_adr.""Address"" = h.""PayToCode""
LEFT JOIN {quotedSchema}.""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode""
LEFT JOIN {quotedSchema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode""
LEFT JOIN {schemaPrefix}""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode""
LEFT JOIN {schemaPrefix}""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode""
AND sup.""CardType"" = 'S'
LEFT JOIN {quotedSchema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode""
LEFT JOIN {schemaPrefix}""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode""
AND sup_adr.""AdresType"" = 'B'
LEFT JOIN {quotedSchema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
LEFT JOIN {schemaPrefix}""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= :{DateFilterParameterName}
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
}
private static string GetCreditNoteQuery(string schema)
{
var quotedSchema = QuoteIdentifier(schema);
var schemaPrefix = BuildSchemaPrefix(schema);
return $@"
SELECT
CURRENT_TIMESTAMP AS extraction_date,
:{TscParameterName} AS tsc,
h.""DocEntry"" AS document_entry,
h.""DocNum"" AS invoice_number,
p.""LineNum"" AS invoice_position,
h.""DocDate"" AS invoice_date,
@@ -299,20 +302,20 @@ SELECT
COALESCE(emp.""SlpName"", '') AS sales_responsible,
NULL AS order_date,
'CRN' AS doc_type
FROM {quotedSchema}.""ORIN"" h
INNER JOIN {quotedSchema}.""RIN1"" p ON h.""DocEntry"" = p.""DocEntry""
CROSS JOIN {quotedSchema}.""OADM"" adm
LEFT JOIN {quotedSchema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode""
LEFT JOIN {quotedSchema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod""
LEFT JOIN {quotedSchema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode""
LEFT JOIN {quotedSchema}.""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode""
FROM {schemaPrefix}""ORIN"" h
INNER JOIN {schemaPrefix}""RIN1"" p ON h.""DocEntry"" = p.""DocEntry""
CROSS JOIN {schemaPrefix}""OADM"" adm
LEFT JOIN {schemaPrefix}""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode""
LEFT JOIN {schemaPrefix}""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod""
LEFT JOIN {schemaPrefix}""OCRD"" cust ON h.""CardCode"" = cust.""CardCode""
LEFT JOIN {schemaPrefix}""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode""
AND cust_adr.""AdresType"" = 'B' AND cust_adr.""Address"" = h.""PayToCode""
LEFT JOIN {quotedSchema}.""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode""
LEFT JOIN {quotedSchema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode""
LEFT JOIN {schemaPrefix}""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode""
LEFT JOIN {schemaPrefix}""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode""
AND sup.""CardType"" = 'S'
LEFT JOIN {quotedSchema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode""
LEFT JOIN {schemaPrefix}""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode""
AND sup_adr.""AdresType"" = 'B'
LEFT JOIN {quotedSchema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
LEFT JOIN {schemaPrefix}""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= :{DateFilterParameterName}
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
}
@@ -328,7 +331,7 @@ ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
private static string BuildQueryLogDetails(string query, string schema, string tsc, DateTime dateFilter)
=> $"{query}{Environment.NewLine}-- schema={schema}; tsc={tsc}; dateFilter={dateFilter:yyyy-MM-dd}";
private static string QuoteIdentifier(string identifier)
private static string BuildSchemaPrefix(string identifier)
{
var value = identifier?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(value))
@@ -340,7 +343,7 @@ ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
throw new InvalidOperationException($"Ungueltiger HANA-Identifier: '{identifier}'.");
}
return $@"""{value}""";
return $"{value}.";
}
}
@@ -1,15 +1,23 @@
using System.Globalization;
using System.Reflection;
using ClosedXML.Excel;
using Microsoft.EntityFrameworkCore;
using TrafagSalesExporter.Data;
using TrafagSalesExporter.Models;
namespace TrafagSalesExporter.Services;
public class ManualExcelImportService : IManualExcelImportService
{
private static readonly Dictionary<string, PropertyInfo> SalesRecordProperties = typeof(SalesRecord)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, string> HeaderMap = new(StringComparer.OrdinalIgnoreCase)
{
["extractiondate"] = nameof(SalesRecord.ExtractionDate),
["tsc"] = nameof(SalesRecord.Tsc),
["documententry"] = nameof(SalesRecord.DocumentEntry),
["invoicenumber"] = nameof(SalesRecord.InvoiceNumber),
["positiononinvoice"] = nameof(SalesRecord.PositionOnInvoice),
["material"] = nameof(SalesRecord.Material),
@@ -47,15 +55,62 @@ public class ManualExcelImportService : IManualExcelImportService
["documenttype"] = nameof(SalesRecord.DocumentType)
};
public Task<List<SalesRecord>> ReadSalesRecordsAsync(string filePath, Site site)
private readonly IDbContextFactory<AppDbContext>? _dbFactory;
public ManualExcelImportService()
{
}
public ManualExcelImportService(IDbContextFactory<AppDbContext> dbFactory)
{
_dbFactory = dbFactory;
}
public async Task<List<SalesRecord>> ReadSalesRecordsAsync(string filePath, Site site)
{
var mappings = await LoadMappingsAsync(site.Id);
return ReadSalesRecords(filePath, site, mappings);
}
public Task<List<SalesRecord>> ReadSalesRecordsAsync(string filePath, Site site, IReadOnlyList<ManualExcelColumnMapping> mappings)
=> Task.FromResult(ReadSalesRecords(filePath, site, mappings));
private async Task<List<ManualExcelColumnMapping>> LoadMappingsAsync(int siteId)
{
if (_dbFactory is null || siteId <= 0)
return [];
await using var db = await _dbFactory.CreateDbContextAsync();
return await db.ManualExcelColumnMappings
.AsNoTracking()
.Where(m => m.SiteId == siteId && m.IsActive)
.OrderBy(m => m.SortOrder)
.ThenBy(m => m.Id)
.ToListAsync();
}
private static List<SalesRecord> ReadSalesRecords(string filePath, Site site, IReadOnlyList<ManualExcelColumnMapping> mappings)
{
using var workbook = new XLWorkbook(filePath);
var worksheet = workbook.Worksheets.FirstOrDefault()
?? throw new InvalidOperationException("Die Excel-Datei enthält kein Arbeitsblatt.");
?? throw new InvalidOperationException("Die Excel-Datei enthaelt kein Arbeitsblatt.");
var usedRange = worksheet.RangeUsed()
?? throw new InvalidOperationException("Die Excel-Datei enthält keine Daten.");
?? throw new InvalidOperationException("Die Excel-Datei enthaelt keine Daten.");
var headerRow = usedRange.FirstRow();
var activeMappings = mappings
.Where(m => m.IsActive && !string.IsNullOrWhiteSpace(m.TargetField) && !string.IsNullOrWhiteSpace(m.SourceHeader))
.OrderBy(m => m.SortOrder)
.ThenBy(m => m.Id)
.ToList();
return activeMappings.Count > 0
? ReadMappedRows(usedRange, headerRow, site, activeMappings)
: ReadDefaultRows(usedRange, headerRow, site);
}
private static List<SalesRecord> ReadDefaultRows(IXLRange usedRange, IXLRangeRow headerRow, Site site)
{
var headerIndexes = BuildHeaderIndexMap(headerRow);
var rows = new List<SalesRecord>();
@@ -68,6 +123,7 @@ public class ManualExcelImportService : IManualExcelImportService
{
ExtractionDate = ReadDate(headerIndexes, row, nameof(SalesRecord.ExtractionDate)) ?? DateTime.UtcNow,
Tsc = ReadString(headerIndexes, row, nameof(SalesRecord.Tsc), site.TSC),
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))),
Material = ReadString(headerIndexes, row, nameof(SalesRecord.Material)),
@@ -102,7 +158,64 @@ public class ManualExcelImportService : IManualExcelImportService
});
}
return Task.FromResult(rows);
return rows;
}
private static List<SalesRecord> ReadMappedRows(
IXLRange usedRange,
IXLRangeRow headerRow,
Site site,
IReadOnlyList<ManualExcelColumnMapping> mappings)
{
var headerIndexes = BuildRawHeaderIndexMap(headerRow);
foreach (var mapping in mappings.Where(m => m.IsRequired))
{
if (mapping.SourceHeader.Trim().StartsWith('='))
continue;
if (!TryResolveHeaderIndex(headerIndexes, mapping.SourceHeader, out _))
throw new InvalidOperationException($"Pflichtspalte '{mapping.SourceHeader}' fuer Zielfeld '{mapping.TargetField}' fehlt.");
}
var rows = new List<SalesRecord>();
foreach (var row in usedRange.RowsUsed().Skip(1))
{
if (IsRowEmpty(row))
continue;
var record = new SalesRecord
{
ExtractionDate = DateTime.UtcNow,
Tsc = site.TSC,
Land = site.Land,
DocumentType = "Manual Excel"
};
foreach (var mapping in mappings)
{
if (!SalesRecordProperties.TryGetValue(mapping.TargetField, out var property))
continue;
var value = ReadMappedValue(headerIndexes, row, mapping.SourceHeader);
SetPropertyValue(record, property, value);
}
if (record.ExtractionDate == default)
record.ExtractionDate = DateTime.UtcNow;
if (string.IsNullOrWhiteSpace(record.Tsc))
record.Tsc = site.TSC;
if (string.IsNullOrWhiteSpace(record.Land))
record.Land = site.Land;
if (string.IsNullOrWhiteSpace(record.DocumentType))
record.DocumentType = "Manual Excel";
if (!IsMeaningfulMappedRecord(record))
continue;
rows.Add(record);
}
return rows;
}
private static Dictionary<string, int> BuildHeaderIndexMap(IXLRangeRow headerRow)
@@ -125,6 +238,41 @@ public class ManualExcelImportService : IManualExcelImportService
return result;
}
private static Dictionary<string, int> BuildRawHeaderIndexMap(IXLRangeRow headerRow)
{
var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var cell in headerRow.CellsUsed())
{
var header = cell.GetString().Trim();
if (string.IsNullOrWhiteSpace(header))
continue;
result[header] = cell.Address.ColumnNumber;
result[NormalizeHeader(header)] = cell.Address.ColumnNumber;
}
return result;
}
private static bool TryResolveHeaderIndex(Dictionary<string, int> headerIndexes, string sourceHeader, out int index)
{
var trimmed = sourceHeader.Trim();
return headerIndexes.TryGetValue(trimmed, out index) ||
headerIndexes.TryGetValue(NormalizeHeader(trimmed), out index);
}
private static object? ReadMappedValue(Dictionary<string, int> headerIndexes, IXLRangeRow row, string sourceHeader)
{
var trimmed = sourceHeader.Trim();
if (trimmed.StartsWith('='))
return trimmed[1..];
return TryResolveHeaderIndex(headerIndexes, trimmed, out var index)
? row.Cell(index).GetFormattedString().Trim()
: null;
}
private static bool IsRowEmpty(IXLRangeRow row)
=> row.CellsUsed().All(cell => string.IsNullOrWhiteSpace(cell.GetFormattedString()));
@@ -148,18 +296,7 @@ public class ManualExcelImportService : IManualExcelImportService
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.GetCultureInfo("de-CH"), out decimalValue))
return decimalValue;
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("de-DE"), out decimalValue))
return decimalValue;
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out decimalValue))
return decimalValue;
return 0m;
return ParseDecimal(cell.GetFormattedString().Trim());
}
private static DateTime? ReadDate(Dictionary<string, int> headerIndexes, IXLRangeRow row, string fieldName)
@@ -171,7 +308,65 @@ public class ManualExcelImportService : IManualExcelImportService
if (cell.TryGetValue<DateTime>(out var dateValue))
return dateValue;
var text = cell.GetFormattedString().Trim();
return ParseDate(cell.GetFormattedString().Trim());
}
private static void SetPropertyValue(SalesRecord record, PropertyInfo property, object? value)
{
try
{
var text = value?.ToString()?.Trim() ?? string.Empty;
if (property.PropertyType == typeof(string))
{
property.SetValue(record, text);
return;
}
if (property.PropertyType == typeof(int))
{
property.SetValue(record, (int)Math.Round(ParseDecimal(text)));
return;
}
if (property.PropertyType == typeof(decimal))
{
property.SetValue(record, ParseDecimal(text));
return;
}
if (property.PropertyType == typeof(DateTime?))
{
property.SetValue(record, ParseDate(text));
return;
}
if (property.PropertyType == typeof(DateTime))
property.SetValue(record, ParseDate(text) ?? default);
}
catch
{
// Einzelne fehlerhafte Zellen duerfen den kompletten manuellen Import nicht abbrechen.
}
}
private static decimal ParseDecimal(string text)
{
if (string.IsNullOrWhiteSpace(text))
return 0m;
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("de-CH"), out var decimalValue))
return decimalValue;
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("de-DE"), out decimalValue))
return decimalValue;
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out decimalValue))
return decimalValue;
return 0m;
}
private static DateTime? ParseDate(string text)
{
if (string.IsNullOrWhiteSpace(text))
return null;
@@ -184,7 +379,7 @@ public class ManualExcelImportService : IManualExcelImportService
"O"
};
if (DateTime.TryParseExact(text, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out dateValue))
if (DateTime.TryParseExact(text, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var dateValue))
return dateValue;
if (DateTime.TryParse(text, CultureInfo.GetCultureInfo("de-CH"), DateTimeStyles.AssumeLocal, out dateValue))
return dateValue;
@@ -194,6 +389,12 @@ public class ManualExcelImportService : IManualExcelImportService
return null;
}
private static bool IsMeaningfulMappedRecord(SalesRecord record)
=> record.PositionOnInvoice != 0 ||
record.Quantity != 0m ||
record.SalesPriceValue != 0m ||
!string.IsNullOrWhiteSpace(record.Material);
private static string NormalizeHeader(string value)
{
var chars = value
@@ -1,4 +1,5 @@
using System.Text.Json;
using ClosedXML.Excel;
using Microsoft.EntityFrameworkCore;
using TrafagSalesExporter.Data;
using TrafagSalesExporter.Models;
@@ -12,11 +13,12 @@ public interface IStandortePageService
Task DeleteServerAsync(HanaServer server);
Task<ConnectionTestResult> TestServerConnectionAsync(HanaServer server);
Task<StandortEditorState> LoadSiteEditorAsync(Site site, IEnumerable<SourceSystemDefinition> sourceSystems);
Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<string> sapEntitySetsCache);
Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, bool isManualExcelSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<ManualExcelColumnMapping> manualExcelMappings, List<string> sapEntitySetsCache);
Task DeleteSiteAsync(Site site);
Task<List<string>> LoadAvailableSchemasAsync(Site site);
Task<SapEntitySetRefreshResult> RefreshSapEntitySetsAsync(Site site);
Task<SapSourceFieldRefreshResult> RefreshSapSourceFieldsAsync(Site site, List<SapSourceDefinition> sapSources, List<SapFieldMapping> sapMappings);
Task<List<string>> LoadManualExcelHeadersAsync(string manualImportFilePath);
Task<DateTime> ValidateManualImportPathAsync(string manualImportFilePath);
}
@@ -163,6 +165,7 @@ public sealed class StandortePageService : IStandortePageService
var sapSources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).OrderBy(s => s.SortOrder).ThenBy(s => s.Id).ToListAsync();
var sapJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).OrderBy(j => j.SortOrder).ThenBy(j => j.Id).ToListAsync();
var sapMappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToListAsync();
var manualExcelMappings = await db.ManualExcelColumnMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToListAsync();
return new StandortEditorState
{
@@ -188,11 +191,12 @@ public sealed class StandortePageService : IStandortePageService
SapEntitySets = ParseSapEntitySets(site.SapEntitySetsCache),
SapSources = sapSources,
SapJoins = sapJoins,
SapMappings = sapMappings
SapMappings = sapMappings,
ManualExcelMappings = manualExcelMappings
};
}
public async Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<string> sapEntitySetsCache)
public async Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, bool isManualExcelSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<ManualExcelColumnMapping> manualExcelMappings, List<string> sapEntitySetsCache)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var serverId = usesHanaConnection ? await ResolveCentralHanaServerIdAsync(db, site) : (int?)null;
@@ -212,6 +216,7 @@ public sealed class StandortePageService : IStandortePageService
await db.SaveChangesAsync();
await SaveSapConfigurationAsync(db, site.Id, isSapSite, sapSources, sapJoins, sapMappings);
await SaveManualExcelConfigurationAsync(db, site.Id, isManualExcelSite, manualExcelMappings);
}
public async Task DeleteSiteAsync(Site site)
@@ -224,10 +229,12 @@ public sealed class StandortePageService : IStandortePageService
var sources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync();
var joins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).ToListAsync();
var mappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).ToListAsync();
var manualMappings = await db.ManualExcelColumnMappings.Where(m => m.SiteId == site.Id).ToListAsync();
var centralRows = await db.CentralSalesRecords.Where(r => r.SiteId == site.Id).ToListAsync();
if (sources.Count > 0) db.SapSourceDefinitions.RemoveRange(sources);
if (joins.Count > 0) db.SapJoinDefinitions.RemoveRange(joins);
if (mappings.Count > 0) db.SapFieldMappings.RemoveRange(mappings);
if (manualMappings.Count > 0) db.ManualExcelColumnMappings.RemoveRange(manualMappings);
if (centralRows.Count > 0) db.CentralSalesRecords.RemoveRange(centralRows);
db.Sites.Remove(entity);
await db.SaveChangesAsync();
@@ -381,6 +388,59 @@ public sealed class StandortePageService : IStandortePageService
}
}
public async Task<List<string>> LoadManualExcelHeadersAsync(string manualImportFilePath)
{
var filePath = await ResolveManualImportFilePathAsync(manualImportFilePath);
var deleteAfterRead = !string.Equals(filePath, manualImportFilePath?.Trim(), StringComparison.OrdinalIgnoreCase);
try
{
using var workbook = new XLWorkbook(filePath);
var worksheet = workbook.Worksheets.FirstOrDefault()
?? throw new InvalidOperationException("Die Excel-Datei enthaelt kein Arbeitsblatt.");
var usedRange = worksheet.RangeUsed()
?? throw new InvalidOperationException("Die Excel-Datei enthaelt keine Daten.");
return usedRange.FirstRow().CellsUsed()
.Select(cell => cell.GetString().Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
.ToList();
}
finally
{
if (deleteAfterRead && File.Exists(filePath))
File.Delete(filePath);
}
}
private async Task<string> ResolveManualImportFilePathAsync(string manualImportFilePath)
{
var trimmedPath = manualImportFilePath.Trim();
if (string.IsNullOrWhiteSpace(trimmedPath))
throw new InvalidOperationException("Bitte zuerst einen Dateipfad eintragen.");
if (File.Exists(trimmedPath))
return trimmedPath;
if (!LooksLikeSharePointReference(trimmedPath))
throw new InvalidOperationException($"Datei nicht gefunden oder nicht erreichbar: {trimmedPath}");
await using var db = await _dbFactory.CreateDbContextAsync();
var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync();
if (spConfig is null ||
string.IsNullOrWhiteSpace(spConfig.TenantId) ||
string.IsNullOrWhiteSpace(spConfig.ClientId) ||
string.IsNullOrWhiteSpace(spConfig.ClientSecret) ||
string.IsNullOrWhiteSpace(spConfig.SiteUrl))
{
throw new InvalidOperationException("Fuer SharePoint-Pruefung fehlt eine vollstaendige SharePoint-Konfiguration in Settings.");
}
return await _sharePointService.DownloadToTempFileAsync(
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath);
}
private static void ApplyServer(HanaServer target, HanaServer source)
{
target.SourceSystem = source.SourceSystem;
@@ -452,6 +512,12 @@ public sealed class StandortePageService : IStandortePageService
sapSources[0].IsPrimary = true;
}
private static void NormalizeManualExcelMappings(List<ManualExcelColumnMapping> manualExcelMappings)
{
for (var i = 0; i < manualExcelMappings.Count; i++)
manualExcelMappings[i].SortOrder = i;
}
private static async Task SaveSapConfigurationAsync(AppDbContext db, int siteId, bool isSapSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings)
{
var oldSources = await db.SapSourceDefinitions.Where(s => s.SiteId == siteId).ToListAsync();
@@ -475,6 +541,22 @@ public sealed class StandortePageService : IStandortePageService
await db.SaveChangesAsync();
}
private static async Task SaveManualExcelConfigurationAsync(AppDbContext db, int siteId, bool isManualExcelSite, List<ManualExcelColumnMapping> manualExcelMappings)
{
var oldMappings = await db.ManualExcelColumnMappings.Where(m => m.SiteId == siteId).ToListAsync();
if (oldMappings.Count > 0) db.ManualExcelColumnMappings.RemoveRange(oldMappings);
if (isManualExcelSite)
{
NormalizeManualExcelMappings(manualExcelMappings);
foreach (var mapping in manualExcelMappings)
mapping.SiteId = siteId;
db.ManualExcelColumnMappings.AddRange(manualExcelMappings);
}
await db.SaveChangesAsync();
}
private static async Task<int> ResolveCentralHanaServerIdAsync(AppDbContext db, Site site)
{
site.UsernameOverride = site.UsernameOverride.Trim();
@@ -507,6 +589,7 @@ public sealed class StandortEditorState
public List<SapSourceDefinition> SapSources { get; set; } = [];
public List<SapJoinDefinition> SapJoins { get; set; } = [];
public List<SapFieldMapping> SapMappings { get; set; } = [];
public List<ManualExcelColumnMapping> ManualExcelMappings { get; set; } = [];
}
public sealed class SapEntitySetRefreshResult