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.
This commit is contained in:
82
DXApp.TemplateKitProject/Migrations/20260527094043_AddResultFilePath.Designer.cs
generated
Normal file
82
DXApp.TemplateKitProject/Migrations/20260527094043_AddResultFilePath.Designer.cs
generated
Normal file
@@ -0,0 +1,82 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("BuyerName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Iban")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("ImportedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("InvoiceDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("InvoiceNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RawXml")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ResultFilePath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SellerName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SellerTaxId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SourceType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<decimal>("TaxAmount")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("TotalAmount")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ZugferdInvoices");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DXApp.TemplateKitProject.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddResultFilePath : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ResultFilePath",
|
||||
table: "ZugferdInvoices",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ResultFilePath",
|
||||
table: "ZugferdInvoices");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,9 @@ namespace DXApp.TemplateKitProject.Migrations
|
||||
b.Property<string>("RawXml")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ResultFilePath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SellerName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,13 @@
|
||||
{
|
||||
<strong>⚠ Kein ZUGFeRD-XML gefunden.</strong>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.ResultFilePath))
|
||||
{
|
||||
<div class="alert alert-success mt-2">
|
||||
📦 <strong>Result-PDF erstellt:</strong>
|
||||
<small class="text-muted">@Model.ResultFilePath</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* PDF/A-Konformitätsstufe anzeigen *@
|
||||
|
||||
@@ -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<UploadModel> 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)
|
||||
|
||||
@@ -14,6 +14,7 @@ builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
|
||||
// Services
|
||||
builder.Services.AddScoped<PdfAttachmentExtractorService>();
|
||||
builder.Services.AddScoped<PdfResultPackageService>();
|
||||
builder.Services.AddScoped<ZugferdExtractorService>();
|
||||
builder.Services.AddScoped<ZugferdParserService>();
|
||||
builder.Services.AddScoped<ZugferdImportService>();
|
||||
|
||||
93
DXApp.TemplateKitProject/Services/PdfResultPackageService.cs
Normal file
93
DXApp.TemplateKitProject/Services/PdfResultPackageService.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using DevExpress.Pdf;
|
||||
using DXApp.TemplateKitProject.Models;
|
||||
|
||||
namespace DXApp.TemplateKitProject.Services;
|
||||
|
||||
public class PdfResultPackageService(
|
||||
IConfiguration configuration,
|
||||
ILogger<PdfResultPackageService> logger)
|
||||
{
|
||||
public async Task<string?> 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;
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,11 @@
|
||||
},
|
||||
"PdfExtraction": {
|
||||
"OutputDirectory": "C:\\PdfExtractions"
|
||||
},
|
||||
"PdfResultReports": {
|
||||
"InputDirectory": "C:\\PdfResultReports"
|
||||
},
|
||||
"PdfResults": {
|
||||
"OutputDirectory": "C:\\PdfResults"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user