Commit pending finance and Power BI work
This commit is contained in:
@@ -92,6 +92,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
||||
CompanyCurrency = r.CompanyCurrency,
|
||||
Incoterms2020 = r.Incoterms2020,
|
||||
SalesResponsibleEmployee = r.SalesResponsibleEmployee,
|
||||
PostingDate = r.PostingDate,
|
||||
InvoiceDate = r.InvoiceDate,
|
||||
OrderDate = r.OrderDate,
|
||||
Land = r.Land,
|
||||
@@ -167,7 +168,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
||||
CustomerNumber, CustomerName, CustomerCountry, CustomerIndustry, StandardCost,
|
||||
StandardCostCurrency, PurchaseOrderNumber, SalesPriceValue, SalesCurrency, Incoterms2020,
|
||||
DocumentCurrency, DocumentTotalForeignCurrency, DocumentTotalLocalCurrency, VatSumForeignCurrency,
|
||||
VatSumLocalCurrency, DocumentRate, CompanyCurrency, SalesResponsibleEmployee, InvoiceDate, OrderDate, Land, DocumentType
|
||||
VatSumLocalCurrency, DocumentRate, CompanyCurrency, SalesResponsibleEmployee, PostingDate, InvoiceDate, OrderDate, Land, DocumentType
|
||||
)
|
||||
VALUES (
|
||||
$storedAtUtc, $siteId, $sourceSystem, $extractionDate, $tsc, $documentEntry, $invoiceNumber, $positionOnInvoice,
|
||||
@@ -175,7 +176,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
||||
$customerNumber, $customerName, $customerCountry, $customerIndustry, $standardCost,
|
||||
$standardCostCurrency, $purchaseOrderNumber, $salesPriceValue, $salesCurrency, $incoterms2020,
|
||||
$documentCurrency, $documentTotalForeignCurrency, $documentTotalLocalCurrency, $vatSumForeignCurrency,
|
||||
$vatSumLocalCurrency, $documentRate, $companyCurrency, $salesResponsibleEmployee, $invoiceDate, $orderDate, $land, $documentType
|
||||
$vatSumLocalCurrency, $documentRate, $companyCurrency, $salesResponsibleEmployee, $postingDate, $invoiceDate, $orderDate, $land, $documentType
|
||||
);
|
||||
""";
|
||||
|
||||
@@ -212,6 +213,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
||||
command.Parameters.Add("$companyCurrency", SqliteType.Text);
|
||||
command.Parameters.Add("$incoterms2020", SqliteType.Text);
|
||||
command.Parameters.Add("$salesResponsibleEmployee", SqliteType.Text);
|
||||
command.Parameters.Add("$postingDate", SqliteType.Text);
|
||||
command.Parameters.Add("$invoiceDate", SqliteType.Text);
|
||||
command.Parameters.Add("$orderDate", SqliteType.Text);
|
||||
command.Parameters.Add("$land", SqliteType.Text);
|
||||
@@ -255,6 +257,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
||||
command.Parameters["$companyCurrency"].Value = record.CompanyCurrency ?? string.Empty;
|
||||
command.Parameters["$incoterms2020"].Value = record.Incoterms2020 ?? string.Empty;
|
||||
command.Parameters["$salesResponsibleEmployee"].Value = record.SalesResponsibleEmployee ?? string.Empty;
|
||||
command.Parameters["$postingDate"].Value = record.PostingDate?.ToString("O") ?? (object)DBNull.Value;
|
||||
command.Parameters["$invoiceDate"].Value = record.InvoiceDate?.ToString("O") ?? (object)DBNull.Value;
|
||||
command.Parameters["$orderDate"].Value = record.OrderDate?.ToString("O") ?? (object)DBNull.Value;
|
||||
command.Parameters["$land"].Value = record.Land ?? string.Empty;
|
||||
|
||||
@@ -413,6 +413,7 @@ public class ConfigTransferService : IConfigTransferService
|
||||
CompanyCurrency = record.CompanyCurrency,
|
||||
Incoterms2020 = record.Incoterms2020,
|
||||
SalesResponsibleEmployee = record.SalesResponsibleEmployee,
|
||||
PostingDate = record.PostingDate,
|
||||
InvoiceDate = record.InvoiceDate,
|
||||
OrderDate = record.OrderDate,
|
||||
Land = record.Land,
|
||||
|
||||
@@ -9,4 +9,5 @@ public sealed class DataSourceFetchContext
|
||||
public required ExportSettings Settings { get; init; }
|
||||
public SharePointConfig? SharePointConfig { get; init; }
|
||||
public Action<string>? UpdateStatus { get; init; }
|
||||
public int? PreferredImportYear { get; init; }
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
||||
string filePath;
|
||||
string? localOutputDirectory = null;
|
||||
string? sharePointUploadFolder = null;
|
||||
string? tempManualImportPath = null;
|
||||
var tempManualImportPaths = new List<string>();
|
||||
try
|
||||
{
|
||||
if (File.Exists(manualImportPath))
|
||||
@@ -59,19 +59,28 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
||||
siteId: site.Id, land: site.Land, details: manualImportPath);
|
||||
|
||||
var sharePointFileReference = manualImportPath;
|
||||
var sharePointFileReferences = new List<string>();
|
||||
if (LooksLikeSharePointFolderReference(manualImportPath))
|
||||
{
|
||||
var latestFile = await _sharePointService.ResolveLatestFileInFolderAsync(
|
||||
var files = await _sharePointService.ResolveManualImportFilesInFolderAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret,
|
||||
spConfig.SiteUrl, manualImportPath, site.TSC);
|
||||
sharePointFileReference = latestFile.FileReference;
|
||||
spConfig.SiteUrl, manualImportPath, site.TSC, context.PreferredImportYear);
|
||||
sharePointFileReferences.AddRange(files.Select(file => file.FileReference));
|
||||
sharePointFileReference = sharePointFileReferences.FirstOrDefault() ?? manualImportPath;
|
||||
await _appEventLogService.WriteAsync("Export", "Neueste SharePoint-Datei ausgewaehlt",
|
||||
siteId: site.Id, land: site.Land, details: sharePointFileReference);
|
||||
siteId: site.Id, land: site.Land, details: string.Join(" | ", sharePointFileReferences));
|
||||
}
|
||||
else
|
||||
{
|
||||
sharePointFileReferences.Add(sharePointFileReference);
|
||||
}
|
||||
|
||||
tempManualImportPath = await _sharePointService.DownloadToTempFileAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret,
|
||||
spConfig.SiteUrl, sharePointFileReference);
|
||||
foreach (var fileReference in sharePointFileReferences)
|
||||
{
|
||||
tempManualImportPaths.Add(await _sharePointService.DownloadToTempFileAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret,
|
||||
spConfig.SiteUrl, fileReference));
|
||||
}
|
||||
filePath = sharePointFileReference;
|
||||
sharePointUploadFolder = ResolveSharePointParentFolder(sharePointFileReference, spConfig.SiteUrl);
|
||||
}
|
||||
@@ -81,12 +90,14 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
||||
$"Die manuelle Excel-Datei wurde nicht gefunden: {manualImportPath}");
|
||||
}
|
||||
|
||||
var readPath = tempManualImportPath ?? filePath;
|
||||
context.UpdateStatus?.Invoke("Manuelle Excel lesen...");
|
||||
await _appEventLogService.WriteAsync("Export", "Manuelle Excel lesen",
|
||||
siteId: site.Id, land: site.Land, details: filePath);
|
||||
|
||||
var records = await _manualExcelImportService.ReadSalesRecordsAsync(readPath, site);
|
||||
var records = new List<SalesRecord>();
|
||||
var readPaths = tempManualImportPaths.Count > 0 ? tempManualImportPaths : [filePath];
|
||||
foreach (var readPath in readPaths)
|
||||
records.AddRange(await _manualExcelImportService.ReadSalesRecordsAsync(readPath, site));
|
||||
return new DataSourceFetchResult
|
||||
{
|
||||
Records = records,
|
||||
@@ -97,8 +108,11 @@ public sealed class ManualExcelDataSourceAdapter : IDataSourceAdapter
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tempManualImportPath) && File.Exists(tempManualImportPath))
|
||||
File.Delete(tempManualImportPath);
|
||||
foreach (var tempManualImportPath in tempManualImportPaths)
|
||||
{
|
||||
if (File.Exists(tempManualImportPath))
|
||||
File.Delete(tempManualImportPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public sealed class SapGatewayDataSourceAdapter : IDataSourceAdapter
|
||||
var effectiveSite = CloneSiteWithSapServiceUrl(site, sapServiceUrl);
|
||||
var records = await _sapCompositionService.BuildSalesRecordsAsync(
|
||||
effectiveSite, sapSources, sapJoins, sapMappings,
|
||||
credentials.Username, credentials.Password);
|
||||
credentials.Username, credentials.Password, context.PreferredImportYear);
|
||||
|
||||
return new DataSourceFetchResult { Records = records };
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ CREATE TABLE CentralSalesRecords (
|
||||
CompanyCurrency TEXT NOT NULL DEFAULT '',
|
||||
Incoterms2020 TEXT NOT NULL,
|
||||
SalesResponsibleEmployee TEXT NOT NULL,
|
||||
PostingDate TEXT NULL,
|
||||
InvoiceDate TEXT NULL,
|
||||
OrderDate TEXT NULL,
|
||||
Land TEXT NOT NULL,
|
||||
|
||||
@@ -51,6 +51,7 @@ public class DatabaseSchemaMaintenanceService : IDatabaseSchemaMaintenanceServic
|
||||
AddColumnIfMissing(db, "CentralSalesRecords", "VatSumLocalCurrency", "TEXT NOT NULL DEFAULT '0'");
|
||||
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentRate", "TEXT NOT NULL DEFAULT '0'");
|
||||
AddColumnIfMissing(db, "CentralSalesRecords", "CompanyCurrency", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "CentralSalesRecords", "PostingDate", "TEXT NULL");
|
||||
EnsureAppEventLogTable(db);
|
||||
}
|
||||
|
||||
|
||||
@@ -308,12 +308,105 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
}
|
||||
|
||||
if (string.Equals(existing.SourceSystem, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase) &&
|
||||
string.IsNullOrWhiteSpace(existing.ManualImportFilePath))
|
||||
(string.IsNullOrWhiteSpace(existing.ManualImportFilePath) ||
|
||||
existing.ManualImportFilePath.Contains("/England", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
existing.ManualImportFilePath = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform/Import/Finance/UK_B1";
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
db.SaveChanges();
|
||||
|
||||
if (CanSeedSiteDependentTable(db, "ManualExcelColumnMappings"))
|
||||
EnsureUkManualExcelMapping(db, existing.Id);
|
||||
}
|
||||
|
||||
private static bool CanSeedSiteDependentTable(AppDbContext db, string tableName)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var columns = DatabaseSchemaTools.GetTableColumns(conn, transaction: null, tableName);
|
||||
if (columns.Count == 0)
|
||||
return false;
|
||||
|
||||
return !DatabaseSchemaTools.TableReferences(conn, tableName, "Sites_old") &&
|
||||
!DatabaseSchemaTools.TableReferencesObsoleteTable(conn, tableName, "Sites");
|
||||
}
|
||||
|
||||
private static void EnsureUkManualExcelMapping(AppDbContext db, int siteId)
|
||||
{
|
||||
var mappings = new (string Target, string Source, bool Required)[]
|
||||
{
|
||||
(nameof(SalesRecord.Tsc), "TSC", false),
|
||||
(nameof(SalesRecord.Land), "Land", false),
|
||||
(nameof(SalesRecord.InvoiceNumber), "Invoice Number", true),
|
||||
(nameof(SalesRecord.PositionOnInvoice), "Position on invoice", false),
|
||||
(nameof(SalesRecord.Material), "Material", false),
|
||||
(nameof(SalesRecord.Name), "Name", false),
|
||||
(nameof(SalesRecord.ProductGroup), "Product Group", false),
|
||||
(nameof(SalesRecord.Quantity), "Quantity", true),
|
||||
(nameof(SalesRecord.CustomerNumber), "Customer number", false),
|
||||
(nameof(SalesRecord.CustomerName), "Customer name", false),
|
||||
(nameof(SalesRecord.CustomerCountry), "Customer country", false),
|
||||
(nameof(SalesRecord.SalesPriceValue), "=[Sales Price/Value]*[Quantity]", true),
|
||||
(nameof(SalesRecord.SalesCurrency), "=GBP", false),
|
||||
(nameof(SalesRecord.DocumentCurrency), "=GBP", false),
|
||||
(nameof(SalesRecord.CompanyCurrency), "=GBP", false),
|
||||
(nameof(SalesRecord.PostingDate), "invoice date", false),
|
||||
(nameof(SalesRecord.InvoiceDate), "invoice date", false),
|
||||
(nameof(SalesRecord.DocumentType), "=Manual Excel", false)
|
||||
};
|
||||
|
||||
var changed = false;
|
||||
for (var i = 0; i < mappings.Length; i++)
|
||||
{
|
||||
var mapping = db.ManualExcelColumnMappings
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefault(x => x.SiteId == siteId && x.TargetField == mappings[i].Target);
|
||||
|
||||
if (mapping is null)
|
||||
{
|
||||
db.ManualExcelColumnMappings.Add(new ManualExcelColumnMapping
|
||||
{
|
||||
SiteId = siteId,
|
||||
TargetField = mappings[i].Target,
|
||||
SourceHeader = mappings[i].Source,
|
||||
IsRequired = mappings[i].Required,
|
||||
IsActive = true,
|
||||
SortOrder = i
|
||||
});
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mapping.SourceHeader != mappings[i].Source)
|
||||
{
|
||||
mapping.SourceHeader = mappings[i].Source;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (mapping.IsRequired != mappings[i].Required)
|
||||
{
|
||||
mapping.IsRequired = mappings[i].Required;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!mapping.IsActive)
|
||||
{
|
||||
mapping.IsActive = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (mapping.SortOrder != i)
|
||||
{
|
||||
mapping.SortOrder = i;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
db.SaveChanges();
|
||||
}
|
||||
@@ -386,7 +479,7 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
{
|
||||
SiteId = siteId,
|
||||
Alias = "Z",
|
||||
EntitySet = "ZSCHWEIZSet",
|
||||
EntitySet = "FinanzdataSchweizOeSet",
|
||||
IsPrimary = true,
|
||||
IsActive = true,
|
||||
SortOrder = 0
|
||||
@@ -395,9 +488,9 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
}
|
||||
else
|
||||
{
|
||||
if (source.EntitySet != "ZSCHWEIZSet")
|
||||
if (source.EntitySet != "FinanzdataSchweizOeSet")
|
||||
{
|
||||
source.EntitySet = "ZSCHWEIZSet";
|
||||
source.EntitySet = "FinanzdataSchweizOeSet";
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -420,33 +513,52 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
}
|
||||
}
|
||||
|
||||
var obsoleteSources = db.SapSourceDefinitions
|
||||
.Where(x => x.SiteId == siteId && x.Alias != "Z")
|
||||
.ToList();
|
||||
foreach (var obsoleteSource in obsoleteSources)
|
||||
{
|
||||
if (obsoleteSource.IsActive)
|
||||
{
|
||||
obsoleteSource.IsActive = false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (obsoleteSource.IsPrimary)
|
||||
{
|
||||
obsoleteSource.IsPrimary = false;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
var mappings = new (string Target, string Source, bool Required)[]
|
||||
{
|
||||
(nameof(SalesRecord.Tsc), "Z.TSC", true),
|
||||
(nameof(SalesRecord.Land), "Z.LAND1", true),
|
||||
(nameof(SalesRecord.DocumentEntry), "Z.VBELN", false),
|
||||
(nameof(SalesRecord.InvoiceNumber), "Z.VBELN", true),
|
||||
(nameof(SalesRecord.PositionOnInvoice), "Z.POSNR", true),
|
||||
(nameof(SalesRecord.InvoiceDate), "Z.FKDAT", true),
|
||||
(nameof(SalesRecord.Material), "Z.MATNR", false),
|
||||
(nameof(SalesRecord.Name), "Z.ARKTX", false),
|
||||
(nameof(SalesRecord.ProductGroup), "Z.PRODH", false),
|
||||
(nameof(SalesRecord.Quantity), "Z.FKIMG", false),
|
||||
(nameof(SalesRecord.CustomerNumber), "Z.KUNNR", false),
|
||||
(nameof(SalesRecord.CustomerName), "Z.NAME1", false),
|
||||
(nameof(SalesRecord.CustomerCountry), "Z.CUSTOMER_LAND", false),
|
||||
(nameof(SalesRecord.Tsc), "Z.Tsc", true),
|
||||
(nameof(SalesRecord.Land), "Z.Land1", true),
|
||||
(nameof(SalesRecord.DocumentEntry), "Z.Vbeln", false),
|
||||
(nameof(SalesRecord.InvoiceNumber), "Z.Vbeln", true),
|
||||
(nameof(SalesRecord.PositionOnInvoice), "Z.Posnr", true),
|
||||
(nameof(SalesRecord.PostingDate), "Z.Fkdat", true),
|
||||
(nameof(SalesRecord.InvoiceDate), "Z.Fkdat", true),
|
||||
(nameof(SalesRecord.Material), "Z.Matnr", false),
|
||||
(nameof(SalesRecord.Name), "Z.Arktx", false),
|
||||
(nameof(SalesRecord.ProductGroup), "Z.Prodh", false),
|
||||
(nameof(SalesRecord.Quantity), "Z.Fkimg", false),
|
||||
(nameof(SalesRecord.CustomerNumber), "Z.Kunnr", false),
|
||||
(nameof(SalesRecord.CustomerName), "Z.Name1", false),
|
||||
(nameof(SalesRecord.CustomerCountry), "Z.CustomerLand", false),
|
||||
(nameof(SalesRecord.StandardCost), "=0", false),
|
||||
(nameof(SalesRecord.StandardCostCurrency), "Z.HWAER", false),
|
||||
(nameof(SalesRecord.SalesPriceValue), "Z.NETWR_HC", true),
|
||||
(nameof(SalesRecord.SalesCurrency), "Z.HWAER", true),
|
||||
(nameof(SalesRecord.DocumentCurrency), "Z.WAERK", false),
|
||||
(nameof(SalesRecord.DocumentTotalForeignCurrency), "Z.NETWR_DC", false),
|
||||
(nameof(SalesRecord.DocumentTotalLocalCurrency), "Z.NETWR_HC", false),
|
||||
(nameof(SalesRecord.VatSumForeignCurrency), "Z.TAX_DC", false),
|
||||
(nameof(SalesRecord.VatSumLocalCurrency), "Z.TAX_HC", false),
|
||||
(nameof(SalesRecord.DocumentRate), "Z.KURRF", false),
|
||||
(nameof(SalesRecord.CompanyCurrency), "Z.HWAER", true),
|
||||
(nameof(SalesRecord.DocumentType), "Z.FKART", false)
|
||||
(nameof(SalesRecord.StandardCostCurrency), "Z.Hwaer", false),
|
||||
(nameof(SalesRecord.SalesPriceValue), "Z.NetwrHc", true),
|
||||
(nameof(SalesRecord.SalesCurrency), "Z.Hwaer", true),
|
||||
(nameof(SalesRecord.DocumentCurrency), "Z.Waerk", false),
|
||||
(nameof(SalesRecord.DocumentTotalForeignCurrency), "Z.NetwrDc", false),
|
||||
(nameof(SalesRecord.DocumentTotalLocalCurrency), "Z.NetwrHc", false),
|
||||
(nameof(SalesRecord.VatSumForeignCurrency), "=0", false),
|
||||
(nameof(SalesRecord.VatSumLocalCurrency), "=0", false),
|
||||
(nameof(SalesRecord.DocumentRate), "Z.Kurrf", false),
|
||||
(nameof(SalesRecord.CompanyCurrency), "Z.Hwaer", true),
|
||||
(nameof(SalesRecord.DocumentType), "Z.Fkart", false)
|
||||
};
|
||||
|
||||
for (var i = 0; i < mappings.Length; i++)
|
||||
|
||||
@@ -70,6 +70,7 @@ public class ExcelExportService : IExcelExportService
|
||||
"Company Currency",
|
||||
"Incoterms 2020",
|
||||
"Sales responsible employee",
|
||||
"posting date",
|
||||
"invoice date",
|
||||
"order date",
|
||||
"Land",
|
||||
@@ -115,10 +116,11 @@ public class ExcelExportService : IExcelExportService
|
||||
ws.Cell(row, 28).Value = record.CompanyCurrency;
|
||||
ws.Cell(row, 29).Value = record.Incoterms2020;
|
||||
ws.Cell(row, 30).Value = record.SalesResponsibleEmployee;
|
||||
ws.Cell(row, 31).Value = record.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 32).Value = record.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 33).Value = record.Land;
|
||||
ws.Cell(row, 34).Value = record.DocumentType;
|
||||
ws.Cell(row, 31).Value = record.PostingDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 32).Value = record.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 33).Value = record.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 34).Value = record.Land;
|
||||
ws.Cell(row, 35).Value = record.DocumentType;
|
||||
row++;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,15 +81,15 @@ public class ExportOrchestrationService
|
||||
return await RunConsolidatedExportAsync();
|
||||
}
|
||||
|
||||
public async Task<SiteExportResult?> ExportSiteByIdAsync(int siteId)
|
||||
public async Task<SiteExportResult?> ExportSiteByIdAsync(int siteId, int? preferredImportYear = null)
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var site = await db.Sites.Include(s => s.HanaServer).FirstOrDefaultAsync(s => s.Id == siteId);
|
||||
if (site is null) return null;
|
||||
return await ExportSiteAsync(site);
|
||||
return await ExportSiteAsync(site, preferredImportYear);
|
||||
}
|
||||
|
||||
private async Task<SiteExportResult?> ExportSiteAsync(Site site)
|
||||
private async Task<SiteExportResult?> ExportSiteAsync(Site site, int? preferredImportYear = null)
|
||||
{
|
||||
SiteExportResult? result = null;
|
||||
|
||||
@@ -102,7 +102,7 @@ public class ExportOrchestrationService
|
||||
|
||||
try
|
||||
{
|
||||
result = await _siteExportService.ExportAsync(site, status => UpdateStatus(site.Id, status));
|
||||
result = await _siteExportService.ExportAsync(site, status => UpdateStatus(site.Id, status), preferredImportYear);
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -34,13 +34,16 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
|
||||
var centralRows = await db.CentralSalesRecords
|
||||
.AsNoTracking()
|
||||
.Where(r => (r.InvoiceDate ?? r.ExtractionDate).Year == year)
|
||||
.Where(r => (r.PostingDate ?? r.InvoiceDate ?? r.ExtractionDate).Year == year)
|
||||
.Select(r => new NetSalesActualSourceRow(
|
||||
r.Land,
|
||||
r.Tsc,
|
||||
r.DocumentEntry,
|
||||
r.InvoiceNumber,
|
||||
r.DocumentType,
|
||||
r.PostingDate,
|
||||
r.InvoiceDate,
|
||||
r.ExtractionDate,
|
||||
r.CustomerNumber,
|
||||
r.CustomerName,
|
||||
r.SalesCurrency,
|
||||
@@ -57,7 +60,7 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
.GroupBy(r => ResolveReferenceKey(r.Land, r.Tsc), StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
rows => BuildNetSalesActual(rows, budgetRatesToChf, intercompanyRules),
|
||||
g => BuildNetSalesActual(g.Key, g, budgetRatesToChf, intercompanyRules),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return financeReferences
|
||||
@@ -73,7 +76,9 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
groupedActuals.TryGetValue(reference.Key, out var actual);
|
||||
var referenceValue = reference.CheckValue ?? reference.LocalCurrencyValue;
|
||||
var selected = actual?.Candidates
|
||||
.OrderByDescending(candidate => candidate.Key == "NetDocumentLocalCurrency")
|
||||
.OrderByDescending(candidate => candidate.IsPreferred)
|
||||
.ThenByDescending(candidate => candidate.Key == "NetDocumentLocalCurrencyPosition")
|
||||
.ThenByDescending(candidate => candidate.Key == "NetDocumentLocalCurrencyDocument")
|
||||
.ThenByDescending(candidate => candidate.Key == "SalesPriceValue")
|
||||
.FirstOrDefault();
|
||||
var difference = selected is null || !referenceValue.HasValue ? (decimal?)null : selected.Value - referenceValue.Value;
|
||||
@@ -106,6 +111,7 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
Value = candidate.Value,
|
||||
IntercompanyValue = candidate.IntercompanyValue,
|
||||
ValueExcludingIntercompany = candidate.ValueExcludingIntercompany,
|
||||
IsPreferred = candidate.IsPreferred,
|
||||
Difference = referenceValue.HasValue ? candidate.Value - referenceValue.Value : null,
|
||||
DifferenceExcludingIntercompany = referenceValue.HasValue
|
||||
? candidate.ValueExcludingIntercompany - referenceValue.Value
|
||||
@@ -139,24 +145,30 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
}
|
||||
|
||||
private static NetSalesActual BuildNetSalesActual(
|
||||
string referenceKey,
|
||||
IEnumerable<NetSalesActualSourceRow> rows,
|
||||
IReadOnlyDictionary<string, decimal> budgetRatesToChf,
|
||||
IReadOnlyList<FinanceIntercompanyRule> intercompanyRules)
|
||||
{
|
||||
var rowList = rows.ToList();
|
||||
var houseCurrency = ResolveHouseCurrency(referenceKey, rowList);
|
||||
var documentRows = rowList
|
||||
.GroupBy(row => BuildDocumentKey(row.Tsc, row.DocumentType, row.DocumentEntry, row.InvoiceNumber), StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
var repeatedDocumentTotals = LooksLikeRepeatedDocumentTotals(rowList);
|
||||
|
||||
var salesPriceValue = rowList.Sum(row => row.SalesPriceValue);
|
||||
var salesPriceIntercompanyValue = rowList.Where(row => IsIntercompanyCustomer(row, intercompanyRules)).Sum(row => row.SalesPriceValue);
|
||||
var candidates = new List<NetSalesCandidate>
|
||||
{
|
||||
new(
|
||||
"SalesPriceValue",
|
||||
"Sales Price/Value",
|
||||
ResolveCurrencyLabel(rowList.Select(row => row.SalesCurrency)),
|
||||
rowList.Sum(row => row.SalesPriceValue),
|
||||
rowList.Where(row => IsIntercompanyCustomer(row, intercompanyRules)).Sum(row => row.SalesPriceValue))
|
||||
"Positions-Netto (Sales Price/Value)",
|
||||
houseCurrency,
|
||||
salesPriceValue,
|
||||
salesPriceIntercompanyValue,
|
||||
repeatedDocumentTotals && salesPriceValue != 0m)
|
||||
};
|
||||
|
||||
var netDocumentForeignCurrency = documentRows.Sum(row => row.DocumentTotalForeignCurrency - row.VatSumForeignCurrency);
|
||||
@@ -166,46 +178,100 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
"DocTotalFC - VatSumFC",
|
||||
ResolveCurrencyLabel(rowList.Select(row => row.DocumentCurrency)),
|
||||
netDocumentForeignCurrency,
|
||||
documentRows.Where(row => IsIntercompanyCustomer(row, intercompanyRules)).Sum(row => row.DocumentTotalForeignCurrency - row.VatSumForeignCurrency)));
|
||||
documentRows.Where(row => IsIntercompanyCustomer(row, intercompanyRules)).Sum(row => row.DocumentTotalForeignCurrency - row.VatSumForeignCurrency),
|
||||
false));
|
||||
|
||||
var positionNetDocumentLocalCurrency = rowList.Sum(row => row.DocumentTotalLocalCurrency - row.VatSumLocalCurrency);
|
||||
if (positionNetDocumentLocalCurrency != 0m)
|
||||
candidates.Add(new(
|
||||
"NetDocumentLocalCurrencyPosition",
|
||||
"Nettofakturawert Hauswaehrung pro Position",
|
||||
houseCurrency,
|
||||
positionNetDocumentLocalCurrency,
|
||||
rowList.Where(row => IsIntercompanyCustomer(row, intercompanyRules)).Sum(row => row.DocumentTotalLocalCurrency - row.VatSumLocalCurrency),
|
||||
!repeatedDocumentTotals));
|
||||
|
||||
var netDocumentLocalCurrency = documentRows.Sum(row => row.DocumentTotalLocalCurrency - row.VatSumLocalCurrency);
|
||||
if (netDocumentLocalCurrency != 0m)
|
||||
candidates.Add(new(
|
||||
"NetDocumentLocalCurrency",
|
||||
"Nettofakturawert Hauswaehrung",
|
||||
ResolveCurrencyLabel(rowList.Select(row => row.CompanyCurrency)),
|
||||
"NetDocumentLocalCurrencyDocument",
|
||||
"Nettofakturawert Hauswaehrung pro Beleg dedupliziert",
|
||||
houseCurrency,
|
||||
netDocumentLocalCurrency,
|
||||
documentRows.Where(row => IsIntercompanyCustomer(row, intercompanyRules)).Sum(row => row.DocumentTotalLocalCurrency - row.VatSumLocalCurrency)));
|
||||
documentRows.Where(row => IsIntercompanyCustomer(row, intercompanyRules)).Sum(row => row.DocumentTotalLocalCurrency - row.VatSumLocalCurrency),
|
||||
repeatedDocumentTotals && salesPriceValue == 0m));
|
||||
|
||||
var selectedNetRows = repeatedDocumentTotals ? documentRows : rowList;
|
||||
var budgetChf = selectedNetRows.Sum(row => ConvertHouseCurrencyNetToBudgetChf(houseCurrency, row, row.DocumentTotalLocalCurrency - row.VatSumLocalCurrency, budgetRatesToChf));
|
||||
|
||||
var budgetChf = documentRows.Sum(row => ConvertHouseCurrencyNetToBudgetChf(row, row.DocumentTotalLocalCurrency - row.VatSumLocalCurrency, budgetRatesToChf));
|
||||
if (budgetChf != 0m)
|
||||
candidates.Add(new(
|
||||
"NetDocumentLocalCurrencyBudgetChf",
|
||||
"Nettofakturawert Hauswaehrung -> CHF Budget 2025",
|
||||
$"Nettofakturawert Hauswaehrung -> CHF Budget 2025 ({(repeatedDocumentTotals ? "Beleg" : "Position")})",
|
||||
"CHF",
|
||||
budgetChf,
|
||||
documentRows.Where(row => IsIntercompanyCustomer(row, intercompanyRules)).Sum(row => ConvertHouseCurrencyNetToBudgetChf(row, row.DocumentTotalLocalCurrency - row.VatSumLocalCurrency, budgetRatesToChf))));
|
||||
selectedNetRows.Where(row => IsIntercompanyCustomer(row, intercompanyRules)).Sum(row => ConvertHouseCurrencyNetToBudgetChf(houseCurrency, row, row.DocumentTotalLocalCurrency - row.VatSumLocalCurrency, budgetRatesToChf)),
|
||||
false));
|
||||
|
||||
return new NetSalesActual
|
||||
{
|
||||
RowCount = rowList.Count,
|
||||
Currencies = string.Join(", ", rowList.Select(row => string.IsNullOrWhiteSpace(row.CompanyCurrency) ? row.SalesCurrency : row.CompanyCurrency)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)),
|
||||
Currencies = houseCurrency,
|
||||
Candidates = candidates
|
||||
};
|
||||
}
|
||||
|
||||
private static bool LooksLikeRepeatedDocumentTotals(IReadOnlyList<NetSalesActualSourceRow> rows)
|
||||
{
|
||||
var multiLineGroups = rows
|
||||
.GroupBy(row => BuildDocumentKey(row.Tsc, row.DocumentType, row.DocumentEntry, row.InvoiceNumber), StringComparer.OrdinalIgnoreCase)
|
||||
.Where(group => group.Count() > 1)
|
||||
.ToList();
|
||||
|
||||
if (multiLineGroups.Count == 0)
|
||||
return false;
|
||||
|
||||
var repeatedGroups = multiLineGroups.Count(group =>
|
||||
group.Select(row => Math.Round(row.DocumentTotalLocalCurrency - row.VatSumLocalCurrency, 2))
|
||||
.Distinct()
|
||||
.Count() == 1);
|
||||
|
||||
return repeatedGroups / (decimal)multiLineGroups.Count >= 0.8m;
|
||||
}
|
||||
|
||||
private static decimal ConvertHouseCurrencyNetToBudgetChf(
|
||||
string houseCurrency,
|
||||
NetSalesActualSourceRow row,
|
||||
decimal value,
|
||||
IReadOnlyDictionary<string, decimal> budgetRatesToChf)
|
||||
{
|
||||
var currency = (row.CompanyCurrency ?? string.Empty).Trim().ToUpperInvariant();
|
||||
var currency = !string.IsNullOrWhiteSpace(houseCurrency) && houseCurrency != "-"
|
||||
? houseCurrency.Trim().ToUpperInvariant()
|
||||
: (row.CompanyCurrency ?? string.Empty).Trim().ToUpperInvariant();
|
||||
return budgetRatesToChf.TryGetValue(currency, out var rate) ? value * rate : 0m;
|
||||
}
|
||||
|
||||
private static string ResolveHouseCurrency(string referenceKey, IReadOnlyList<NetSalesActualSourceRow> rows)
|
||||
{
|
||||
var configured = referenceKey.ToUpperInvariant() switch
|
||||
{
|
||||
"CH" => "CHF",
|
||||
"AT" => "EUR",
|
||||
"DE" => "EUR",
|
||||
"ES" => "EUR",
|
||||
"FR" => "EUR",
|
||||
"IN" => "INR",
|
||||
"IT" => "EUR",
|
||||
"UK" => "GBP",
|
||||
"US" => "USD",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
return string.IsNullOrWhiteSpace(configured)
|
||||
? ResolveCurrencyLabel(rows.Select(row => string.IsNullOrWhiteSpace(row.CompanyCurrency) ? row.SalesCurrency : row.CompanyCurrency))
|
||||
: configured;
|
||||
}
|
||||
|
||||
private static bool IsIntercompanyCustomer(NetSalesActualSourceRow row, IReadOnlyList<FinanceIntercompanyRule> rules)
|
||||
{
|
||||
var customerNumber = row.CustomerNumber?.Trim() ?? string.Empty;
|
||||
@@ -315,6 +381,7 @@ public sealed class NetSalesCandidateRow
|
||||
public decimal Value { get; set; }
|
||||
public decimal IntercompanyValue { get; set; }
|
||||
public decimal ValueExcludingIntercompany { get; set; }
|
||||
public bool IsPreferred { get; set; }
|
||||
public decimal? Difference { get; set; }
|
||||
public decimal? DifferenceExcludingIntercompany { get; set; }
|
||||
}
|
||||
@@ -332,6 +399,9 @@ internal sealed record NetSalesActualSourceRow(
|
||||
int DocumentEntry,
|
||||
string InvoiceNumber,
|
||||
string DocumentType,
|
||||
DateTime? PostingDate,
|
||||
DateTime? InvoiceDate,
|
||||
DateTime ExtractionDate,
|
||||
string CustomerNumber,
|
||||
string CustomerName,
|
||||
string SalesCurrency,
|
||||
@@ -343,7 +413,7 @@ internal sealed record NetSalesActualSourceRow(
|
||||
decimal VatSumForeignCurrency,
|
||||
decimal VatSumLocalCurrency);
|
||||
|
||||
internal sealed record NetSalesCandidate(string Key, string Label, string Currency, decimal Value, decimal IntercompanyValue)
|
||||
internal sealed record NetSalesCandidate(string Key, string Label, string Currency, decimal Value, decimal IntercompanyValue, bool IsPreferred)
|
||||
{
|
||||
public decimal ValueExcludingIntercompany => Value - IntercompanyValue;
|
||||
}
|
||||
|
||||
@@ -267,6 +267,7 @@ public class HanaQueryService : IHanaQueryService
|
||||
DocumentEntry = Convert.ToInt32(reader["document_entry"]),
|
||||
InvoiceNumber = reader["invoice_number"]?.ToString() ?? string.Empty,
|
||||
PositionOnInvoice = Convert.ToInt32(reader["invoice_position"]),
|
||||
PostingDate = reader.IsDBNull(reader.GetOrdinal("posting_date")) ? null : reader.GetDateTime(reader.GetOrdinal("posting_date")),
|
||||
InvoiceDate = reader.IsDBNull(reader.GetOrdinal("invoice_date")) ? null : reader.GetDateTime(reader.GetOrdinal("invoice_date")),
|
||||
Material = reader["material"]?.ToString() ?? string.Empty,
|
||||
Name = reader["material_name"]?.ToString() ?? string.Empty,
|
||||
@@ -373,7 +374,8 @@ SELECT
|
||||
h.""DocEntry"" AS document_entry,
|
||||
h.""DocNum"" AS invoice_number,
|
||||
p.""LineNum"" AS invoice_position,
|
||||
h.""DocDate"" AS invoice_date,
|
||||
h.""DocDate"" AS posting_date,
|
||||
h.""TaxDate"" AS invoice_date,
|
||||
p.""ItemCode"" AS material,
|
||||
p.""Dscription"" AS material_name,
|
||||
COALESCE(grp.""ItmsGrpNam"", '') AS product_group,
|
||||
@@ -391,7 +393,7 @@ SELECT
|
||||
THEN CAST(p.""BaseRef"" AS NVARCHAR(20))
|
||||
ELSE '' END AS purchase_order_number,
|
||||
p.""LineTotal"" AS sales_value,
|
||||
COALESCE(p.""Currency"", h.""DocCur"") AS sales_currency,
|
||||
COALESCE(adm.""MainCurncy"", '') AS sales_currency,
|
||||
COALESCE(h.""DocCur"", '') AS document_currency,
|
||||
COALESCE(h.""DocTotalFC"", 0) AS document_total_fc,
|
||||
COALESCE(h.""DocTotal"", 0) AS document_total_lc,
|
||||
@@ -434,7 +436,8 @@ SELECT
|
||||
h.""DocEntry"" AS document_entry,
|
||||
h.""DocNum"" AS invoice_number,
|
||||
p.""LineNum"" AS invoice_position,
|
||||
h.""DocDate"" AS invoice_date,
|
||||
h.""DocDate"" AS posting_date,
|
||||
h.""TaxDate"" AS invoice_date,
|
||||
p.""ItemCode"" AS material,
|
||||
p.""Dscription"" AS material_name,
|
||||
COALESCE(grp.""ItmsGrpNam"", '') AS product_group,
|
||||
@@ -450,7 +453,7 @@ SELECT
|
||||
COALESCE(adm.""MainCurncy"", '') AS standard_cost_currency,
|
||||
'' AS purchase_order_number,
|
||||
p.""LineTotal"" * -1 AS sales_value,
|
||||
COALESCE(p.""Currency"", h.""DocCur"") AS sales_currency,
|
||||
COALESCE(adm.""MainCurncy"", '') AS sales_currency,
|
||||
COALESCE(h.""DocCur"", '') AS document_currency,
|
||||
COALESCE(h.""DocTotalFC"", 0) * -1 AS document_total_fc,
|
||||
COALESCE(h.""DocTotal"", 0) * -1 AS document_total_lc,
|
||||
|
||||
@@ -11,5 +11,6 @@ public interface ISapCompositionService
|
||||
IReadOnlyList<SapFieldMapping> mappings,
|
||||
string username,
|
||||
string password,
|
||||
int? preferredYear = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@ public interface ISapGatewayService
|
||||
Task TestConnectionAsync(string serviceUrl, string username, string password, CancellationToken cancellationToken = default);
|
||||
Task<List<string>> GetEntitySetsAsync(string serviceUrl, string username, string password, CancellationToken cancellationToken = default);
|
||||
Task<List<string>> GetEntityFieldNamesAsync(string serviceUrl, string entitySet, string username, string password, CancellationToken cancellationToken = default);
|
||||
Task<List<Dictionary<string, object?>>> GetEntityRowsAsync(string serviceUrl, string entitySet, string username, string password, CancellationToken cancellationToken = default);
|
||||
Task<List<Dictionary<string, object?>>> GetEntityRowsAsync(string serviceUrl, string entitySet, string username, string password, string? filter = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ public interface ISharePointUploadService
|
||||
{
|
||||
Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath);
|
||||
Task<string> DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference);
|
||||
Task<SharePointFileReference> ResolveLatestFileInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc);
|
||||
Task<SharePointFileReference> ResolveLatestFileInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null);
|
||||
Task<IReadOnlyList<SharePointFileReference>> ResolveManualImportFilesInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null);
|
||||
Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface ISiteExportService
|
||||
{
|
||||
Task<SiteExportResult> ExportAsync(Site site, Action<string>? updateStatus = null);
|
||||
Task<SiteExportResult> ExportAsync(Site site, Action<string>? updateStatus = null, int? preferredImportYear = null);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,11 @@ public class ManualExcelImportService : IManualExcelImportService
|
||||
["companycurrency"] = nameof(SalesRecord.CompanyCurrency),
|
||||
["incoterms2020"] = nameof(SalesRecord.Incoterms2020),
|
||||
["salesresponsibleemployee"] = nameof(SalesRecord.SalesResponsibleEmployee),
|
||||
["postingdate"] = nameof(SalesRecord.PostingDate),
|
||||
["buchungsdatum"] = nameof(SalesRecord.PostingDate),
|
||||
["lineregistrationdate"] = nameof(SalesRecord.PostingDate),
|
||||
["invoicedate"] = nameof(SalesRecord.InvoiceDate),
|
||||
["fakturadatum"] = nameof(SalesRecord.InvoiceDate),
|
||||
["orderdate"] = nameof(SalesRecord.OrderDate),
|
||||
["land"] = nameof(SalesRecord.Land),
|
||||
["documenttype"] = nameof(SalesRecord.DocumentType)
|
||||
@@ -180,6 +184,7 @@ public class ManualExcelImportService : IManualExcelImportService
|
||||
CompanyCurrency = ReadString(headerIndexes, fields, nameof(SalesRecord.CompanyCurrency)),
|
||||
Incoterms2020 = ReadString(headerIndexes, fields, nameof(SalesRecord.Incoterms2020)),
|
||||
SalesResponsibleEmployee = ReadString(headerIndexes, fields, nameof(SalesRecord.SalesResponsibleEmployee)),
|
||||
PostingDate = ReadDate(headerIndexes, fields, nameof(SalesRecord.PostingDate)),
|
||||
InvoiceDate = ReadDate(headerIndexes, fields, nameof(SalesRecord.InvoiceDate)),
|
||||
OrderDate = ReadDate(headerIndexes, fields, nameof(SalesRecord.OrderDate)),
|
||||
Land = ReadString(headerIndexes, fields, nameof(SalesRecord.Land), site.Land),
|
||||
@@ -290,6 +295,7 @@ public class ManualExcelImportService : IManualExcelImportService
|
||||
CompanyCurrency = ReadString(headerIndexes, row, nameof(SalesRecord.CompanyCurrency)),
|
||||
Incoterms2020 = ReadString(headerIndexes, row, nameof(SalesRecord.Incoterms2020)),
|
||||
SalesResponsibleEmployee = ReadString(headerIndexes, row, nameof(SalesRecord.SalesResponsibleEmployee)),
|
||||
PostingDate = ReadDate(headerIndexes, row, nameof(SalesRecord.PostingDate)),
|
||||
InvoiceDate = ReadDate(headerIndexes, row, nameof(SalesRecord.InvoiceDate)),
|
||||
OrderDate = ReadDate(headerIndexes, row, nameof(SalesRecord.OrderDate)),
|
||||
Land = ReadString(headerIndexes, row, nameof(SalesRecord.Land), site.Land),
|
||||
@@ -442,7 +448,9 @@ public class ManualExcelImportService : IManualExcelImportService
|
||||
{
|
||||
var trimmed = sourceHeader.Trim();
|
||||
if (trimmed.StartsWith('='))
|
||||
return trimmed[1..];
|
||||
return EvaluateMappedExpression(trimmed[1..], headerIndexes, header => TryResolveHeaderIndex(headerIndexes, header, out var index)
|
||||
? row.Cell(index).GetFormattedString().Trim()
|
||||
: null);
|
||||
|
||||
return TryResolveHeaderIndex(headerIndexes, trimmed, out var index)
|
||||
? row.Cell(index).GetFormattedString().Trim()
|
||||
@@ -453,13 +461,41 @@ public class ManualExcelImportService : IManualExcelImportService
|
||||
{
|
||||
var trimmed = sourceHeader.Trim();
|
||||
if (trimmed.StartsWith('='))
|
||||
return trimmed[1..];
|
||||
return EvaluateMappedExpression(trimmed[1..], headerIndexes, header => TryResolveHeaderIndex(headerIndexes, header, out var index) && index < fields.Length
|
||||
? fields[index].Trim()
|
||||
: null);
|
||||
|
||||
return TryResolveHeaderIndex(headerIndexes, trimmed, out var index) && index < fields.Length
|
||||
? fields[index].Trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
private static object? EvaluateMappedExpression(string expression, Dictionary<string, int> headerIndexes, Func<string, string?> readHeader)
|
||||
{
|
||||
if (!expression.Contains('[') || !expression.Contains(']'))
|
||||
return expression;
|
||||
|
||||
var parts = expression.Split('*', 2, StringSplitOptions.TrimEntries);
|
||||
if (parts.Length != 2)
|
||||
return expression;
|
||||
|
||||
var left = ResolveExpressionOperand(parts[0], headerIndexes, readHeader);
|
||||
var right = ResolveExpressionOperand(parts[1], headerIndexes, readHeader);
|
||||
return left * right;
|
||||
}
|
||||
|
||||
private static decimal ResolveExpressionOperand(string operand, Dictionary<string, int> headerIndexes, Func<string, string?> readHeader)
|
||||
{
|
||||
var trimmed = operand.Trim();
|
||||
if (trimmed.StartsWith('[') && trimmed.EndsWith(']'))
|
||||
{
|
||||
var header = trimmed[1..^1].Trim();
|
||||
return ParseDecimal(readHeader(header) ?? string.Empty);
|
||||
}
|
||||
|
||||
return ParseDecimal(trimmed);
|
||||
}
|
||||
|
||||
private static bool IsRowEmpty(IXLRangeRow row)
|
||||
=> row.CellsUsed().All(cell => string.IsNullOrWhiteSpace(cell.GetFormattedString()));
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ public class SapCompositionService : ISapCompositionService
|
||||
IReadOnlyList<SapFieldMapping> mappings,
|
||||
string username,
|
||||
string password,
|
||||
int? preferredYear = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
||||
@@ -44,7 +45,8 @@ public class SapCompositionService : ISapCompositionService
|
||||
{
|
||||
await _appEventLogService.WriteDebugAsync("SAP", "Quelle wird gelesen", site.Id, site.Land,
|
||||
$"Alias={source.Alias} | EntitySet={source.EntitySet}");
|
||||
var rows = await _sapGatewayService.GetEntityRowsAsync(site.SapServiceUrl, source.EntitySet, username, password, cancellationToken);
|
||||
var filter = BuildODataYearFilter(source.EntitySet, preferredYear);
|
||||
var rows = await _sapGatewayService.GetEntityRowsAsync(site.SapServiceUrl, source.EntitySet, username, password, filter, cancellationToken);
|
||||
sourceRows[source.Alias] = rows;
|
||||
await _appEventLogService.WriteDebugAsync("SAP", "Quelle gelesen", site.Id, site.Land,
|
||||
$"Alias={source.Alias} | EntitySet={source.EntitySet} | Zeilen={rows.Count}");
|
||||
@@ -57,4 +59,14 @@ public class SapCompositionService : ISapCompositionService
|
||||
$"SalesRecords={result.Count} | Mappings={mappings.Count(x => x.IsActive)}");
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? BuildODataYearFilter(string entitySet, int? preferredYear)
|
||||
{
|
||||
if (preferredYear is null)
|
||||
return null;
|
||||
|
||||
return string.Equals(entitySet, "FinanzdataSchweizOeSet", StringComparison.OrdinalIgnoreCase)
|
||||
? $"Gjahr eq '{preferredYear.Value}'"
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,10 +87,13 @@ public class SapGatewayService : ISapGatewayService
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<Dictionary<string, object?>>> GetEntityRowsAsync(string serviceUrl, string entitySet, string username, string password, CancellationToken cancellationToken = default)
|
||||
public async Task<List<Dictionary<string, object?>>> GetEntityRowsAsync(string serviceUrl, string entitySet, string username, string password, string? filter = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var client = CreateClient(username, password);
|
||||
var requestUrl = $"{BuildServiceUri(serviceUrl)}{entitySet}?$format=json";
|
||||
var query = string.IsNullOrWhiteSpace(filter)
|
||||
? "$format=json"
|
||||
: $"$format=json&$filter={Uri.EscapeDataString(filter)}";
|
||||
var requestUrl = $"{BuildServiceUri(serviceUrl)}{entitySet}?{query}";
|
||||
await _appEventLogService.WriteAsync("SAP", "Entity-Read gestartet", details: requestUrl);
|
||||
using var response = await client.GetAsync(requestUrl, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
@@ -91,7 +91,22 @@ public class SharePointUploadService : ISharePointUploadService
|
||||
string clientSecret,
|
||||
string siteUrl,
|
||||
string folderReference,
|
||||
string siteTsc)
|
||||
string siteTsc,
|
||||
int? preferredYear = null)
|
||||
{
|
||||
var files = await ResolveManualImportFilesInFolderAsync(
|
||||
tenantId, clientId, clientSecret, siteUrl, folderReference, siteTsc, preferredYear);
|
||||
return files.First();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SharePointFileReference>> ResolveManualImportFilesInFolderAsync(
|
||||
string tenantId,
|
||||
string clientId,
|
||||
string clientSecret,
|
||||
string siteUrl,
|
||||
string folderReference,
|
||||
string siteTsc,
|
||||
int? preferredYear = null)
|
||||
{
|
||||
var normalizedTenantId = Normalize(tenantId);
|
||||
var normalizedClientId = Normalize(clientId);
|
||||
@@ -119,18 +134,56 @@ public class SharePointUploadService : ISharePointUploadService
|
||||
|
||||
var folderPath = ResolveRemotePath(normalizedReference, siteUri);
|
||||
var children = await graphClient.Drives[drive.Id].Root.ItemWithPath(folderPath).Children.GetAsync();
|
||||
var candidates = children?.Value?
|
||||
var allCandidates = children?.Value?
|
||||
.Where(item => item.File is not null)
|
||||
.Where(item => IsSupportedManualImportFile(item.Name))
|
||||
.Where(item => MatchesTsc(item.Name, normalizedTsc))
|
||||
.Select(item => new
|
||||
{
|
||||
Item = item,
|
||||
FileDate = TryParseDatedSiteFileName(item.Name, normalizedTsc, out var fileDate) ? fileDate : (DateTime?)null
|
||||
FileDate = TryParseDatedSiteFileName(item.Name, normalizedTsc, out var fileDate) ? fileDate : (DateTime?)null,
|
||||
AnnualYear = TryParseAnnualSiteFileName(item.Name, normalizedTsc, out var annualYear) ? annualYear : (int?)null,
|
||||
SnapshotDate = TryParseSnapshotDate(item.Name, out var snapshotDate) ? snapshotDate : (DateTime?)null
|
||||
})
|
||||
.ToList() ?? [];
|
||||
|
||||
if (preferredYear is not null)
|
||||
{
|
||||
var annual = allCandidates
|
||||
.Where(x => x.AnnualYear == preferredYear.Value)
|
||||
.OrderByDescending(x => x.SnapshotDate ?? x.Item.LastModifiedDateTime?.UtcDateTime ?? DateTime.MinValue)
|
||||
.FirstOrDefault()
|
||||
?? throw new InvalidOperationException(
|
||||
$"Im SharePoint-Ordner '{folderPath}' wurde keine Jahresdatei fuer '{normalizedTsc}' und Jahr {preferredYear.Value} gefunden.");
|
||||
|
||||
var references = new List<SharePointFileReference>
|
||||
{
|
||||
new(string.Join("/", folderPath.Trim('/'), annual.Item.Name).Trim('/'), annual.Item.LastModifiedDateTime)
|
||||
};
|
||||
|
||||
if (preferredYear.Value >= DateTime.Today.Year)
|
||||
{
|
||||
var baseDate = annual.SnapshotDate
|
||||
?? annual.Item.LastModifiedDateTime?.UtcDateTime.Date
|
||||
?? new DateTime(preferredYear.Value, 1, 1);
|
||||
|
||||
references.AddRange(allCandidates
|
||||
.Where(x => x.FileDate is not null)
|
||||
.Where(x => x.FileDate!.Value.Year == preferredYear.Value)
|
||||
.Where(x => x.FileDate!.Value.Date > baseDate.Date)
|
||||
.OrderBy(x => x.FileDate)
|
||||
.Select(x => new SharePointFileReference(
|
||||
string.Join("/", folderPath.Trim('/'), x.Item.Name).Trim('/'),
|
||||
x.Item.LastModifiedDateTime)));
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
var candidates = allCandidates
|
||||
.OrderByDescending(x => x.FileDate ?? x.Item.LastModifiedDateTime?.UtcDateTime ?? DateTime.MinValue)
|
||||
.ThenByDescending(x => x.Item.LastModifiedDateTime?.UtcDateTime ?? DateTime.MinValue)
|
||||
.ToList() ?? [];
|
||||
.ToList();
|
||||
|
||||
var selected = candidates.FirstOrDefault()
|
||||
?? throw new InvalidOperationException(
|
||||
@@ -138,9 +191,12 @@ public class SharePointUploadService : ISharePointUploadService
|
||||
? $"Im SharePoint-Ordner '{folderPath}' wurde keine Excel-/CSV-Datei gefunden."
|
||||
: $"Im SharePoint-Ordner '{folderPath}' wurde keine Excel-/CSV-Datei fuer '{normalizedTsc}' gefunden.");
|
||||
|
||||
return new SharePointFileReference(
|
||||
string.Join("/", folderPath.Trim('/'), selected.Item.Name).Trim('/'),
|
||||
selected.Item.LastModifiedDateTime);
|
||||
return
|
||||
[
|
||||
new SharePointFileReference(
|
||||
string.Join("/", folderPath.Trim('/'), selected.Item.Name).Trim('/'),
|
||||
selected.Item.LastModifiedDateTime)
|
||||
];
|
||||
}
|
||||
|
||||
public async Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl)
|
||||
@@ -217,7 +273,8 @@ public class SharePointUploadService : ISharePointUploadService
|
||||
return true;
|
||||
|
||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName ?? string.Empty);
|
||||
return nameWithoutExtension.EndsWith($"_{normalizedTsc}", StringComparison.OrdinalIgnoreCase);
|
||||
return nameWithoutExtension.EndsWith($"_{normalizedTsc}", StringComparison.OrdinalIgnoreCase) ||
|
||||
Regex.IsMatch(nameWithoutExtension, $@"(^|[^A-Z0-9]){Regex.Escape(normalizedTsc)}([^A-Z0-9]|$)", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
private static bool TryParseDatedSiteFileName(string? fileName, string normalizedTsc, out DateTime fileDate)
|
||||
@@ -239,6 +296,33 @@ public class SharePointUploadService : ISharePointUploadService
|
||||
out fileDate);
|
||||
}
|
||||
|
||||
private static bool TryParseAnnualSiteFileName(string? fileName, string normalizedTsc, out int year)
|
||||
{
|
||||
year = default;
|
||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName ?? string.Empty);
|
||||
if (!Regex.IsMatch(nameWithoutExtension, $@"(^|[^A-Z0-9]){Regex.Escape(normalizedTsc)}([^A-Z0-9]|$)", RegexOptions.IgnoreCase))
|
||||
return false;
|
||||
if (TryParseDatedSiteFileName(fileName, normalizedTsc, out _))
|
||||
return false;
|
||||
|
||||
var match = Regex.Match(nameWithoutExtension, @"(?<!\d)(20\d{2})(?!\d)");
|
||||
return match.Success && int.TryParse(match.Groups[1].Value, CultureInfo.InvariantCulture, out year);
|
||||
}
|
||||
|
||||
private static bool TryParseSnapshotDate(string? fileName, out DateTime snapshotDate)
|
||||
{
|
||||
snapshotDate = default;
|
||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName ?? string.Empty);
|
||||
var match = Regex.Match(nameWithoutExtension, @"(?<!\d)(?<date>20\d{2}[-_.]\d{2}[-_.]\d{2})(?!\d)");
|
||||
return match.Success &&
|
||||
DateTime.TryParseExact(
|
||||
match.Groups["date"].Value.Replace('_', '-').Replace('.', '-'),
|
||||
"yyyy-MM-dd",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out snapshotDate);
|
||||
}
|
||||
|
||||
private static string BuildInputPreview(string tenantId, string clientId, string clientSecret, string siteUrl)
|
||||
{
|
||||
var maskedSecret = string.IsNullOrEmpty(clientSecret)
|
||||
|
||||
@@ -37,7 +37,7 @@ public class SiteExportService : ISiteExportService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SiteExportResult> ExportAsync(Site site, Action<string>? updateStatus = null)
|
||||
public async Task<SiteExportResult> ExportAsync(Site site, Action<string>? updateStatus = null, int? preferredImportYear = null)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var log = new ExportLog
|
||||
@@ -63,7 +63,8 @@ public class SiteExportService : ISiteExportService
|
||||
SourceDefinition = sourceDefinition,
|
||||
Settings = settings,
|
||||
SharePointConfig = spConfig,
|
||||
UpdateStatus = updateStatus
|
||||
UpdateStatus = updateStatus,
|
||||
PreferredImportYear = preferredImportYear
|
||||
});
|
||||
|
||||
var records = fetchResult.Records;
|
||||
|
||||
Reference in New Issue
Block a user