Add manual Excel column mapping
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user