From 673bba7298ba3d67d3f322ea1e67281120b9448d Mon Sep 17 00:00:00 2001 From: Metacube Date: Thu, 9 Apr 2026 15:47:55 +0200 Subject: [PATCH] Add Trafag SAP HANA to Excel SharePoint exporter console app --- TrafagSalesExporter/Models/SalesRecord.cs | 31 ++++ TrafagSalesExporter/Program.cs | 100 +++++++++++ .../Services/ExcelExportService.cs | 89 ++++++++++ .../Services/HanaQueryService.cs | 167 ++++++++++++++++++ .../Services/SharePointUploadService.cs | 44 +++++ .../TrafagSalesExporter.csproj | 24 +++ TrafagSalesExporter/appsettings.json | 30 ++++ 7 files changed, 485 insertions(+) create mode 100644 TrafagSalesExporter/Models/SalesRecord.cs create mode 100644 TrafagSalesExporter/Program.cs create mode 100644 TrafagSalesExporter/Services/ExcelExportService.cs create mode 100644 TrafagSalesExporter/Services/HanaQueryService.cs create mode 100644 TrafagSalesExporter/Services/SharePointUploadService.cs create mode 100644 TrafagSalesExporter/TrafagSalesExporter.csproj create mode 100644 TrafagSalesExporter/appsettings.json diff --git a/TrafagSalesExporter/Models/SalesRecord.cs b/TrafagSalesExporter/Models/SalesRecord.cs new file mode 100644 index 0000000..1871250 --- /dev/null +++ b/TrafagSalesExporter/Models/SalesRecord.cs @@ -0,0 +1,31 @@ +namespace TrafagSalesExporter.Models; + +public class SalesRecord +{ + 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; +} diff --git a/TrafagSalesExporter/Program.cs b/TrafagSalesExporter/Program.cs new file mode 100644 index 0000000..5ca6023 --- /dev/null +++ b/TrafagSalesExporter/Program.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.Configuration; +using TrafagSalesExporter.Services; + +namespace TrafagSalesExporter; + +internal static class Program +{ + private static async Task Main() + { + var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) + .Build(); + + var appConfig = config.Get() ?? throw new InvalidOperationException("Konfiguration konnte nicht geladen werden."); + + var hanaService = new HanaQueryService(); + var excelService = new ExcelExportService(); + var sharePointService = new SharePointUploadService( + appConfig.SharePoint.TenantId, + appConfig.SharePoint.ClientId, + appConfig.SharePoint.ClientSecret, + appConfig.SharePoint.SiteUrl, + appConfig.SharePoint.ExportFolder); + + var outputDir = Path.Combine(AppContext.BaseDirectory, "output"); + + foreach (var site in appConfig.Sites) + { + try + { + Log($"Starte Standort: {site.Land} ({site.Schema})"); + + if (!appConfig.HanaServers.TryGetValue(site.Server, out var serverConfig)) + { + throw new InvalidOperationException($"HANA Server-Konfiguration '{site.Server}' nicht gefunden."); + } + + var records = hanaService.GetSalesRecords( + serverConfig.Host, + serverConfig.Port, + serverConfig.Username, + serverConfig.Password, + site.Schema, + site.TSC, + site.Land); + + var filePath = excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records); + Log($"Excel erzeugt: {filePath}"); + + await sharePointService.UploadAsync(site.Land, filePath); + Log($"Upload abgeschlossen: {site.Land}"); + } + catch (Exception ex) + { + Log($"Fehler bei Standort {site.Land}: {ex.Message}"); + } + } + + Log("Export beendet."); + } + + private static void Log(string message) + { + Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}"); + } +} + +public class AppConfig +{ + public Dictionary HanaServers { get; set; } = new(); + public List Sites { get; set; } = new(); + public SharePointConfig SharePoint { get; set; } = new(); + public string DateFilter { get; set; } = "2025-01-01"; +} + +public class HanaServerConfig +{ + public string Host { get; set; } = string.Empty; + public int Port { get; set; } + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} + +public class SiteConfig +{ + public string Schema { get; set; } = string.Empty; + public string Server { get; set; } = string.Empty; + public string TSC { get; set; } = string.Empty; + public string Land { get; set; } = string.Empty; +} + +public class SharePointConfig +{ + 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; } = string.Empty; +} diff --git a/TrafagSalesExporter/Services/ExcelExportService.cs b/TrafagSalesExporter/Services/ExcelExportService.cs new file mode 100644 index 0000000..36034ee --- /dev/null +++ b/TrafagSalesExporter/Services/ExcelExportService.cs @@ -0,0 +1,89 @@ +using ClosedXML.Excel; +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public class ExcelExportService +{ + public string CreateExcelFile(string outputDirectory, string tsc, DateTime fileDate, List records) + { + Directory.CreateDirectory(outputDirectory); + var fileName = $"Sales_{tsc}_{fileDate:yyyy-MM-dd}.xlsx"; + var fullPath = Path.Combine(outputDirectory, fileName); + + using var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Sales"); + + var headers = new[] + { + "extraction date", + "TSC", + "Invoice Number", + "Position on invoice", + "Material", + "Name", + "Product Group", + "Quantity", + "Supplier number", + "Supplier name", + "Supplier country", + "Customer number", + "Customer name", + "Customer country", + "Customer Industry", + "Standard cost", + "Standard Cost Currency", + "Purchase Order number", + "Sales Price/Value", + "Sales Currency", + "Incoterms 2020", + "Sales responsible employee", + "invoice date", + "order date", + "Land", + "Document Type" + }; + + for (var i = 0; i < headers.Length; i++) + { + ws.Cell(1, i + 1).Value = headers[i]; + ws.Cell(1, i + 1).Style.Font.Bold = true; + } + + var row = 2; + foreach (var record in records) + { + ws.Cell(row, 1).Value = record.ExtractionDate.ToString("dd.MM.yyyy HH:mm:ss"); + ws.Cell(row, 2).Value = record.Tsc; + ws.Cell(row, 3).Value = record.InvoiceNumber; + ws.Cell(row, 4).Value = record.PositionOnInvoice; + ws.Cell(row, 5).Value = record.Material; + ws.Cell(row, 6).Value = record.Name; + ws.Cell(row, 7).Value = record.ProductGroup; + ws.Cell(row, 8).Value = record.Quantity; + ws.Cell(row, 9).Value = record.SupplierNumber; + ws.Cell(row, 10).Value = record.SupplierName; + ws.Cell(row, 11).Value = record.SupplierCountry; + ws.Cell(row, 12).Value = record.CustomerNumber; + ws.Cell(row, 13).Value = record.CustomerName; + ws.Cell(row, 14).Value = record.CustomerCountry; + ws.Cell(row, 15).Value = record.CustomerIndustry; + ws.Cell(row, 16).Value = record.StandardCost; + ws.Cell(row, 17).Value = record.StandardCostCurrency; + ws.Cell(row, 18).Value = record.PurchaseOrderNumber; + ws.Cell(row, 19).Value = record.SalesPriceValue; + ws.Cell(row, 20).Value = record.SalesCurrency; + ws.Cell(row, 21).Value = record.Incoterms2020; + ws.Cell(row, 22).Value = record.SalesResponsibleEmployee; + ws.Cell(row, 23).Value = record.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty; + ws.Cell(row, 24).Value = record.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty; + ws.Cell(row, 25).Value = record.Land; + ws.Cell(row, 26).Value = record.DocumentType; + row++; + } + + ws.Columns().AdjustToContents(); + workbook.SaveAs(fullPath); + return fullPath; + } +} diff --git a/TrafagSalesExporter/Services/HanaQueryService.cs b/TrafagSalesExporter/Services/HanaQueryService.cs new file mode 100644 index 0000000..125c2ad --- /dev/null +++ b/TrafagSalesExporter/Services/HanaQueryService.cs @@ -0,0 +1,167 @@ +using Sap.Data.Hana; +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public class HanaQueryService +{ + public List GetSalesRecords(string host, int port, string username, string password, string schema, string tsc, string land) + { + var connectionString = $"ServerNode={host}:{port};UserName={username};Password={password}"; + var result = new List(); + + using var connection = new HanaConnection(connectionString); + connection.Open(); + + var invoiceQuery = GetInvoiceQuery(schema, tsc); + var creditNoteQuery = GetCreditNoteQuery(schema, tsc); + + result.AddRange(ReadRecords(connection, invoiceQuery, land)); + result.AddRange(ReadRecords(connection, creditNoteQuery, land)); + + foreach (var record in result) + { + if (record.Material.Contains('/')) + { + var parts = record.Material.Split('/'); + record.Material = parts[^1]; + } + } + + return result; + } + + private static List ReadRecords(HanaConnection connection, string query, string land) + { + var records = new List(); + + using var command = new HanaCommand(query, connection); + using var reader = command.ExecuteReader(); + + while (reader.Read()) + { + records.Add(new SalesRecord + { + ExtractionDate = reader.GetDateTime(reader.GetOrdinal("extraction_date")), + Tsc = reader.GetString(reader.GetOrdinal("tsc")), + InvoiceNumber = reader["invoice_number"]?.ToString() ?? string.Empty, + PositionOnInvoice = Convert.ToInt32(reader["invoice_position"]), + InvoiceDate = reader.IsDBNull(reader.GetOrdinal("invoice_date")) ? null : reader.GetDateTime(reader.GetOrdinal("invoice_date")), + Material = reader["material"]?.ToString() ?? string.Empty, + Name = reader["material_name"]?.ToString() ?? string.Empty, + ProductGroup = reader["product_group"]?.ToString() ?? string.Empty, + Quantity = Convert.ToDecimal(reader["quantity"]), + SupplierNumber = reader["supplier_number"]?.ToString() ?? string.Empty, + SupplierName = reader["supplier_name"]?.ToString() ?? string.Empty, + SupplierCountry = reader["supplier_country"]?.ToString() ?? string.Empty, + CustomerNumber = reader["customer_number"]?.ToString() ?? string.Empty, + CustomerName = reader["customer_name"]?.ToString() ?? string.Empty, + CustomerCountry = reader["customer_country"]?.ToString() ?? string.Empty, + CustomerIndustry = reader["customer_industry"]?.ToString() ?? string.Empty, + StandardCost = Convert.ToDecimal(reader["standard_cost"]), + StandardCostCurrency = reader["standard_cost_currency"]?.ToString() ?? string.Empty, + PurchaseOrderNumber = reader["purchase_order_number"]?.ToString() ?? string.Empty, + SalesPriceValue = Convert.ToDecimal(reader["sales_value"]), + SalesCurrency = reader["sales_currency"]?.ToString() ?? string.Empty, + Incoterms2020 = reader["incoterms_2020"]?.ToString() ?? string.Empty, + SalesResponsibleEmployee = reader["sales_responsible"]?.ToString() ?? string.Empty, + OrderDate = reader.IsDBNull(reader.GetOrdinal("order_date")) ? null : reader.GetDateTime(reader.GetOrdinal("order_date")), + Land = land, + DocumentType = reader["doc_type"]?.ToString() ?? string.Empty + }); + } + + return records; + } + + private static string GetInvoiceQuery(string schema, string tsc) => $@" +SELECT + CURRENT_TIMESTAMP AS extraction_date, + '{tsc}' AS tsc, + h.""DocNum"" AS invoice_number, + p.""LineNum"" AS invoice_position, + h.""DocDate"" AS invoice_date, + p.""ItemCode"" AS material, + p.""Dscription"" AS material_name, + COALESCE(grp.""ItmsGrpNam"", '') AS product_group, + p.""Quantity"" AS quantity, + COALESCE(itm.""CardCode"", '') AS supplier_number, + COALESCE(sup.""CardName"", '') AS supplier_name, + COALESCE(sup_adr.""Country"", '') AS supplier_country, + h.""CardCode"" AS customer_number, + h.""CardName"" AS customer_name, + COALESCE(cust_adr.""Country"", '') AS customer_country, + COALESCE(ind.""IndName"", '') AS customer_industry, + p.""StockPrice"" AS standard_cost, + COALESCE(p.""Currency"", h.""DocCur"") AS standard_cost_currency, + CASE WHEN p.""BaseType"" = 22 + 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, + '' AS incoterms_2020, + COALESCE(emp.""SlpName"", '') AS sales_responsible, + CASE WHEN p.""BaseType"" = 17 + THEN (SELECT o.""DocDate"" FROM {schema}.""ORDR"" o + WHERE o.""DocEntry"" = p.""BaseEntry"") + ELSE NULL END AS order_date, + 'INV' AS doc_type +FROM {schema}.""OINV"" h +INNER JOIN {schema}.""INV1"" p ON h.""DocEntry"" = p.""DocEntry"" +LEFT JOIN {schema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode"" +LEFT JOIN {schema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod"" +LEFT JOIN {schema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode"" +LEFT JOIN {schema}.""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode"" + AND cust_adr.""AdresType"" = 'B' AND cust_adr.""Address"" = h.""PayToCode"" +LEFT JOIN {schema}.""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode"" +LEFT JOIN {schema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode"" + AND sup.""CardType"" = 'S' +LEFT JOIN {schema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode"" + AND sup_adr.""AdresType"" = 'B' +LEFT JOIN {schema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode"" +WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '2025-01-01' +ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum"""; + + private static string GetCreditNoteQuery(string schema, string tsc) => $@" +SELECT + CURRENT_TIMESTAMP AS extraction_date, + '{tsc}' AS tsc, + h.""DocNum"" AS invoice_number, + p.""LineNum"" AS invoice_position, + h.""DocDate"" AS invoice_date, + p.""ItemCode"" AS material, + p.""Dscription"" AS material_name, + COALESCE(grp.""ItmsGrpNam"", '') AS product_group, + p.""Quantity"" * -1 AS quantity, + COALESCE(itm.""CardCode"", '') AS supplier_number, + COALESCE(sup.""CardName"", '') AS supplier_name, + COALESCE(sup_adr.""Country"", '') AS supplier_country, + h.""CardCode"" AS customer_number, + h.""CardName"" AS customer_name, + COALESCE(cust_adr.""Country"", '') AS customer_country, + COALESCE(ind.""IndName"", '') AS customer_industry, + p.""StockPrice"" AS standard_cost, + COALESCE(p.""Currency"", h.""DocCur"") AS standard_cost_currency, + '' AS purchase_order_number, + p.""LineTotal"" * -1 AS sales_value, + COALESCE(p.""Currency"", h.""DocCur"") AS sales_currency, + '' AS incoterms_2020, + COALESCE(emp.""SlpName"", '') AS sales_responsible, + NULL AS order_date, + 'CRN' AS doc_type +FROM {schema}.""ORIN"" h +INNER JOIN {schema}.""RIN1"" p ON h.""DocEntry"" = p.""DocEntry"" +LEFT JOIN {schema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode"" +LEFT JOIN {schema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod"" +LEFT JOIN {schema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode"" +LEFT JOIN {schema}.""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode"" + AND cust_adr.""AdresType"" = 'B' AND cust_adr.""Address"" = h.""PayToCode"" +LEFT JOIN {schema}.""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode"" +LEFT JOIN {schema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode"" + AND sup.""CardType"" = 'S' +LEFT JOIN {schema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode"" + AND sup_adr.""AdresType"" = 'B' +LEFT JOIN {schema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode"" +WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '2025-01-01' +ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum"""; +} diff --git a/TrafagSalesExporter/Services/SharePointUploadService.cs b/TrafagSalesExporter/Services/SharePointUploadService.cs new file mode 100644 index 0000000..12aae78 --- /dev/null +++ b/TrafagSalesExporter/Services/SharePointUploadService.cs @@ -0,0 +1,44 @@ +using Azure.Identity; +using Microsoft.Graph; + +namespace TrafagSalesExporter.Services; + +public class SharePointUploadService +{ + private readonly GraphServiceClient _graphClient; + private readonly string _siteUrl; + private readonly string _exportFolder; + + public SharePointUploadService(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder) + { + var credential = new ClientSecretCredential(tenantId, clientId, clientSecret); + _graphClient = new GraphServiceClient(credential, ["https://graph.microsoft.com/.default"]); + _siteUrl = siteUrl; + _exportFolder = exportFolder; + } + + public async Task UploadAsync(string land, string localFilePath) + { + var uri = new Uri(_siteUrl); + var sitePath = uri.AbsolutePath; + var site = await _graphClient.Sites[$"{uri.Host}:{sitePath}"].GetAsync(); + + if (site?.Id is null) + { + throw new InvalidOperationException("SharePoint Site konnte nicht gefunden werden."); + } + + var drive = await _graphClient.Sites[site.Id].Drive.GetAsync(); + if (drive?.Id is null) + { + throw new InvalidOperationException("SharePoint Dokumentenbibliothek konnte nicht gefunden werden."); + } + + var fileName = Path.GetFileName(localFilePath); + var folderPath = $"{_exportFolder.Trim('/').Trim()}"; + var remotePath = $"{folderPath}/{land}/{fileName}"; + + await using var stream = File.OpenRead(localFilePath); + await _graphClient.Drives[drive.Id].Root.ItemWithPath(remotePath).Content.PutAsync(stream); + } +} diff --git a/TrafagSalesExporter/TrafagSalesExporter.csproj b/TrafagSalesExporter/TrafagSalesExporter.csproj new file mode 100644 index 0000000..4d9d45a --- /dev/null +++ b/TrafagSalesExporter/TrafagSalesExporter.csproj @@ -0,0 +1,24 @@ + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/TrafagSalesExporter/appsettings.json b/TrafagSalesExporter/appsettings.json new file mode 100644 index 0000000..0a5463d --- /dev/null +++ b/TrafagSalesExporter/appsettings.json @@ -0,0 +1,30 @@ +{ + "HanaServers": { + "Internal": { + "Host": "travtrp0", + "Port": 30015, + "Username": "", + "Password": "" + }, + "India": { + "Host": "20.197.20.60", + "Port": 30015, + "Username": "", + "Password": "" + } + }, + "Sites": [ + { "Schema": "fr01_p", "Server": "Internal", "TSC": "TRFR", "Land": "Frankreich" }, + { "Schema": "it01_p", "Server": "Internal", "TSC": "TRIT", "Land": "Italien" }, + { "Schema": "us01_p", "Server": "Internal", "TSC": "TRUS", "Land": "USA" }, + { "Schema": "TRAFAG_LIVE", "Server": "India", "TSC": "TRIN", "Land": "Indien" } + ], + "SharePoint": { + "SiteUrl": "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform", + "ExportFolder": "/Shared Documents/Exports/", + "TenantId": "", + "ClientId": "", + "ClientSecret": "" + }, + "DateFilter": "2025-01-01" +}