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")
|
b.Property<string>("RawXml")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ResultFilePath")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<string>("SellerName")
|
b.Property<string>("SellerName")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
public string RawXml { get; set; } = string.Empty; // Original-XML zur Sicherheit
|
public string RawXml { get; set; } = string.Empty; // Original-XML zur Sicherheit
|
||||||
public DateTime ImportedAt { get; set; }
|
public DateTime ImportedAt { get; set; }
|
||||||
public string SourceType { get; set; } = string.Empty; // "Upload" oder "Email"
|
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>
|
<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>
|
</div>
|
||||||
|
|
||||||
@* PDF/A-Konformitätsstufe anzeigen *@
|
@* 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 DXApp.TemplateKitProject.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
@@ -8,6 +9,8 @@ namespace DXApp.TemplateKitProject.Pages.Invoices;
|
|||||||
public class UploadModel(
|
public class UploadModel(
|
||||||
PdfAttachmentExtractorService extractor,
|
PdfAttachmentExtractorService extractor,
|
||||||
ZugferdImportService zugferdImportService,
|
ZugferdImportService zugferdImportService,
|
||||||
|
PdfResultPackageService resultPackageService,
|
||||||
|
AppDbContext db,
|
||||||
ILogger<UploadModel> logger) : PageModel
|
ILogger<UploadModel> logger) : PageModel
|
||||||
{
|
{
|
||||||
[BindProperty]
|
[BindProperty]
|
||||||
@@ -17,6 +20,7 @@ public class UploadModel(
|
|||||||
public bool ExtractionDone { get; private set; }
|
public bool ExtractionDone { get; private set; }
|
||||||
public string? ErrorMessage { get; private set; }
|
public string? ErrorMessage { get; private set; }
|
||||||
public ZugferdInvoice? ImportedInvoice { get; private set; }
|
public ZugferdInvoice? ImportedInvoice { get; private set; }
|
||||||
|
public string? ResultFilePath { get; private set; }
|
||||||
|
|
||||||
public void OnGet()
|
public void OnGet()
|
||||||
{ }
|
{ }
|
||||||
@@ -42,8 +46,9 @@ public class UploadModel(
|
|||||||
// Stream in MemoryStream puffern → kann zweimal gelesen werden
|
// Stream in MemoryStream puffern → kann zweimal gelesen werden
|
||||||
using var memStream = new MemoryStream();
|
using var memStream = new MemoryStream();
|
||||||
await PdfFile.CopyToAsync(memStream);
|
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;
|
memStream.Position = 0;
|
||||||
Result = extractor.ExtractAttachments(memStream, PdfFile.FileName);
|
Result = extractor.ExtractAttachments(memStream, PdfFile.FileName);
|
||||||
|
|
||||||
@@ -52,6 +57,20 @@ public class UploadModel(
|
|||||||
{
|
{
|
||||||
memStream.Position = 0;
|
memStream.Position = 0;
|
||||||
ImportedInvoice = await zugferdImportService.ImportAsync(memStream, "Upload");
|
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)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ builder.Services.AddDbContext<AppDbContext>(options =>
|
|||||||
|
|
||||||
// Services
|
// Services
|
||||||
builder.Services.AddScoped<PdfAttachmentExtractorService>();
|
builder.Services.AddScoped<PdfAttachmentExtractorService>();
|
||||||
|
builder.Services.AddScoped<PdfResultPackageService>();
|
||||||
builder.Services.AddScoped<ZugferdExtractorService>();
|
builder.Services.AddScoped<ZugferdExtractorService>();
|
||||||
builder.Services.AddScoped<ZugferdParserService>();
|
builder.Services.AddScoped<ZugferdParserService>();
|
||||||
builder.Services.AddScoped<ZugferdImportService>();
|
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": {
|
"PdfExtraction": {
|
||||||
"OutputDirectory": "C:\\PdfExtractions"
|
"OutputDirectory": "C:\\PdfExtractions"
|
||||||
|
},
|
||||||
|
"PdfResultReports": {
|
||||||
|
"InputDirectory": "C:\\PdfResultReports"
|
||||||
|
},
|
||||||
|
"PdfResults": {
|
||||||
|
"OutputDirectory": "C:\\PdfResults"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user