From 87f27682ce2bc7fb3876ee9aa8a1ee112e466f01 Mon Sep 17 00:00:00 2001 From: OlgunR Date: Wed, 27 May 2026 13:43:14 +0200 Subject: [PATCH] Add support for generating result PDFs Introduced the `ResultFilePath` property in the `ZugferdInvoice` model to store the path of generated result PDFs. Added a new service, `PdfResultPackageService`, to create result PDFs by converting the original PDF to PDF/A-3b format and attaching a report file. Updated `Upload.cshtml` and `Upload.cshtml.cs` to handle and display the `ResultFilePath`. Created a migration to add the `ResultFilePath` column to the database. Updated `Program.cs` to register the new service and added configuration sections in `appsettings.json` for input and output directories. Enhanced error handling and logging for better traceability. --- ...260527094043_AddResultFilePath.Designer.cs | 82 ++++++++++++++++ .../20260527094043_AddResultFilePath.cs | 28 ++++++ .../Migrations/AppDbContextModelSnapshot.cs | 3 + .../Models/ZugferdInvoice.cs | 1 + .../Pages/Invoices/Upload.cshtml | 7 ++ .../Pages/Invoices/Upload.cshtml.cs | 23 ++++- DXApp.TemplateKitProject/Program.cs | 1 + .../Services/PdfResultPackageService.cs | 93 +++++++++++++++++++ DXApp.TemplateKitProject/appsettings.json | 6 ++ 9 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 DXApp.TemplateKitProject/Migrations/20260527094043_AddResultFilePath.Designer.cs create mode 100644 DXApp.TemplateKitProject/Migrations/20260527094043_AddResultFilePath.cs create mode 100644 DXApp.TemplateKitProject/Services/PdfResultPackageService.cs diff --git a/DXApp.TemplateKitProject/Migrations/20260527094043_AddResultFilePath.Designer.cs b/DXApp.TemplateKitProject/Migrations/20260527094043_AddResultFilePath.Designer.cs new file mode 100644 index 0000000..d9014e4 --- /dev/null +++ b/DXApp.TemplateKitProject/Migrations/20260527094043_AddResultFilePath.Designer.cs @@ -0,0 +1,82 @@ +// +using System; +using DXApp.TemplateKitProject.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DXApp.TemplateKitProject.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260527094043_AddResultFilePath")] + partial class AddResultFilePath + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DXApp.TemplateKitProject.Models.ZugferdInvoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BuyerName") + .HasColumnType("nvarchar(max)"); + + b.Property("CurrencyCode") + .HasColumnType("nvarchar(max)"); + + b.Property("Iban") + .HasColumnType("nvarchar(max)"); + + b.Property("ImportedAt") + .HasColumnType("datetime2"); + + b.Property("InvoiceDate") + .HasColumnType("datetime2"); + + b.Property("InvoiceNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("RawXml") + .HasColumnType("nvarchar(max)"); + + b.Property("ResultFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("SellerName") + .HasColumnType("nvarchar(max)"); + + b.Property("SellerTaxId") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceType") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalAmount") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.ToTable("ZugferdInvoices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DXApp.TemplateKitProject/Migrations/20260527094043_AddResultFilePath.cs b/DXApp.TemplateKitProject/Migrations/20260527094043_AddResultFilePath.cs new file mode 100644 index 0000000..7d0bd3b --- /dev/null +++ b/DXApp.TemplateKitProject/Migrations/20260527094043_AddResultFilePath.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DXApp.TemplateKitProject.Migrations +{ + /// + public partial class AddResultFilePath : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ResultFilePath", + table: "ZugferdInvoices", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ResultFilePath", + table: "ZugferdInvoices"); + } + } +} diff --git a/DXApp.TemplateKitProject/Migrations/AppDbContextModelSnapshot.cs b/DXApp.TemplateKitProject/Migrations/AppDbContextModelSnapshot.cs index ddaeced..e5f481e 100644 --- a/DXApp.TemplateKitProject/Migrations/AppDbContextModelSnapshot.cs +++ b/DXApp.TemplateKitProject/Migrations/AppDbContextModelSnapshot.cs @@ -51,6 +51,9 @@ namespace DXApp.TemplateKitProject.Migrations b.Property("RawXml") .HasColumnType("nvarchar(max)"); + b.Property("ResultFilePath") + .HasColumnType("nvarchar(max)"); + b.Property("SellerName") .HasColumnType("nvarchar(max)"); diff --git a/DXApp.TemplateKitProject/Models/ZugferdInvoice.cs b/DXApp.TemplateKitProject/Models/ZugferdInvoice.cs index 3fa808f..0da23fb 100644 --- a/DXApp.TemplateKitProject/Models/ZugferdInvoice.cs +++ b/DXApp.TemplateKitProject/Models/ZugferdInvoice.cs @@ -15,5 +15,6 @@ public string RawXml { get; set; } = string.Empty; // Original-XML zur Sicherheit public DateTime ImportedAt { get; set; } public string SourceType { get; set; } = string.Empty; // "Upload" oder "Email" + public string ResultFilePath { get; set; } = string.Empty; // Pfad der Result-PDF } } \ No newline at end of file diff --git a/DXApp.TemplateKitProject/Pages/Invoices/Upload.cshtml b/DXApp.TemplateKitProject/Pages/Invoices/Upload.cshtml index 7552eef..2fca806 100644 --- a/DXApp.TemplateKitProject/Pages/Invoices/Upload.cshtml +++ b/DXApp.TemplateKitProject/Pages/Invoices/Upload.cshtml @@ -40,6 +40,13 @@ { ⚠ Kein ZUGFeRD-XML gefunden. } + @if (!string.IsNullOrEmpty(Model.ResultFilePath)) + { +
+ 📦 Result-PDF erstellt: + @Model.ResultFilePath +
+ } @* PDF/A-Konformitätsstufe anzeigen *@ diff --git a/DXApp.TemplateKitProject/Pages/Invoices/Upload.cshtml.cs b/DXApp.TemplateKitProject/Pages/Invoices/Upload.cshtml.cs index 3e5cf8b..759bb6f 100644 --- a/DXApp.TemplateKitProject/Pages/Invoices/Upload.cshtml.cs +++ b/DXApp.TemplateKitProject/Pages/Invoices/Upload.cshtml.cs @@ -1,4 +1,5 @@ -using DXApp.TemplateKitProject.Models; +using DXApp.TemplateKitProject.Data; +using DXApp.TemplateKitProject.Models; using DXApp.TemplateKitProject.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -8,6 +9,8 @@ namespace DXApp.TemplateKitProject.Pages.Invoices; public class UploadModel( PdfAttachmentExtractorService extractor, ZugferdImportService zugferdImportService, + PdfResultPackageService resultPackageService, + AppDbContext db, ILogger logger) : PageModel { [BindProperty] @@ -17,6 +20,7 @@ public class UploadModel( public bool ExtractionDone { get; private set; } public string? ErrorMessage { get; private set; } public ZugferdInvoice? ImportedInvoice { get; private set; } + public string? ResultFilePath { get; private set; } public void OnGet() { } @@ -42,8 +46,9 @@ public class UploadModel( // Stream in MemoryStream puffern → kann zweimal gelesen werden using var memStream = new MemoryStream(); await PdfFile.CopyToAsync(memStream); + var originalBytes = memStream.ToArray(); // ← neu: als byte[] merken - // 1. Anhänge extrahieren und auf Disk speichern + // 1. Anhänge extrahieren memStream.Position = 0; Result = extractor.ExtractAttachments(memStream, PdfFile.FileName); @@ -52,6 +57,20 @@ public class UploadModel( { memStream.Position = 0; ImportedInvoice = await zugferdImportService.ImportAsync(memStream, "Upload"); + + // 3. Result-Package erstellen (nur wenn Import erfolgreich) + if (ImportedInvoice is not null) + { + ResultFilePath = await resultPackageService.CreateResultPackageAsync( + originalBytes, PdfFile.FileName, ImportedInvoice); + + // ResultFilePath in DB aktualisieren + if (ResultFilePath is not null) + { + ImportedInvoice.ResultFilePath = ResultFilePath; + await db.SaveChangesAsync(); + } + } } } catch (Exception ex) diff --git a/DXApp.TemplateKitProject/Program.cs b/DXApp.TemplateKitProject/Program.cs index ad43148..47fd9b4 100644 --- a/DXApp.TemplateKitProject/Program.cs +++ b/DXApp.TemplateKitProject/Program.cs @@ -14,6 +14,7 @@ builder.Services.AddDbContext(options => // Services builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/DXApp.TemplateKitProject/Services/PdfResultPackageService.cs b/DXApp.TemplateKitProject/Services/PdfResultPackageService.cs new file mode 100644 index 0000000..c9b0898 --- /dev/null +++ b/DXApp.TemplateKitProject/Services/PdfResultPackageService.cs @@ -0,0 +1,93 @@ +using DevExpress.Pdf; +using DXApp.TemplateKitProject.Models; + +namespace DXApp.TemplateKitProject.Services; + +public class PdfResultPackageService( + IConfiguration configuration, + ILogger logger) +{ + public async Task CreateResultPackageAsync( + byte[] originalPdfBytes, + string originalFileName, + ZugferdInvoice invoice) + { + // 1. Bericht-PDF suchen + var reportPath = FindReportFile(originalFileName); + if (reportPath is null) + { + logger.LogWarning( + "Kein Ergebnisbericht gefunden für '{FileName}'.", originalFileName); + return null; + } + + logger.LogInformation( + "Ergebnisbericht gefunden: '{ReportPath}'.", reportPath); + + // 2. Ausgabepfad bestimmen + var outputDir = configuration["PdfResults:OutputDirectory"] + ?? Path.Combine(Path.GetTempPath(), "PdfResults"); + Directory.CreateDirectory(outputDir); + + var baseName = Path.GetFileNameWithoutExtension(originalFileName); + var outputPath = Path.Combine(outputDir, $"{baseName}_result.pdf"); + + // 3. Original auf PDF/A-3b hochstufen + Bericht anhängen + await Task.Run(() => + { + // Original in MemoryStream laden + using var inputStream = new MemoryStream(originalPdfBytes); + using var outputStream = new MemoryStream(); + + // PDF/A-3b Konvertierung + var converter = new PdfDocumentConverter(inputStream); + converter.Convert(PdfCompatibility.PdfA3b); + + // Konvertiertes PDF in MemoryStream speichern + using var convertedStream = new MemoryStream(); + converter.SaveDocument(convertedStream); + convertedStream.Position = 0; + + // Bericht als Anhang einbetten + using var processor = new PdfDocumentProcessor(); + processor.LoadDocument(convertedStream); + + processor.AttachFile(new PdfFileAttachment + { + FileName = Path.GetFileName(reportPath), + Description = "Ergebnisbericht", + MimeType = "application/pdf", + Relationship = PdfAssociatedFileRelationship.Supplement, + CreationDate = DateTime.Now, + Data = File.ReadAllBytes(reportPath) + }); + + // Speichern + processor.SaveDocument(outputPath); + }); + + logger.LogInformation( + "Result-PDF gespeichert: '{OutputPath}'.", outputPath); + + return outputPath; + } + + private string? FindReportFile(string originalFileName) + { + var inputDir = configuration["PdfResultReports:InputDirectory"] + ?? Path.Combine(Path.GetTempPath(), "PdfResultReports"); + + if (!Directory.Exists(inputDir)) + { + logger.LogWarning("Berichtsverzeichnis nicht gefunden: '{Dir}'.", inputDir); + return null; + } + + // Konvention Option A: {originalname}_report.pdf + var baseName = Path.GetFileNameWithoutExtension(originalFileName); + var reportName = $"{baseName}_report.pdf"; + var reportPath = Path.Combine(inputDir, reportName); + + return File.Exists(reportPath) ? reportPath : null; + } +} \ No newline at end of file diff --git a/DXApp.TemplateKitProject/appsettings.json b/DXApp.TemplateKitProject/appsettings.json index 93808b8..8fb61d4 100644 --- a/DXApp.TemplateKitProject/appsettings.json +++ b/DXApp.TemplateKitProject/appsettings.json @@ -11,5 +11,11 @@ }, "PdfExtraction": { "OutputDirectory": "C:\\PdfExtractions" + }, + "PdfResultReports": { + "InputDirectory": "C:\\PdfResultReports" + }, + "PdfResults": { + "OutputDirectory": "C:\\PdfResults" } } \ No newline at end of file