import exxport settings, join over sap hana tables
This commit is contained in:
@@ -14,5 +14,6 @@
|
|||||||
<Routes @rendermode="@Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer" />
|
<Routes @rendermode="@Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer" />
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
|
<script src="js/download.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -116,7 +116,9 @@
|
|||||||
Land = s.Land,
|
Land = s.Land,
|
||||||
TSC = s.TSC,
|
TSC = s.TSC,
|
||||||
Schema = s.Schema,
|
Schema = s.Schema,
|
||||||
ServerName = s.HanaServer?.Name ?? "",
|
ServerName = string.Equals(s.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? (string.IsNullOrWhiteSpace(s.SapServiceUrl) ? "SAP Gateway" : s.SapServiceUrl)
|
||||||
|
: s.HanaServer?.Name ?? "",
|
||||||
LastStatus = log?.Status ?? "",
|
LastStatus = log?.Status ?? "",
|
||||||
RowCount = log?.RowCount ?? 0,
|
RowCount = log?.RowCount ?? 0,
|
||||||
LastRun = log?.Timestamp,
|
LastRun = log?.Timestamp,
|
||||||
|
|||||||
@@ -8,12 +8,39 @@
|
|||||||
@inject TimerBackgroundService TimerService
|
@inject TimerBackgroundService TimerService
|
||||||
@inject IHanaQueryService HanaService
|
@inject IHanaQueryService HanaService
|
||||||
@inject ISapGatewayService SapGatewayService
|
@inject ISapGatewayService SapGatewayService
|
||||||
|
@inject IConfigTransferService ConfigTransferService
|
||||||
|
@inject IJSRuntime JS
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
<PageTitle>Settings</PageTitle>
|
<PageTitle>Settings</PageTitle>
|
||||||
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-4">Settings</MudText>
|
<MudText Typo="Typo.h4" Class="mb-4">Settings</MudText>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h5" Class="mb-2">Konfiguration Import/Export</MudText>
|
||||||
|
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudCheckBox @bind-Value="_includeSecretsInExport" Label="Mit Secrets exportieren" />
|
||||||
|
<MudText Typo="Typo.caption">
|
||||||
|
Wenn deaktiviert, bleiben Passwörter und Secrets beim Export leer. Beim Import ohne Secrets werden bestehende Secrets auf dem Zielsystem beibehalten.
|
||||||
|
</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudStack Row Spacing="2">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ExportConfiguration"
|
||||||
|
StartIcon="@Icons.Material.Filled.Download" Disabled="_exportingConfig">
|
||||||
|
@(_exportingConfig ? "Exportiere..." : "Konfiguration exportieren")
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Warning" HtmlTag="label"
|
||||||
|
StartIcon="@Icons.Material.Filled.UploadFile" Disabled="_importingConfig">
|
||||||
|
@(_importingConfig ? "Importiere..." : "Konfiguration importieren")
|
||||||
|
<InputFile OnChange="ImportConfiguration" accept=".json,application/json" style="display:none" />
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
@* SharePoint Config *@
|
@* SharePoint Config *@
|
||||||
<MudText Typo="Typo.h5" Class="mb-2">SharePoint Konfiguration</MudText>
|
<MudText Typo="Typo.h5" Class="mb-2">SharePoint Konfiguration</MudText>
|
||||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||||
@@ -166,6 +193,9 @@
|
|||||||
private SharePointConfig _spConfig = new();
|
private SharePointConfig _spConfig = new();
|
||||||
private ExportSettings _exportSettings = new();
|
private ExportSettings _exportSettings = new();
|
||||||
private bool _testingSp;
|
private bool _testingSp;
|
||||||
|
private bool _includeSecretsInExport;
|
||||||
|
private bool _exportingConfig;
|
||||||
|
private bool _importingConfig;
|
||||||
private readonly HashSet<string> _testingSystems = [];
|
private readonly HashSet<string> _testingSystems = [];
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -240,6 +270,60 @@
|
|||||||
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
|
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ExportConfiguration()
|
||||||
|
{
|
||||||
|
if (_exportingConfig)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_exportingConfig = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await ConfigTransferService.ExportJsonAsync(_includeSecretsInExport);
|
||||||
|
var suffix = _includeSecretsInExport ? "with-secrets" : "without-secrets";
|
||||||
|
var fileName = $"trafag-config-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{suffix}.json";
|
||||||
|
await JS.InvokeVoidAsync("trafagDownload.saveTextFile", fileName, json, "application/json;charset=utf-8");
|
||||||
|
Snackbar.Add("Konfiguration exportiert", Severity.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Export fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_exportingConfig = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ImportConfiguration(InputFileChangeEventArgs args)
|
||||||
|
{
|
||||||
|
if (_importingConfig)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_importingConfig = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var file = args.File;
|
||||||
|
await using var stream = file.OpenReadStream(5 * 1024 * 1024);
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var json = await reader.ReadToEndAsync();
|
||||||
|
await ConfigTransferService.ImportJsonAsync(json);
|
||||||
|
|
||||||
|
using var db = await DbFactory.CreateDbContextAsync();
|
||||||
|
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
|
||||||
|
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
|
||||||
|
TimerService.Recalculate();
|
||||||
|
Snackbar.Add("Konfiguration importiert", Severity.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Import fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_importingConfig = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task TestCentralCredentials(string sourceSystem)
|
private async Task TestCentralCredentials(string sourceSystem)
|
||||||
{
|
{
|
||||||
if (sourceSystem == "SAP")
|
if (sourceSystem == "SAP")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@page "/standorte"
|
@page "/standorte"
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
|
@using System.Reflection
|
||||||
@using TrafagSalesExporter.Data
|
@using TrafagSalesExporter.Data
|
||||||
@using TrafagSalesExporter.Models
|
@using TrafagSalesExporter.Models
|
||||||
@using TrafagSalesExporter.Services
|
@using TrafagSalesExporter.Services
|
||||||
@@ -180,12 +181,110 @@
|
|||||||
</MudText>
|
</MudText>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudSelect @bind-Value="_editingSite.SapEntitySet" Label="SAP Entity Set" Required>
|
<MudDivider Class="my-4" />
|
||||||
@foreach (var entitySet in _sapEntitySetsCache)
|
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
|
||||||
{
|
<MudText Typo="Typo.h6">SAP Quellen</MudText>
|
||||||
<MudSelectItem Value="@entitySet">@entitySet</MudSelectItem>
|
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapSource">Quelle hinzufügen</MudButton>
|
||||||
}
|
</MudStack>
|
||||||
</MudSelect>
|
<MudText Typo="Typo.caption" Class="mb-2">
|
||||||
|
Pro Quelle Alias und Entity Set definieren. Joins verwenden links/rechts kommagetrennte Schlüsselfelder wie `VBELN,POSNR`. Feldmappings erwarten `Alias.Feldname` oder Konstanten wie `=SAP`.
|
||||||
|
</MudText>
|
||||||
|
<MudTable Items="_sapSources" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Alias</MudTh>
|
||||||
|
<MudTh>Entity Set</MudTh>
|
||||||
|
<MudTh>Primär</MudTh>
|
||||||
|
<MudTh>Aktiv</MudTh>
|
||||||
|
<MudTh>Aktionen</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd><MudTextField @bind-Value="context.Alias" Dense /></MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudSelect @bind-Value="context.EntitySet" Dense>
|
||||||
|
@foreach (var entitySet in _sapEntitySetsCache)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@entitySet">@entitySet</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd><MudCheckBox @bind-Value="context.IsPrimary" Dense /></MudTd>
|
||||||
|
<MudTd><MudCheckBox @bind-Value="context.IsActive" Dense /></MudTd>
|
||||||
|
<MudTd><MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="() => RemoveSapSource(context)" /></MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
|
||||||
|
<MudDivider Class="my-4" />
|
||||||
|
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
|
||||||
|
<MudText Typo="Typo.h6">SAP Joins</MudText>
|
||||||
|
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapJoin">Join hinzufügen</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
<MudTable Items="_sapJoins" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Links</MudTh>
|
||||||
|
<MudTh>Left Keys</MudTh>
|
||||||
|
<MudTh>Rechts</MudTh>
|
||||||
|
<MudTh>Right Keys</MudTh>
|
||||||
|
<MudTh>Typ</MudTh>
|
||||||
|
<MudTh>Aktiv</MudTh>
|
||||||
|
<MudTh>Aktionen</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>
|
||||||
|
<MudSelect @bind-Value="context.LeftAlias" Dense>
|
||||||
|
@foreach (var alias in GetSapAliases())
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@alias">@alias</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd><MudTextField @bind-Value="context.LeftKeys" Dense Placeholder="VBELN,POSNR" /></MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudSelect @bind-Value="context.RightAlias" Dense>
|
||||||
|
@foreach (var alias in GetSapAliases())
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@alias">@alias</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd><MudTextField @bind-Value="context.RightKeys" Dense Placeholder="VBELN,POSNR" /></MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudSelect @bind-Value="context.JoinType" Dense>
|
||||||
|
<MudSelectItem Value="@("Left")">Left</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd><MudCheckBox @bind-Value="context.IsActive" Dense /></MudTd>
|
||||||
|
<MudTd><MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="() => RemoveSapJoin(context)" /></MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
|
||||||
|
<MudDivider Class="my-4" />
|
||||||
|
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
|
||||||
|
<MudText Typo="Typo.h6">Feldmappings ins zentrale Schema</MudText>
|
||||||
|
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapMapping">Mapping hinzufügen</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
<MudTable Items="_sapMappings" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Zielfeld</MudTh>
|
||||||
|
<MudTh>Source Expression</MudTh>
|
||||||
|
<MudTh>Pflicht</MudTh>
|
||||||
|
<MudTh>Aktiv</MudTh>
|
||||||
|
<MudTh>Aktionen</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>
|
||||||
|
<MudSelect @bind-Value="context.TargetField" Dense>
|
||||||
|
@foreach (var field in _salesRecordFields)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@field">@field</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd><MudTextField @bind-Value="context.SourceExpression" Dense Placeholder="VBAK.VBELN oder =SAP" /></MudTd>
|
||||||
|
<MudTd><MudCheckBox @bind-Value="context.IsRequired" Dense /></MudTd>
|
||||||
|
<MudTd><MudCheckBox @bind-Value="context.IsActive" Dense /></MudTd>
|
||||||
|
<MudTd><MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="() => RemoveSapMapping(context)" /></MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -222,6 +321,13 @@
|
|||||||
private List<HanaServer> _servers = new();
|
private List<HanaServer> _servers = new();
|
||||||
private List<Site> _sites = new();
|
private List<Site> _sites = new();
|
||||||
private List<string> _sapEntitySetsCache = [];
|
private List<string> _sapEntitySetsCache = [];
|
||||||
|
private List<SapSourceDefinition> _sapSources = [];
|
||||||
|
private List<SapJoinDefinition> _sapJoins = [];
|
||||||
|
private List<SapFieldMapping> _sapMappings = [];
|
||||||
|
private readonly string[] _salesRecordFields = typeof(SalesRecord)
|
||||||
|
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Select(p => p.Name)
|
||||||
|
.ToArray();
|
||||||
private HanaServer _editingServer = new();
|
private HanaServer _editingServer = new();
|
||||||
private Site _editingSite = new();
|
private Site _editingSite = new();
|
||||||
private HanaServer _editingSiteServer = new();
|
private HanaServer _editingSiteServer = new();
|
||||||
@@ -348,9 +454,12 @@
|
|||||||
{
|
{
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
SourceSystem = "SAP",
|
SourceSystem = "SAP",
|
||||||
HanaServerId = 0
|
HanaServerId = null
|
||||||
};
|
};
|
||||||
_sapEntitySetsCache = [];
|
_sapEntitySetsCache = [];
|
||||||
|
_sapSources = [];
|
||||||
|
_sapJoins = [];
|
||||||
|
_sapMappings = [];
|
||||||
_editingSiteServer = CreateDefaultSiteServer();
|
_editingSiteServer = CreateDefaultSiteServer();
|
||||||
_siteDialogVisible = true;
|
_siteDialogVisible = true;
|
||||||
}
|
}
|
||||||
@@ -374,6 +483,10 @@
|
|||||||
IsActive = site.IsActive
|
IsActive = site.IsActive
|
||||||
};
|
};
|
||||||
_sapEntitySetsCache = ParseSapEntitySets(site.SapEntitySetsCache);
|
_sapEntitySetsCache = ParseSapEntitySets(site.SapEntitySetsCache);
|
||||||
|
using var db = DbFactory.CreateDbContext();
|
||||||
|
_sapSources = db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).OrderBy(s => s.SortOrder).ThenBy(s => s.Id).ToList();
|
||||||
|
_sapJoins = db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).OrderBy(j => j.SortOrder).ThenBy(j => j.Id).ToList();
|
||||||
|
_sapMappings = db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToList();
|
||||||
_editingSiteServer = site.HanaServer is null
|
_editingSiteServer = site.HanaServer is null
|
||||||
? CreateDefaultSiteServer(site)
|
? CreateDefaultSiteServer(site)
|
||||||
: CloneServer(site.HanaServer);
|
: CloneServer(site.HanaServer);
|
||||||
@@ -418,6 +531,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
await SaveSapConfigurationAsync(db, _editingSite.Id);
|
||||||
_siteDialogVisible = false;
|
_siteDialogVisible = false;
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
Snackbar.Add("Standort gespeichert", Severity.Success);
|
Snackbar.Add("Standort gespeichert", Severity.Success);
|
||||||
@@ -445,6 +559,14 @@
|
|||||||
var entity = await db.Sites.FindAsync(site.Id);
|
var entity = await db.Sites.FindAsync(site.Id);
|
||||||
if (entity is not null)
|
if (entity is not null)
|
||||||
{
|
{
|
||||||
|
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 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 (centralRows.Count > 0) db.CentralSalesRecords.RemoveRange(centralRows);
|
||||||
db.Sites.Remove(entity);
|
db.Sites.Remove(entity);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
@@ -632,4 +754,97 @@
|
|||||||
|
|
||||||
private static string SerializeSapEntitySets(List<string> entitySets)
|
private static string SerializeSapEntitySets(List<string> entitySets)
|
||||||
=> JsonSerializer.Serialize(entitySets);
|
=> JsonSerializer.Serialize(entitySets);
|
||||||
|
|
||||||
|
private void AddSapSource()
|
||||||
|
{
|
||||||
|
_sapSources.Add(new SapSourceDefinition
|
||||||
|
{
|
||||||
|
Alias = $"SRC{_sapSources.Count + 1}",
|
||||||
|
EntitySet = _sapEntitySetsCache.FirstOrDefault() ?? string.Empty,
|
||||||
|
IsActive = true,
|
||||||
|
IsPrimary = _sapSources.Count == 0,
|
||||||
|
SortOrder = _sapSources.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSapSource(SapSourceDefinition source)
|
||||||
|
{
|
||||||
|
_sapSources.Remove(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddSapJoin()
|
||||||
|
{
|
||||||
|
_sapJoins.Add(new SapJoinDefinition
|
||||||
|
{
|
||||||
|
JoinType = "Left",
|
||||||
|
IsActive = true,
|
||||||
|
SortOrder = _sapJoins.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSapJoin(SapJoinDefinition join)
|
||||||
|
{
|
||||||
|
_sapJoins.Remove(join);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddSapMapping()
|
||||||
|
{
|
||||||
|
_sapMappings.Add(new SapFieldMapping
|
||||||
|
{
|
||||||
|
TargetField = _salesRecordFields.First(),
|
||||||
|
IsActive = true,
|
||||||
|
SortOrder = _sapMappings.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSapMapping(SapFieldMapping mapping)
|
||||||
|
{
|
||||||
|
_sapMappings.Remove(mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> GetSapAliases()
|
||||||
|
=> _sapSources.Where(s => !string.IsNullOrWhiteSpace(s.Alias)).Select(s => s.Alias).Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private async Task SaveSapConfigurationAsync(AppDbContext db, int siteId)
|
||||||
|
{
|
||||||
|
var oldSources = await db.SapSourceDefinitions.Where(s => s.SiteId == siteId).ToListAsync();
|
||||||
|
var oldJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == siteId).ToListAsync();
|
||||||
|
var oldMappings = await db.SapFieldMappings.Where(m => m.SiteId == siteId).ToListAsync();
|
||||||
|
if (oldSources.Count > 0) db.SapSourceDefinitions.RemoveRange(oldSources);
|
||||||
|
if (oldJoins.Count > 0) db.SapJoinDefinitions.RemoveRange(oldJoins);
|
||||||
|
if (oldMappings.Count > 0) db.SapFieldMappings.RemoveRange(oldMappings);
|
||||||
|
|
||||||
|
if (IsSapSite())
|
||||||
|
{
|
||||||
|
NormalizeSapConfigCollections();
|
||||||
|
foreach (var source in _sapSources)
|
||||||
|
source.SiteId = siteId;
|
||||||
|
foreach (var join in _sapJoins)
|
||||||
|
join.SiteId = siteId;
|
||||||
|
foreach (var mapping in _sapMappings)
|
||||||
|
mapping.SiteId = siteId;
|
||||||
|
db.SapSourceDefinitions.AddRange(_sapSources);
|
||||||
|
db.SapJoinDefinitions.AddRange(_sapJoins);
|
||||||
|
db.SapFieldMappings.AddRange(_sapMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NormalizeSapConfigCollections()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _sapSources.Count; i++)
|
||||||
|
_sapSources[i].SortOrder = i;
|
||||||
|
for (var i = 0; i < _sapJoins.Count; i++)
|
||||||
|
_sapJoins[i].SortOrder = i;
|
||||||
|
for (var i = 0; i < _sapMappings.Count; i++)
|
||||||
|
_sapMappings[i].SortOrder = i;
|
||||||
|
|
||||||
|
var selectedPrimaryIndex = _sapSources.FindIndex(s => s.IsPrimary);
|
||||||
|
var primarySource = selectedPrimaryIndex >= 0 ? _sapSources[selectedPrimaryIndex] : _sapSources.FirstOrDefault();
|
||||||
|
foreach (var source in _sapSources)
|
||||||
|
source.IsPrimary = primarySource is not null && ReferenceEquals(source, primarySource);
|
||||||
|
if (_sapSources.Count > 0 && _sapSources.All(s => !s.IsPrimary))
|
||||||
|
_sapSources[0].IsPrimary = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,8 @@ public class AppDbContext : DbContext
|
|||||||
public DbSet<ExportSettings> ExportSettings => Set<ExportSettings>();
|
public DbSet<ExportSettings> ExportSettings => Set<ExportSettings>();
|
||||||
public DbSet<ExportLog> ExportLogs => Set<ExportLog>();
|
public DbSet<ExportLog> ExportLogs => Set<ExportLog>();
|
||||||
public DbSet<FieldTransformationRule> FieldTransformationRules => Set<FieldTransformationRule>();
|
public DbSet<FieldTransformationRule> FieldTransformationRules => Set<FieldTransformationRule>();
|
||||||
|
public DbSet<SapSourceDefinition> SapSourceDefinitions => Set<SapSourceDefinition>();
|
||||||
|
public DbSet<SapJoinDefinition> SapJoinDefinitions => Set<SapJoinDefinition>();
|
||||||
|
public DbSet<SapFieldMapping> SapFieldMappings => Set<SapFieldMapping>();
|
||||||
|
public DbSet<CentralSalesRecord> CentralSalesRecords => Set<CentralSalesRecord>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
public class CentralSalesRecord
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public DateTime StoredAtUtc { get; set; }
|
||||||
|
public int SiteId { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(SiteId))]
|
||||||
|
public Site? Site { get; set; }
|
||||||
|
|
||||||
|
public string SourceSystem { get; set; } = string.Empty;
|
||||||
|
public DateTime ExtractionDate { get; set; }
|
||||||
|
public string Tsc { get; set; } = string.Empty;
|
||||||
|
public string InvoiceNumber { get; set; } = string.Empty;
|
||||||
|
public int PositionOnInvoice { get; set; }
|
||||||
|
public string Material { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string ProductGroup { get; set; } = string.Empty;
|
||||||
|
public decimal Quantity { get; set; }
|
||||||
|
public string SupplierNumber { get; set; } = string.Empty;
|
||||||
|
public string SupplierName { get; set; } = string.Empty;
|
||||||
|
public string SupplierCountry { get; set; } = string.Empty;
|
||||||
|
public string CustomerNumber { get; set; } = string.Empty;
|
||||||
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
public string CustomerCountry { get; set; } = string.Empty;
|
||||||
|
public string CustomerIndustry { get; set; } = string.Empty;
|
||||||
|
public decimal StandardCost { get; set; }
|
||||||
|
public string StandardCostCurrency { get; set; } = string.Empty;
|
||||||
|
public string PurchaseOrderNumber { get; set; } = string.Empty;
|
||||||
|
public decimal SalesPriceValue { get; set; }
|
||||||
|
public string SalesCurrency { get; set; } = string.Empty;
|
||||||
|
public string Incoterms2020 { get; set; } = string.Empty;
|
||||||
|
public string SalesResponsibleEmployee { get; set; } = string.Empty;
|
||||||
|
public DateTime? InvoiceDate { get; set; }
|
||||||
|
public DateTime? OrderDate { get; set; }
|
||||||
|
public string Land { get; set; } = string.Empty;
|
||||||
|
public string DocumentType { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
namespace TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
public class ConfigTransferPackage
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
public DateTime ExportedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
public bool IncludesSecrets { get; set; }
|
||||||
|
public ConfigTransferSharePoint? SharePointConfig { get; set; }
|
||||||
|
public ConfigTransferExportSettings? ExportSettings { get; set; }
|
||||||
|
public List<ConfigTransferHanaServer> HanaServers { get; set; } = [];
|
||||||
|
public List<ConfigTransferSite> Sites { get; set; } = [];
|
||||||
|
public List<FieldTransformationRule> FieldTransformationRules { get; set; } = [];
|
||||||
|
public List<ConfigTransferSapSourceDefinition> SapSourceDefinitions { get; set; } = [];
|
||||||
|
public List<ConfigTransferSapJoinDefinition> SapJoinDefinitions { get; set; } = [];
|
||||||
|
public List<ConfigTransferSapFieldMapping> SapFieldMappings { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigTransferSharePoint
|
||||||
|
{
|
||||||
|
public string SiteUrl { get; set; } = string.Empty;
|
||||||
|
public string ExportFolder { get; set; } = string.Empty;
|
||||||
|
public string TenantId { get; set; } = string.Empty;
|
||||||
|
public string ClientId { get; set; } = string.Empty;
|
||||||
|
public string? ClientSecret { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigTransferExportSettings
|
||||||
|
{
|
||||||
|
public string DateFilter { get; set; } = "2025-01-01";
|
||||||
|
public int TimerHour { get; set; } = 3;
|
||||||
|
public int TimerMinute { get; set; }
|
||||||
|
public bool TimerEnabled { get; set; } = true;
|
||||||
|
public string? SapUsername { get; set; }
|
||||||
|
public string? SapPassword { get; set; }
|
||||||
|
public string? Bi1Username { get; set; }
|
||||||
|
public string? Bi1Password { get; set; }
|
||||||
|
public string? SageUsername { get; set; }
|
||||||
|
public string? SagePassword { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigTransferHanaServer
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = Guid.NewGuid().ToString("N");
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Host { get; set; } = string.Empty;
|
||||||
|
public int Port { get; set; } = 30015;
|
||||||
|
public string? Username { get; set; }
|
||||||
|
public string? Password { get; set; }
|
||||||
|
public string DatabaseName { get; set; } = string.Empty;
|
||||||
|
public bool UseSsl { get; set; }
|
||||||
|
public bool ValidateCertificate { get; set; }
|
||||||
|
public string AdditionalParams { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigTransferSite
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = Guid.NewGuid().ToString("N");
|
||||||
|
public string? HanaServerKey { get; set; }
|
||||||
|
public string Schema { get; set; } = string.Empty;
|
||||||
|
public string TSC { get; set; } = string.Empty;
|
||||||
|
public string Land { get; set; } = string.Empty;
|
||||||
|
public string SourceSystem { get; set; } = "SAP";
|
||||||
|
public string? UsernameOverride { get; set; }
|
||||||
|
public string? PasswordOverride { get; set; }
|
||||||
|
public string SapServiceUrl { get; set; } = string.Empty;
|
||||||
|
public string SapEntitySet { get; set; } = string.Empty;
|
||||||
|
public string SapEntitySetsCache { get; set; } = string.Empty;
|
||||||
|
public DateTime? SapEntitySetsRefreshedAtUtc { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigTransferSapSourceDefinition
|
||||||
|
{
|
||||||
|
public string SiteKey { get; set; } = string.Empty;
|
||||||
|
public string Alias { get; set; } = string.Empty;
|
||||||
|
public string EntitySet { get; set; } = string.Empty;
|
||||||
|
public bool IsPrimary { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigTransferSapJoinDefinition
|
||||||
|
{
|
||||||
|
public string SiteKey { get; set; } = string.Empty;
|
||||||
|
public string LeftAlias { get; set; } = string.Empty;
|
||||||
|
public string RightAlias { get; set; } = string.Empty;
|
||||||
|
public string LeftKeys { get; set; } = string.Empty;
|
||||||
|
public string RightKeys { get; set; } = string.Empty;
|
||||||
|
public string JoinType { get; set; } = "Left";
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigTransferSapFieldMapping
|
||||||
|
{
|
||||||
|
public string SiteKey { get; set; } = string.Empty;
|
||||||
|
public string TargetField { get; set; } = string.Empty;
|
||||||
|
public string SourceExpression { get; set; } = string.Empty;
|
||||||
|
public bool IsRequired { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
public class SapFieldMapping
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public int SiteId { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(SiteId))]
|
||||||
|
public Site? Site { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string TargetField { get; set; } = nameof(SalesRecord.Material);
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string SourceExpression { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool IsRequired { get; set; }
|
||||||
|
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
public class SapJoinDefinition
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public int SiteId { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(SiteId))]
|
||||||
|
public Site? Site { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string LeftAlias { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string RightAlias { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string LeftKeys { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string RightKeys { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string JoinType { get; set; } = "Left";
|
||||||
|
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
public class SapSourceDefinition
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public int SiteId { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(SiteId))]
|
||||||
|
public Site? Site { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Alias { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string EntitySet { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool IsPrimary { get; set; }
|
||||||
|
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ builder.Services.AddSingleton<IHanaQueryService, HanaQueryService>();
|
|||||||
builder.Services.AddSingleton<IExcelExportService, ExcelExportService>();
|
builder.Services.AddSingleton<IExcelExportService, ExcelExportService>();
|
||||||
builder.Services.AddSingleton<ISharePointUploadService, SharePointUploadService>();
|
builder.Services.AddSingleton<ISharePointUploadService, SharePointUploadService>();
|
||||||
builder.Services.AddSingleton<ISapGatewayService, SapGatewayService>();
|
builder.Services.AddSingleton<ISapGatewayService, SapGatewayService>();
|
||||||
|
builder.Services.AddSingleton<ISapCompositionService, SapCompositionService>();
|
||||||
builder.Services.AddSingleton<ITransformationStrategy, CopyTransformationStrategy>();
|
builder.Services.AddSingleton<ITransformationStrategy, CopyTransformationStrategy>();
|
||||||
builder.Services.AddSingleton<ITransformationStrategy, UppercaseTransformationStrategy>();
|
builder.Services.AddSingleton<ITransformationStrategy, UppercaseTransformationStrategy>();
|
||||||
builder.Services.AddSingleton<ITransformationStrategy, LowercaseTransformationStrategy>();
|
builder.Services.AddSingleton<ITransformationStrategy, LowercaseTransformationStrategy>();
|
||||||
@@ -28,6 +29,8 @@ builder.Services.AddSingleton<IRecordTransformationService, RecordTransformation
|
|||||||
builder.Services.AddSingleton<ISiteExportService, SiteExportService>();
|
builder.Services.AddSingleton<ISiteExportService, SiteExportService>();
|
||||||
builder.Services.AddSingleton<IConsolidatedExportService, ConsolidatedExportService>();
|
builder.Services.AddSingleton<IConsolidatedExportService, ConsolidatedExportService>();
|
||||||
builder.Services.AddSingleton<IExportLogService, ExportLogService>();
|
builder.Services.AddSingleton<IExportLogService, ExportLogService>();
|
||||||
|
builder.Services.AddSingleton<ICentralSalesRecordService, CentralSalesRecordService>();
|
||||||
|
builder.Services.AddSingleton<IConfigTransferService, ConfigTransferService>();
|
||||||
builder.Services.AddSingleton<IDatabaseInitializationService, DatabaseInitializationService>();
|
builder.Services.AddSingleton<IDatabaseInitializationService, DatabaseInitializationService>();
|
||||||
builder.Services.AddSingleton<ExportOrchestrationService>();
|
builder.Services.AddSingleton<ExportOrchestrationService>();
|
||||||
builder.Services.AddSingleton<TimerBackgroundService>();
|
builder.Services.AddSingleton<TimerBackgroundService>();
|
||||||
|
|||||||
@@ -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
|
public class ConsolidatedExportService : IConsolidatedExportService
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||||
|
private readonly ICentralSalesRecordService _centralSalesRecordService;
|
||||||
private readonly IExcelExportService _excelService;
|
private readonly IExcelExportService _excelService;
|
||||||
private readonly ISharePointUploadService _sharePointService;
|
private readonly ISharePointUploadService _sharePointService;
|
||||||
|
|
||||||
public ConsolidatedExportService(
|
public ConsolidatedExportService(
|
||||||
IDbContextFactory<AppDbContext> dbFactory,
|
IDbContextFactory<AppDbContext> dbFactory,
|
||||||
|
ICentralSalesRecordService centralSalesRecordService,
|
||||||
IExcelExportService excelService,
|
IExcelExportService excelService,
|
||||||
ISharePointUploadService sharePointService)
|
ISharePointUploadService sharePointService)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
|
_centralSalesRecordService = centralSalesRecordService;
|
||||||
_excelService = excelService;
|
_excelService = excelService;
|
||||||
_sharePointService = sharePointService;
|
_sharePointService = sharePointService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> ExportAsync(List<SalesRecord> records)
|
public async Task<string?> ExportAsync(List<SalesRecord> records)
|
||||||
{
|
{
|
||||||
if (records.Count == 0)
|
var consolidatedRecords = await _centralSalesRecordService.GetAllAsync();
|
||||||
|
if (consolidatedRecords.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
using var db = await _dbFactory.CreateDbContextAsync();
|
using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
@@ -31,7 +35,7 @@ public class ConsolidatedExportService : IConsolidatedExportService
|
|||||||
var consolidatedPath = _excelService.CreateConsolidatedExcelFile(
|
var consolidatedPath = _excelService.CreateConsolidatedExcelFile(
|
||||||
outputDir,
|
outputDir,
|
||||||
DateTime.UtcNow.Date,
|
DateTime.UtcNow.Date,
|
||||||
records
|
consolidatedRecords
|
||||||
.OrderBy(r => r.Land)
|
.OrderBy(r => r.Land)
|
||||||
.ThenBy(r => r.Tsc)
|
.ThenBy(r => r.Tsc)
|
||||||
.ThenByDescending(r => r.InvoiceDate ?? DateTime.MinValue)
|
.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", "SageUsername", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "ExportSettings", "SagePassword", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "ExportSettings", "SagePassword", "TEXT NOT NULL DEFAULT ''");
|
||||||
EnsureTransformationTable(db);
|
EnsureTransformationTable(db);
|
||||||
|
EnsureSapSourceTable(db);
|
||||||
|
EnsureSapJoinTable(db);
|
||||||
|
EnsureSapFieldMappingTable(db);
|
||||||
|
EnsureCentralSalesRecordTable(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EnsureSitesTableSupportsOptionalHanaServer(AppDbContext db)
|
private static void EnsureSitesTableSupportsOptionalHanaServer(AppDbContext db)
|
||||||
@@ -193,6 +197,115 @@ CREATE TABLE IF NOT EXISTS FieldTransformationRules (
|
|||||||
cmd.ExecuteNonQuery();
|
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)
|
private static void SeedIfEmpty(AppDbContext db)
|
||||||
{
|
{
|
||||||
if (db.HanaServers.Any())
|
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 IDbContextFactory<AppDbContext> _dbFactory;
|
||||||
private readonly IHanaQueryService _hanaService;
|
private readonly IHanaQueryService _hanaService;
|
||||||
private readonly ISapGatewayService _sapGatewayService;
|
private readonly ISapGatewayService _sapGatewayService;
|
||||||
|
private readonly ISapCompositionService _sapCompositionService;
|
||||||
private readonly IExcelExportService _excelService;
|
private readonly IExcelExportService _excelService;
|
||||||
private readonly ISharePointUploadService _sharePointService;
|
private readonly ISharePointUploadService _sharePointService;
|
||||||
private readonly IRecordTransformationService _transformationService;
|
private readonly IRecordTransformationService _transformationService;
|
||||||
|
private readonly ICentralSalesRecordService _centralSalesRecordService;
|
||||||
private readonly ILogger<SiteExportService> _logger;
|
private readonly ILogger<SiteExportService> _logger;
|
||||||
|
|
||||||
public SiteExportService(
|
public SiteExportService(
|
||||||
IDbContextFactory<AppDbContext> dbFactory,
|
IDbContextFactory<AppDbContext> dbFactory,
|
||||||
IHanaQueryService hanaService,
|
IHanaQueryService hanaService,
|
||||||
ISapGatewayService sapGatewayService,
|
ISapGatewayService sapGatewayService,
|
||||||
|
ISapCompositionService sapCompositionService,
|
||||||
IExcelExportService excelService,
|
IExcelExportService excelService,
|
||||||
ISharePointUploadService sharePointService,
|
ISharePointUploadService sharePointService,
|
||||||
IRecordTransformationService transformationService,
|
IRecordTransformationService transformationService,
|
||||||
|
ICentralSalesRecordService centralSalesRecordService,
|
||||||
ILogger<SiteExportService> logger)
|
ILogger<SiteExportService> logger)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_hanaService = hanaService;
|
_hanaService = hanaService;
|
||||||
_sapGatewayService = sapGatewayService;
|
_sapGatewayService = sapGatewayService;
|
||||||
|
_sapCompositionService = sapCompositionService;
|
||||||
_excelService = excelService;
|
_excelService = excelService;
|
||||||
_sharePointService = sharePointService;
|
_sharePointService = sharePointService;
|
||||||
_transformationService = transformationService;
|
_transformationService = transformationService;
|
||||||
|
_centralSalesRecordService = centralSalesRecordService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,14 +65,25 @@ public class SiteExportService : ISiteExportService
|
|||||||
var credentials = ResolveCredentials(site, settings, sourceSystem);
|
var credentials = ResolveCredentials(site, settings, sourceSystem);
|
||||||
if (string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
if (string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
||||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP Service URL.");
|
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP Service URL.");
|
||||||
if (string.IsNullOrWhiteSpace(site.SapEntitySet))
|
var sapSources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync();
|
||||||
throw new InvalidOperationException($"Standort '{site.Land}' hat kein SAP Entity Set ausgewählt.");
|
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...");
|
updateStatus?.Invoke("SAP Quellen laden...");
|
||||||
var rows = await _sapGatewayService.GetEntityRowsAsync(site.SapServiceUrl, site.SapEntitySet, credentials.Username, credentials.Password);
|
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...");
|
updateStatus?.Invoke("Excel erstellen...");
|
||||||
filePath = _excelService.CreateGenericExcelFile(outputDir, $"SAP_{site.TSC}_{site.SapEntitySet}", DateTime.UtcNow.Date, site.SapEntitySet, rows);
|
filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
||||||
log.RowCount = rows.Count;
|
log.RowCount = records.Count;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -87,6 +104,9 @@ public class SiteExportService : ISiteExportService
|
|||||||
log.RowCount = records.Count;
|
log.RowCount = records.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateStatus?.Invoke("Zentrale Tabelle aktualisieren...");
|
||||||
|
await _centralSalesRecordService.ReplaceForSiteAsync(site, records);
|
||||||
|
|
||||||
var fileName = Path.GetFileName(filePath);
|
var fileName = Path.GetFileName(filePath);
|
||||||
|
|
||||||
if (spConfig is not null &&
|
if (spConfig is not null &&
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
window.trafagDownload = {
|
||||||
|
saveTextFile: function (filename, content, contentType) {
|
||||||
|
const blob = new Blob([content], { type: contentType || "application/json;charset=utf-8" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user