import exxport settings, join over sap hana tables
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class CentralSalesRecordService : ICentralSalesRecordService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
|
||||
public CentralSalesRecordService(IDbContextFactory<AppDbContext> dbFactory)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public async Task ReplaceForSiteAsync(Site site, IEnumerable<SalesRecord> records)
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var existing = await db.CentralSalesRecords.Where(r => r.SiteId == site.Id).ToListAsync();
|
||||
if (existing.Count > 0)
|
||||
db.CentralSalesRecords.RemoveRange(existing);
|
||||
|
||||
var sourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem;
|
||||
db.CentralSalesRecords.AddRange(records.Select(record => new CentralSalesRecord
|
||||
{
|
||||
StoredAtUtc = DateTime.UtcNow,
|
||||
SiteId = site.Id,
|
||||
SourceSystem = sourceSystem,
|
||||
ExtractionDate = record.ExtractionDate,
|
||||
Tsc = record.Tsc,
|
||||
InvoiceNumber = record.InvoiceNumber,
|
||||
PositionOnInvoice = record.PositionOnInvoice,
|
||||
Material = record.Material,
|
||||
Name = record.Name,
|
||||
ProductGroup = record.ProductGroup,
|
||||
Quantity = record.Quantity,
|
||||
SupplierNumber = record.SupplierNumber,
|
||||
SupplierName = record.SupplierName,
|
||||
SupplierCountry = record.SupplierCountry,
|
||||
CustomerNumber = record.CustomerNumber,
|
||||
CustomerName = record.CustomerName,
|
||||
CustomerCountry = record.CustomerCountry,
|
||||
CustomerIndustry = record.CustomerIndustry,
|
||||
StandardCost = record.StandardCost,
|
||||
StandardCostCurrency = record.StandardCostCurrency,
|
||||
PurchaseOrderNumber = record.PurchaseOrderNumber,
|
||||
SalesPriceValue = record.SalesPriceValue,
|
||||
SalesCurrency = record.SalesCurrency,
|
||||
Incoterms2020 = record.Incoterms2020,
|
||||
SalesResponsibleEmployee = record.SalesResponsibleEmployee,
|
||||
InvoiceDate = record.InvoiceDate,
|
||||
OrderDate = record.OrderDate,
|
||||
Land = record.Land,
|
||||
DocumentType = record.DocumentType
|
||||
}));
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<SalesRecord>> GetAllAsync()
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
return await db.CentralSalesRecords
|
||||
.OrderBy(r => r.Land)
|
||||
.ThenBy(r => r.Tsc)
|
||||
.Select(r => new SalesRecord
|
||||
{
|
||||
ExtractionDate = r.ExtractionDate,
|
||||
Tsc = r.Tsc,
|
||||
InvoiceNumber = r.InvoiceNumber,
|
||||
PositionOnInvoice = r.PositionOnInvoice,
|
||||
Material = r.Material,
|
||||
Name = r.Name,
|
||||
ProductGroup = r.ProductGroup,
|
||||
Quantity = r.Quantity,
|
||||
SupplierNumber = r.SupplierNumber,
|
||||
SupplierName = r.SupplierName,
|
||||
SupplierCountry = r.SupplierCountry,
|
||||
CustomerNumber = r.CustomerNumber,
|
||||
CustomerName = r.CustomerName,
|
||||
CustomerCountry = r.CustomerCountry,
|
||||
CustomerIndustry = r.CustomerIndustry,
|
||||
StandardCost = r.StandardCost,
|
||||
StandardCostCurrency = r.StandardCostCurrency,
|
||||
PurchaseOrderNumber = r.PurchaseOrderNumber,
|
||||
SalesPriceValue = r.SalesPriceValue,
|
||||
SalesCurrency = r.SalesCurrency,
|
||||
Incoterms2020 = r.Incoterms2020,
|
||||
SalesResponsibleEmployee = r.SalesResponsibleEmployee,
|
||||
InvoiceDate = r.InvoiceDate,
|
||||
OrderDate = r.OrderDate,
|
||||
Land = r.Land,
|
||||
DocumentType = r.DocumentType
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class ConfigTransferService : IConfigTransferService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
public ConfigTransferService(IDbContextFactory<AppDbContext> dbFactory)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public async Task<string> ExportJsonAsync(bool includeSecrets)
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var sharePoint = await db.SharePointConfigs.FirstOrDefaultAsync();
|
||||
var exportSettings = await db.ExportSettings.FirstOrDefaultAsync();
|
||||
var hanaServers = await db.HanaServers.OrderBy(x => x.Name).ToListAsync();
|
||||
var sites = await db.Sites.OrderBy(x => x.Land).ToListAsync();
|
||||
var rules = await db.FieldTransformationRules.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync();
|
||||
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 serverKeyMap = hanaServers.ToDictionary(x => x.Id, _ => Guid.NewGuid().ToString("N"));
|
||||
var siteKeyMap = sites.ToDictionary(x => x.Id, _ => Guid.NewGuid().ToString("N"));
|
||||
|
||||
var package = new ConfigTransferPackage
|
||||
{
|
||||
IncludesSecrets = includeSecrets,
|
||||
SharePointConfig = sharePoint is null ? null : new ConfigTransferSharePoint
|
||||
{
|
||||
SiteUrl = sharePoint.SiteUrl,
|
||||
ExportFolder = sharePoint.ExportFolder,
|
||||
TenantId = sharePoint.TenantId,
|
||||
ClientId = sharePoint.ClientId,
|
||||
ClientSecret = includeSecrets ? sharePoint.ClientSecret : null
|
||||
},
|
||||
ExportSettings = exportSettings is null ? null : new ConfigTransferExportSettings
|
||||
{
|
||||
DateFilter = exportSettings.DateFilter,
|
||||
TimerHour = exportSettings.TimerHour,
|
||||
TimerMinute = exportSettings.TimerMinute,
|
||||
TimerEnabled = exportSettings.TimerEnabled,
|
||||
SapUsername = includeSecrets ? exportSettings.SapUsername : null,
|
||||
SapPassword = includeSecrets ? exportSettings.SapPassword : null,
|
||||
Bi1Username = includeSecrets ? exportSettings.Bi1Username : null,
|
||||
Bi1Password = includeSecrets ? exportSettings.Bi1Password : null,
|
||||
SageUsername = includeSecrets ? exportSettings.SageUsername : null,
|
||||
SagePassword = includeSecrets ? exportSettings.SagePassword : null
|
||||
},
|
||||
HanaServers = hanaServers.Select(server => new ConfigTransferHanaServer
|
||||
{
|
||||
Key = serverKeyMap[server.Id],
|
||||
Name = server.Name,
|
||||
Host = server.Host,
|
||||
Port = server.Port,
|
||||
Username = includeSecrets ? server.Username : null,
|
||||
Password = includeSecrets ? server.Password : null,
|
||||
DatabaseName = server.DatabaseName,
|
||||
UseSsl = server.UseSsl,
|
||||
ValidateCertificate = server.ValidateCertificate,
|
||||
AdditionalParams = server.AdditionalParams
|
||||
}).ToList(),
|
||||
Sites = sites.Select(site => new ConfigTransferSite
|
||||
{
|
||||
Key = siteKeyMap[site.Id],
|
||||
HanaServerKey = site.HanaServerId.HasValue && serverKeyMap.TryGetValue(site.HanaServerId.Value, out var serverKey) ? serverKey : null,
|
||||
Schema = site.Schema,
|
||||
TSC = site.TSC,
|
||||
Land = site.Land,
|
||||
SourceSystem = site.SourceSystem,
|
||||
UsernameOverride = includeSecrets ? site.UsernameOverride : null,
|
||||
PasswordOverride = includeSecrets ? site.PasswordOverride : null,
|
||||
SapServiceUrl = site.SapServiceUrl,
|
||||
SapEntitySet = site.SapEntitySet,
|
||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||
SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc,
|
||||
IsActive = site.IsActive
|
||||
}).ToList(),
|
||||
FieldTransformationRules = rules.Select(r => new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = r.SourceSystem,
|
||||
SourceField = r.SourceField,
|
||||
TargetField = r.TargetField,
|
||||
TransformationType = r.TransformationType,
|
||||
Argument = r.Argument,
|
||||
SortOrder = r.SortOrder,
|
||||
IsActive = r.IsActive
|
||||
}).ToList(),
|
||||
SapSourceDefinitions = sapSources.Select(s => new ConfigTransferSapSourceDefinition
|
||||
{
|
||||
SiteKey = siteKeyMap[s.SiteId],
|
||||
Alias = s.Alias,
|
||||
EntitySet = s.EntitySet,
|
||||
IsPrimary = s.IsPrimary,
|
||||
IsActive = s.IsActive,
|
||||
SortOrder = s.SortOrder
|
||||
}).ToList(),
|
||||
SapJoinDefinitions = sapJoins.Select(j => new ConfigTransferSapJoinDefinition
|
||||
{
|
||||
SiteKey = siteKeyMap[j.SiteId],
|
||||
LeftAlias = j.LeftAlias,
|
||||
RightAlias = j.RightAlias,
|
||||
LeftKeys = j.LeftKeys,
|
||||
RightKeys = j.RightKeys,
|
||||
JoinType = j.JoinType,
|
||||
IsActive = j.IsActive,
|
||||
SortOrder = j.SortOrder
|
||||
}).ToList(),
|
||||
SapFieldMappings = sapMappings.Select(m => new ConfigTransferSapFieldMapping
|
||||
{
|
||||
SiteKey = siteKeyMap[m.SiteId],
|
||||
TargetField = m.TargetField,
|
||||
SourceExpression = m.SourceExpression,
|
||||
IsRequired = m.IsRequired,
|
||||
IsActive = m.IsActive,
|
||||
SortOrder = m.SortOrder
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(package, JsonOptions);
|
||||
}
|
||||
|
||||
public async Task ImportJsonAsync(string json)
|
||||
{
|
||||
var package = JsonSerializer.Deserialize<ConfigTransferPackage>(json, JsonOptions)
|
||||
?? throw new InvalidOperationException("Konfigurationsdatei konnte nicht gelesen werden.");
|
||||
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var existingSharePoint = await db.SharePointConfigs.FirstOrDefaultAsync();
|
||||
var existingSettings = await db.ExportSettings.FirstOrDefaultAsync();
|
||||
var existingServers = await db.HanaServers.ToListAsync();
|
||||
var existingSites = await db.Sites.ToListAsync();
|
||||
var existingRules = await db.FieldTransformationRules.ToListAsync();
|
||||
var existingSapSources = await db.SapSourceDefinitions.ToListAsync();
|
||||
var existingSapJoins = await db.SapJoinDefinitions.ToListAsync();
|
||||
var existingSapMappings = await db.SapFieldMappings.ToListAsync();
|
||||
var existingCentralRecords = await db.CentralSalesRecords.ToListAsync();
|
||||
|
||||
var preservedSharePointSecret = existingSharePoint?.ClientSecret ?? string.Empty;
|
||||
var preservedSecrets = existingSettings is null
|
||||
? new ConfigTransferExportSettings()
|
||||
: new ConfigTransferExportSettings
|
||||
{
|
||||
SapUsername = existingSettings.SapUsername,
|
||||
SapPassword = existingSettings.SapPassword,
|
||||
Bi1Username = existingSettings.Bi1Username,
|
||||
Bi1Password = existingSettings.Bi1Password,
|
||||
SageUsername = existingSettings.SageUsername,
|
||||
SagePassword = existingSettings.SagePassword
|
||||
};
|
||||
var preservedServerSecrets = existingServers.ToDictionary(
|
||||
x => BuildServerSignature(x.Name, x.Host, x.Port, x.DatabaseName),
|
||||
x => (x.Username, x.Password));
|
||||
var preservedSiteSecrets = existingSites.ToDictionary(
|
||||
x => BuildSiteSignature(x.Land, x.TSC, x.Schema, x.SourceSystem),
|
||||
x => (x.UsernameOverride, x.PasswordOverride));
|
||||
|
||||
if (existingSapMappings.Count > 0) db.SapFieldMappings.RemoveRange(existingSapMappings);
|
||||
if (existingSapJoins.Count > 0) db.SapJoinDefinitions.RemoveRange(existingSapJoins);
|
||||
if (existingSapSources.Count > 0) db.SapSourceDefinitions.RemoveRange(existingSapSources);
|
||||
if (existingRules.Count > 0) db.FieldTransformationRules.RemoveRange(existingRules);
|
||||
if (existingCentralRecords.Count > 0) db.CentralSalesRecords.RemoveRange(existingCentralRecords);
|
||||
if (existingSites.Count > 0) db.Sites.RemoveRange(existingSites);
|
||||
if (existingServers.Count > 0) db.HanaServers.RemoveRange(existingServers);
|
||||
if (existingSharePoint is not null) db.SharePointConfigs.Remove(existingSharePoint);
|
||||
if (existingSettings is not null) db.ExportSettings.Remove(existingSettings);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var newSharePoint = package.SharePointConfig is null ? new SharePointConfig() : new SharePointConfig
|
||||
{
|
||||
SiteUrl = package.SharePointConfig.SiteUrl,
|
||||
ExportFolder = package.SharePointConfig.ExportFolder,
|
||||
TenantId = package.SharePointConfig.TenantId,
|
||||
ClientId = package.SharePointConfig.ClientId,
|
||||
ClientSecret = package.IncludesSecrets ? package.SharePointConfig.ClientSecret ?? string.Empty : preservedSharePointSecret
|
||||
};
|
||||
db.SharePointConfigs.Add(newSharePoint);
|
||||
|
||||
var importedSettings = package.ExportSettings ?? new ConfigTransferExportSettings();
|
||||
db.ExportSettings.Add(new ExportSettings
|
||||
{
|
||||
DateFilter = importedSettings.DateFilter,
|
||||
TimerHour = importedSettings.TimerHour,
|
||||
TimerMinute = importedSettings.TimerMinute,
|
||||
TimerEnabled = importedSettings.TimerEnabled,
|
||||
SapUsername = package.IncludesSecrets ? importedSettings.SapUsername ?? string.Empty : preservedSecrets.SapUsername ?? string.Empty,
|
||||
SapPassword = package.IncludesSecrets ? importedSettings.SapPassword ?? string.Empty : preservedSecrets.SapPassword ?? string.Empty,
|
||||
Bi1Username = package.IncludesSecrets ? importedSettings.Bi1Username ?? string.Empty : preservedSecrets.Bi1Username ?? string.Empty,
|
||||
Bi1Password = package.IncludesSecrets ? importedSettings.Bi1Password ?? string.Empty : preservedSecrets.Bi1Password ?? string.Empty,
|
||||
SageUsername = package.IncludesSecrets ? importedSettings.SageUsername ?? string.Empty : preservedSecrets.SageUsername ?? string.Empty,
|
||||
SagePassword = package.IncludesSecrets ? importedSettings.SagePassword ?? string.Empty : preservedSecrets.SagePassword ?? string.Empty
|
||||
});
|
||||
|
||||
var serverIdMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var server in package.HanaServers)
|
||||
{
|
||||
preservedServerSecrets.TryGetValue(BuildServerSignature(server.Name, server.Host, server.Port, server.DatabaseName), out var preserved);
|
||||
var entity = new HanaServer
|
||||
{
|
||||
Name = server.Name,
|
||||
Host = server.Host,
|
||||
Port = server.Port,
|
||||
Username = package.IncludesSecrets ? server.Username ?? string.Empty : preserved.Username ?? string.Empty,
|
||||
Password = package.IncludesSecrets ? server.Password ?? string.Empty : preserved.Password ?? string.Empty,
|
||||
DatabaseName = server.DatabaseName,
|
||||
UseSsl = server.UseSsl,
|
||||
ValidateCertificate = server.ValidateCertificate,
|
||||
AdditionalParams = server.AdditionalParams
|
||||
};
|
||||
db.HanaServers.Add(entity);
|
||||
await db.SaveChangesAsync();
|
||||
serverIdMap[server.Key] = entity.Id;
|
||||
}
|
||||
|
||||
var siteIdMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var site in package.Sites)
|
||||
{
|
||||
preservedSiteSecrets.TryGetValue(BuildSiteSignature(site.Land, site.TSC, site.Schema, site.SourceSystem), out var preserved);
|
||||
var entity = new Site
|
||||
{
|
||||
HanaServerId = !string.IsNullOrWhiteSpace(site.HanaServerKey) && serverIdMap.TryGetValue(site.HanaServerKey, out var mappedServerId)
|
||||
? mappedServerId
|
||||
: null,
|
||||
Schema = site.Schema,
|
||||
TSC = site.TSC,
|
||||
Land = site.Land,
|
||||
SourceSystem = site.SourceSystem,
|
||||
UsernameOverride = package.IncludesSecrets ? site.UsernameOverride ?? string.Empty : preserved.UsernameOverride ?? string.Empty,
|
||||
PasswordOverride = package.IncludesSecrets ? site.PasswordOverride ?? string.Empty : preserved.PasswordOverride ?? string.Empty,
|
||||
SapServiceUrl = site.SapServiceUrl,
|
||||
SapEntitySet = site.SapEntitySet,
|
||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||
SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc,
|
||||
IsActive = site.IsActive
|
||||
};
|
||||
db.Sites.Add(entity);
|
||||
await db.SaveChangesAsync();
|
||||
siteIdMap[site.Key] = entity.Id;
|
||||
}
|
||||
|
||||
if (package.FieldTransformationRules.Count > 0)
|
||||
{
|
||||
db.FieldTransformationRules.AddRange(package.FieldTransformationRules.Select(r => new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = r.SourceSystem,
|
||||
SourceField = r.SourceField,
|
||||
TargetField = r.TargetField,
|
||||
TransformationType = r.TransformationType,
|
||||
Argument = r.Argument,
|
||||
SortOrder = r.SortOrder,
|
||||
IsActive = r.IsActive
|
||||
}));
|
||||
}
|
||||
|
||||
if (package.SapSourceDefinitions.Count > 0)
|
||||
{
|
||||
db.SapSourceDefinitions.AddRange(package.SapSourceDefinitions
|
||||
.Where(x => siteIdMap.ContainsKey(x.SiteKey))
|
||||
.Select(x => new SapSourceDefinition
|
||||
{
|
||||
SiteId = siteIdMap[x.SiteKey],
|
||||
Alias = x.Alias,
|
||||
EntitySet = x.EntitySet,
|
||||
IsPrimary = x.IsPrimary,
|
||||
IsActive = x.IsActive,
|
||||
SortOrder = x.SortOrder
|
||||
}));
|
||||
}
|
||||
|
||||
if (package.SapJoinDefinitions.Count > 0)
|
||||
{
|
||||
db.SapJoinDefinitions.AddRange(package.SapJoinDefinitions
|
||||
.Where(x => siteIdMap.ContainsKey(x.SiteKey))
|
||||
.Select(x => new SapJoinDefinition
|
||||
{
|
||||
SiteId = siteIdMap[x.SiteKey],
|
||||
LeftAlias = x.LeftAlias,
|
||||
RightAlias = x.RightAlias,
|
||||
LeftKeys = x.LeftKeys,
|
||||
RightKeys = x.RightKeys,
|
||||
JoinType = x.JoinType,
|
||||
IsActive = x.IsActive,
|
||||
SortOrder = x.SortOrder
|
||||
}));
|
||||
}
|
||||
|
||||
if (package.SapFieldMappings.Count > 0)
|
||||
{
|
||||
db.SapFieldMappings.AddRange(package.SapFieldMappings
|
||||
.Where(x => siteIdMap.ContainsKey(x.SiteKey))
|
||||
.Select(x => new SapFieldMapping
|
||||
{
|
||||
SiteId = siteIdMap[x.SiteKey],
|
||||
TargetField = x.TargetField,
|
||||
SourceExpression = x.SourceExpression,
|
||||
IsRequired = x.IsRequired,
|
||||
IsActive = x.IsActive,
|
||||
SortOrder = x.SortOrder
|
||||
}));
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static string BuildServerSignature(string name, string host, int port, string databaseName)
|
||||
=> $"{name}|{host}|{port}|{databaseName}".ToUpperInvariant();
|
||||
|
||||
private static string BuildSiteSignature(string land, string tsc, string schema, string sourceSystem)
|
||||
=> $"{land}|{tsc}|{schema}|{sourceSystem}".ToUpperInvariant();
|
||||
}
|
||||
@@ -7,22 +7,26 @@ namespace TrafagSalesExporter.Services;
|
||||
public class ConsolidatedExportService : IConsolidatedExportService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly ICentralSalesRecordService _centralSalesRecordService;
|
||||
private readonly IExcelExportService _excelService;
|
||||
private readonly ISharePointUploadService _sharePointService;
|
||||
|
||||
public ConsolidatedExportService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
ICentralSalesRecordService centralSalesRecordService,
|
||||
IExcelExportService excelService,
|
||||
ISharePointUploadService sharePointService)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_centralSalesRecordService = centralSalesRecordService;
|
||||
_excelService = excelService;
|
||||
_sharePointService = sharePointService;
|
||||
}
|
||||
|
||||
public async Task<string?> ExportAsync(List<SalesRecord> records)
|
||||
{
|
||||
if (records.Count == 0)
|
||||
var consolidatedRecords = await _centralSalesRecordService.GetAllAsync();
|
||||
if (consolidatedRecords.Count == 0)
|
||||
return null;
|
||||
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
@@ -31,7 +35,7 @@ public class ConsolidatedExportService : IConsolidatedExportService
|
||||
var consolidatedPath = _excelService.CreateConsolidatedExcelFile(
|
||||
outputDir,
|
||||
DateTime.UtcNow.Date,
|
||||
records
|
||||
consolidatedRecords
|
||||
.OrderBy(r => r.Land)
|
||||
.ThenBy(r => r.Tsc)
|
||||
.ThenByDescending(r => r.InvoiceDate ?? DateTime.MinValue)
|
||||
|
||||
@@ -43,6 +43,10 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
AddColumnIfMissing(db, "ExportSettings", "SageUsername", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportSettings", "SagePassword", "TEXT NOT NULL DEFAULT ''");
|
||||
EnsureTransformationTable(db);
|
||||
EnsureSapSourceTable(db);
|
||||
EnsureSapJoinTable(db);
|
||||
EnsureSapFieldMappingTable(db);
|
||||
EnsureCentralSalesRecordTable(db);
|
||||
}
|
||||
|
||||
private static void EnsureSitesTableSupportsOptionalHanaServer(AppDbContext db)
|
||||
@@ -193,6 +197,115 @@ CREATE TABLE IF NOT EXISTS FieldTransformationRules (
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSapSourceTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS SapSourceDefinitions (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
Alias TEXT NOT NULL,
|
||||
EntitySet TEXT NOT NULL,
|
||||
IsPrimary INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSapJoinTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS SapJoinDefinitions (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
LeftAlias TEXT NOT NULL,
|
||||
RightAlias TEXT NOT NULL,
|
||||
LeftKeys TEXT NOT NULL,
|
||||
RightKeys TEXT NOT NULL,
|
||||
JoinType TEXT NOT NULL DEFAULT 'Left',
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSapFieldMappingTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS SapFieldMappings (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
TargetField TEXT NOT NULL,
|
||||
SourceExpression 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)
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureCentralSalesRecordTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS CentralSalesRecords (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
StoredAtUtc TEXT NOT NULL,
|
||||
SiteId INTEGER NOT NULL,
|
||||
SourceSystem TEXT NOT NULL,
|
||||
ExtractionDate TEXT NOT NULL,
|
||||
Tsc TEXT NOT NULL,
|
||||
InvoiceNumber TEXT NOT NULL,
|
||||
PositionOnInvoice INTEGER NOT NULL,
|
||||
Material TEXT NOT NULL,
|
||||
Name TEXT NOT NULL,
|
||||
ProductGroup TEXT NOT NULL,
|
||||
Quantity TEXT NOT NULL,
|
||||
SupplierNumber TEXT NOT NULL,
|
||||
SupplierName TEXT NOT NULL,
|
||||
SupplierCountry TEXT NOT NULL,
|
||||
CustomerNumber TEXT NOT NULL,
|
||||
CustomerName TEXT NOT NULL,
|
||||
CustomerCountry TEXT NOT NULL,
|
||||
CustomerIndustry TEXT NOT NULL,
|
||||
StandardCost TEXT NOT NULL,
|
||||
StandardCostCurrency TEXT NOT NULL,
|
||||
PurchaseOrderNumber TEXT NOT NULL,
|
||||
SalesPriceValue TEXT NOT NULL,
|
||||
SalesCurrency TEXT NOT NULL,
|
||||
Incoterms2020 TEXT NOT NULL,
|
||||
SalesResponsibleEmployee TEXT NOT NULL,
|
||||
InvoiceDate TEXT NULL,
|
||||
OrderDate TEXT NULL,
|
||||
Land TEXT NOT NULL,
|
||||
DocumentType TEXT NOT NULL,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void SeedIfEmpty(AppDbContext db)
|
||||
{
|
||||
if (db.HanaServers.Any())
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface ICentralSalesRecordService
|
||||
{
|
||||
Task ReplaceForSiteAsync(Site site, IEnumerable<SalesRecord> records);
|
||||
Task<List<SalesRecord>> GetAllAsync();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IConfigTransferService
|
||||
{
|
||||
Task<string> ExportJsonAsync(bool includeSecrets);
|
||||
Task ImportJsonAsync(string json);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface ISapCompositionService
|
||||
{
|
||||
Task<List<SalesRecord>> BuildSalesRecordsAsync(
|
||||
Site site,
|
||||
IReadOnlyList<SapSourceDefinition> sources,
|
||||
IReadOnlyList<SapJoinDefinition> joins,
|
||||
IReadOnlyList<SapFieldMapping> mappings,
|
||||
string username,
|
||||
string password,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
using System.Globalization;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class SapCompositionService : ISapCompositionService
|
||||
{
|
||||
private readonly ISapGatewayService _sapGatewayService;
|
||||
|
||||
public SapCompositionService(ISapGatewayService sapGatewayService)
|
||||
{
|
||||
_sapGatewayService = sapGatewayService;
|
||||
}
|
||||
|
||||
public async Task<List<SalesRecord>> BuildSalesRecordsAsync(
|
||||
Site site,
|
||||
IReadOnlyList<SapSourceDefinition> sources,
|
||||
IReadOnlyList<SapJoinDefinition> joins,
|
||||
IReadOnlyList<SapFieldMapping> mappings,
|
||||
string username,
|
||||
string password,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP Service URL.");
|
||||
|
||||
var activeSources = sources
|
||||
.Where(s => s.IsActive)
|
||||
.OrderBy(s => s.SortOrder)
|
||||
.ThenBy(s => s.Id)
|
||||
.ToList();
|
||||
if (activeSources.Count == 0)
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keine aktiven SAP-Quellen.");
|
||||
|
||||
var primarySource = activeSources.FirstOrDefault(s => s.IsPrimary) ?? activeSources.First();
|
||||
var sourceRows = new Dictionary<string, List<Dictionary<string, object?>>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var source in activeSources)
|
||||
{
|
||||
var rows = await _sapGatewayService.GetEntityRowsAsync(site.SapServiceUrl, source.EntitySet, username, password, cancellationToken);
|
||||
sourceRows[source.Alias] = rows;
|
||||
}
|
||||
|
||||
var composedRows = sourceRows[primarySource.Alias]
|
||||
.Select(r => PrefixRow(primarySource.Alias, r))
|
||||
.ToList();
|
||||
|
||||
foreach (var join in joins.Where(j => j.IsActive).OrderBy(j => j.SortOrder).ThenBy(j => j.Id))
|
||||
{
|
||||
if (!sourceRows.TryGetValue(join.RightAlias, out var rightRows))
|
||||
continue;
|
||||
|
||||
composedRows = ApplyLeftJoin(composedRows, join.LeftAlias, join.LeftKeys, join.RightAlias, join.RightKeys, rightRows);
|
||||
}
|
||||
|
||||
return composedRows
|
||||
.Select(row => MapToSalesRecord(site, row, mappings))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> PrefixRow(string alias, Dictionary<string, object?> row)
|
||||
=> row.ToDictionary(kvp => $"{alias}.{kvp.Key}", kvp => kvp.Value, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static List<Dictionary<string, object?>> ApplyLeftJoin(
|
||||
List<Dictionary<string, object?>> leftRows,
|
||||
string leftAlias,
|
||||
string leftKeys,
|
||||
string rightAlias,
|
||||
string rightKeys,
|
||||
List<Dictionary<string, object?>> rightRows)
|
||||
{
|
||||
var leftKeyParts = SplitKeys(leftKeys);
|
||||
var rightKeyParts = SplitKeys(rightKeys);
|
||||
if (leftKeyParts.Count == 0 || leftKeyParts.Count != rightKeyParts.Count)
|
||||
return leftRows;
|
||||
|
||||
var rightLookup = rightRows
|
||||
.GroupBy(r => BuildKey(r, rightKeyParts))
|
||||
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var results = new List<Dictionary<string, object?>>();
|
||||
foreach (var leftRow in leftRows)
|
||||
{
|
||||
var leftKey = BuildKey(leftRow, leftAlias, leftKeyParts);
|
||||
if (rightLookup.TryGetValue(leftKey, out var matches) && matches.Count > 0)
|
||||
{
|
||||
foreach (var match in matches)
|
||||
{
|
||||
var merged = new Dictionary<string, object?>(leftRow, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var kvp in PrefixRow(rightAlias, match))
|
||||
merged[kvp.Key] = kvp.Value;
|
||||
results.Add(merged);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(leftRow);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static SalesRecord MapToSalesRecord(Site site, Dictionary<string, object?> row, IReadOnlyList<SapFieldMapping> mappings)
|
||||
{
|
||||
var record = new SalesRecord
|
||||
{
|
||||
ExtractionDate = DateTime.UtcNow,
|
||||
Tsc = site.TSC,
|
||||
Land = site.Land,
|
||||
DocumentType = "SAP"
|
||||
};
|
||||
|
||||
foreach (var mapping in mappings.Where(m => m.IsActive).OrderBy(m => m.SortOrder).ThenBy(m => m.Id))
|
||||
{
|
||||
var value = EvaluateExpression(row, mapping.SourceExpression);
|
||||
ApplyValue(record, mapping.TargetField, 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;
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
private static object? EvaluateExpression(Dictionary<string, object?> row, string expression)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(expression))
|
||||
return null;
|
||||
|
||||
var value = expression.Trim();
|
||||
if (value.StartsWith('='))
|
||||
return value[1..];
|
||||
|
||||
if (row.TryGetValue(value, out var direct))
|
||||
return direct;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ApplyValue(SalesRecord record, string targetField, object? value)
|
||||
{
|
||||
var property = typeof(SalesRecord).GetProperty(targetField);
|
||||
if (property is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (property.PropertyType == typeof(string))
|
||||
{
|
||||
property.SetValue(record, value?.ToString() ?? string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.PropertyType == typeof(int))
|
||||
{
|
||||
if (int.TryParse(value?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var intValue))
|
||||
property.SetValue(record, intValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.PropertyType == typeof(decimal))
|
||||
{
|
||||
if (decimal.TryParse(value?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var decimalValue))
|
||||
property.SetValue(record, decimalValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.PropertyType == typeof(DateTime?) || property.PropertyType == typeof(DateTime))
|
||||
{
|
||||
if (TryParseDate(value?.ToString(), out var date))
|
||||
property.SetValue(record, date);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore invalid mappings and continue with remaining fields
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseDate(string? value, out DateTime date)
|
||||
{
|
||||
date = default;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
var trimmed = value.Trim();
|
||||
if (trimmed.StartsWith("/Date(", StringComparison.Ordinal) && trimmed.EndsWith(")/", StringComparison.Ordinal))
|
||||
{
|
||||
var epochRaw = trimmed[6..^2];
|
||||
var separator = epochRaw.IndexOfAny(['+', '-']);
|
||||
if (separator > 0)
|
||||
epochRaw = epochRaw[..separator];
|
||||
if (long.TryParse(epochRaw, out var ms))
|
||||
{
|
||||
date = DateTimeOffset.FromUnixTimeMilliseconds(ms).UtcDateTime;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return DateTime.TryParse(trimmed, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out date)
|
||||
|| DateTime.TryParse(trimmed, CultureInfo.GetCultureInfo("de-CH"), DateTimeStyles.AssumeLocal, out date);
|
||||
}
|
||||
|
||||
private static string BuildKey(Dictionary<string, object?> row, IReadOnlyList<string> keys)
|
||||
=> string.Join("||", keys.Select(k => NormalizeKeyValue(row.TryGetValue(k, out var value) ? value : null)));
|
||||
|
||||
private static string BuildKey(Dictionary<string, object?> row, string alias, IReadOnlyList<string> keys)
|
||||
=> string.Join("||", keys.Select(k =>
|
||||
{
|
||||
row.TryGetValue($"{alias}.{k}", out var value);
|
||||
return NormalizeKeyValue(value);
|
||||
}));
|
||||
|
||||
private static string NormalizeKeyValue(object? value) => value?.ToString()?.Trim() ?? string.Empty;
|
||||
|
||||
private static List<string> SplitKeys(string keys)
|
||||
=> keys.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||
}
|
||||
@@ -10,26 +10,32 @@ public class SiteExportService : ISiteExportService
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly IHanaQueryService _hanaService;
|
||||
private readonly ISapGatewayService _sapGatewayService;
|
||||
private readonly ISapCompositionService _sapCompositionService;
|
||||
private readonly IExcelExportService _excelService;
|
||||
private readonly ISharePointUploadService _sharePointService;
|
||||
private readonly IRecordTransformationService _transformationService;
|
||||
private readonly ICentralSalesRecordService _centralSalesRecordService;
|
||||
private readonly ILogger<SiteExportService> _logger;
|
||||
|
||||
public SiteExportService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
IHanaQueryService hanaService,
|
||||
ISapGatewayService sapGatewayService,
|
||||
ISapCompositionService sapCompositionService,
|
||||
IExcelExportService excelService,
|
||||
ISharePointUploadService sharePointService,
|
||||
IRecordTransformationService transformationService,
|
||||
ICentralSalesRecordService centralSalesRecordService,
|
||||
ILogger<SiteExportService> logger)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_hanaService = hanaService;
|
||||
_sapGatewayService = sapGatewayService;
|
||||
_sapCompositionService = sapCompositionService;
|
||||
_excelService = excelService;
|
||||
_sharePointService = sharePointService;
|
||||
_transformationService = transformationService;
|
||||
_centralSalesRecordService = centralSalesRecordService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -59,14 +65,25 @@ public class SiteExportService : ISiteExportService
|
||||
var credentials = ResolveCredentials(site, settings, sourceSystem);
|
||||
if (string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP Service URL.");
|
||||
if (string.IsNullOrWhiteSpace(site.SapEntitySet))
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat kein SAP Entity Set ausgewählt.");
|
||||
var sapSources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync();
|
||||
var sapJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).ToListAsync();
|
||||
var sapMappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).ToListAsync();
|
||||
if (sapSources.Count == 0)
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP-Quellen konfiguriert.");
|
||||
if (sapMappings.Count == 0)
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP-Feldmappings.");
|
||||
|
||||
updateStatus?.Invoke("SAP Gateway Abfrage...");
|
||||
var rows = await _sapGatewayService.GetEntityRowsAsync(site.SapServiceUrl, site.SapEntitySet, credentials.Username, credentials.Password);
|
||||
updateStatus?.Invoke("SAP Quellen laden...");
|
||||
records = await _sapCompositionService.BuildSalesRecordsAsync(site, sapSources, sapJoins, sapMappings, credentials.Username, credentials.Password);
|
||||
updateStatus?.Invoke("Transformationen anwenden...");
|
||||
var rules = await db.FieldTransformationRules
|
||||
.Where(r => r.IsActive && r.SourceSystem == sourceSystem)
|
||||
.OrderBy(r => r.SortOrder)
|
||||
.ToListAsync();
|
||||
_transformationService.Apply(records, rules);
|
||||
updateStatus?.Invoke("Excel erstellen...");
|
||||
filePath = _excelService.CreateGenericExcelFile(outputDir, $"SAP_{site.TSC}_{site.SapEntitySet}", DateTime.UtcNow.Date, site.SapEntitySet, rows);
|
||||
log.RowCount = rows.Count;
|
||||
filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
||||
log.RowCount = records.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -87,6 +104,9 @@ public class SiteExportService : ISiteExportService
|
||||
log.RowCount = records.Count;
|
||||
}
|
||||
|
||||
updateStatus?.Invoke("Zentrale Tabelle aktualisieren...");
|
||||
await _centralSalesRecordService.ReplaceForSiteAsync(site, records);
|
||||
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
if (spConfig is not null &&
|
||||
|
||||
Reference in New Issue
Block a user