Add ZUGFeRD parsing and invoice storage support
Enhanced `ZugferdInvoice` model with default string values to prevent nulls. Updated `Upload.cshtml` to display parsed invoice data. Refactored `Upload.cshtml.cs` to handle ZUGFeRD XML parsing and database storage. Introduced `ImportedInvoice` property and buffered file processing with `MemoryStream`. Extended `ZugferdParserService` to support ZUGFeRD v1, v1.0 FeRD, and v2/Factur-X. Added version-specific parsing methods and namespaces. Improved date and decimal parsing for robustness. Added database migration (`20260522084606_InitialCreate`) to define `ZugferdInvoices` table. Updated migration snapshot to reflect schema changes. Fixed localization issue in `Upload.cshtml.cs` error message.
This commit is contained in:
79
DXApp.TemplateKitProject/Migrations/20260522084606_InitialCreate.Designer.cs
generated
Normal file
79
DXApp.TemplateKitProject/Migrations/20260522084606_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// <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("20260522084606_InitialCreate")]
|
||||||
|
partial class InitialCreate
|
||||||
|
{
|
||||||
|
/// <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>("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,46 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DXApp.TemplateKitProject.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ZugferdInvoices",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
InvoiceNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
InvoiceDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
SellerName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
SellerTaxId = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
BuyerName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
TotalAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
TaxAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
CurrencyCode = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
Iban = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
RawXml = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
ImportedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
SourceType = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ZugferdInvoices", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ZugferdInvoices");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using DXApp.TemplateKitProject.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DXApp.TemplateKitProject.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(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>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,17 +3,17 @@
|
|||||||
public class ZugferdInvoice
|
public class ZugferdInvoice
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string InvoiceNumber { get; set; }
|
public string InvoiceNumber { get; set; } = string.Empty;
|
||||||
public DateTime InvoiceDate { get; set; }
|
public DateTime InvoiceDate { get; set; }
|
||||||
public string SellerName { get; set; }
|
public string SellerName { get; set; } = string.Empty;
|
||||||
public string SellerTaxId { get; set; }
|
public string SellerTaxId { get; set; } = string.Empty;
|
||||||
public string BuyerName { get; set; }
|
public string BuyerName { get; set; } = string.Empty;
|
||||||
public decimal TotalAmount { get; set; }
|
public decimal TotalAmount { get; set; }
|
||||||
public decimal TaxAmount { get; set; }
|
public decimal TaxAmount { get; set; }
|
||||||
public string CurrencyCode { get; set; }
|
public string CurrencyCode { get; set; } = string.Empty;
|
||||||
public string Iban { get; set; }
|
public string Iban { get; set; } = string.Empty;
|
||||||
public string RawXml { get; set; } // 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; } // "Upload" oder "Email"
|
public string SourceType { get; set; } = string.Empty; // "Upload" oder "Email"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,4 +64,22 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
}
|
}
|
||||||
|
@if (Model.ImportedInvoice is not null)
|
||||||
|
{
|
||||||
|
<hr />
|
||||||
|
<h4>📄 Geparste Rechnungsdaten</h4>
|
||||||
|
<table class="table table-sm table-bordered w-auto">
|
||||||
|
<tr><th>Rechnungsnummer</th><td>@Model.ImportedInvoice.InvoiceNumber</td></tr>
|
||||||
|
<tr><th>Rechnungsdatum</th><td>@Model.ImportedInvoice.InvoiceDate.ToString("dd.MM.yyyy")</td></tr>
|
||||||
|
<tr><th>Verkäufer</th><td>@Model.ImportedInvoice.SellerName</td></tr>
|
||||||
|
<tr><th>USt-ID Verkäufer</th><td>@Model.ImportedInvoice.SellerTaxId</td></tr>
|
||||||
|
<tr><th>Käufer</th><td>@Model.ImportedInvoice.BuyerName</td></tr>
|
||||||
|
<tr><th>Währung</th><td>@Model.ImportedInvoice.CurrencyCode</td></tr>
|
||||||
|
<tr><th>Steuerbetrag</th><td>@Model.ImportedInvoice.TaxAmount.ToString("N2")</td></tr>
|
||||||
|
<tr><th>Gesamtbetrag</th><td><strong>@Model.ImportedInvoice.TotalAmount.ToString("N2")</strong></td></tr>
|
||||||
|
<tr><th>IBAN</th><td>@Model.ImportedInvoice.Iban</td></tr>
|
||||||
|
<tr><th>Importiert am</th><td>@Model.ImportedInvoice.ImportedAt.ToString("dd.MM.yyyy HH:mm")</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="alert alert-success mt-2">✔ Rechnung wurde in der Datenbank gespeichert (ID: @Model.ImportedInvoice.Id)</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using DXApp.TemplateKitProject.Models;
|
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;
|
||||||
@@ -7,6 +7,7 @@ namespace DXApp.TemplateKitProject.Pages.Invoices;
|
|||||||
|
|
||||||
public class UploadModel(
|
public class UploadModel(
|
||||||
PdfAttachmentExtractorService extractor,
|
PdfAttachmentExtractorService extractor,
|
||||||
|
ZugferdImportService zugferdImportService,
|
||||||
ILogger<UploadModel> logger) : PageModel
|
ILogger<UploadModel> logger) : PageModel
|
||||||
{
|
{
|
||||||
[BindProperty]
|
[BindProperty]
|
||||||
@@ -15,6 +16,7 @@ public class UploadModel(
|
|||||||
public PdfExtractionResult? Result { get; private set; }
|
public PdfExtractionResult? Result { get; private set; }
|
||||||
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 void OnGet()
|
public void OnGet()
|
||||||
{ }
|
{ }
|
||||||
@@ -23,7 +25,7 @@ public class UploadModel(
|
|||||||
{
|
{
|
||||||
if (PdfFile is null || PdfFile.Length == 0)
|
if (PdfFile is null || PdfFile.Length == 0)
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(PdfFile), "Bitte eine PDF-Datei auswählen.");
|
ModelState.AddModelError(nameof(PdfFile), "Bitte eine PDF-Datei auswählen.");
|
||||||
return Page();
|
return Page();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,8 +39,20 @@ public class UploadModel(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await using var stream = PdfFile.OpenReadStream();
|
// Stream in MemoryStream puffern → kann zweimal gelesen werden
|
||||||
Result = extractor.ExtractAttachments(stream, PdfFile.FileName);
|
using var memStream = new MemoryStream();
|
||||||
|
await PdfFile.CopyToAsync(memStream);
|
||||||
|
|
||||||
|
// 1. Anhänge extrahieren und auf Disk speichern
|
||||||
|
memStream.Position = 0;
|
||||||
|
Result = extractor.ExtractAttachments(memStream, PdfFile.FileName);
|
||||||
|
|
||||||
|
// 2. Wenn ZUGFeRD-XML gefunden → parsen und in DB speichern
|
||||||
|
if (Result.HasZugferdXml)
|
||||||
|
{
|
||||||
|
memStream.Position = 0;
|
||||||
|
ImportedInvoice = await zugferdImportService.ImportAsync(memStream, "Upload");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,20 +5,77 @@ using System.Xml.Linq;
|
|||||||
|
|
||||||
public class ZugferdParserService
|
public class ZugferdParserService
|
||||||
{
|
{
|
||||||
// ZUGFeRD v2 / Factur-X Namespaces
|
// ZUGFeRD v1 Namespace
|
||||||
private static readonly XNamespace Ram = "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100";
|
private static readonly XNamespace RsmV1 = "urn:un:unece:uncefact:data:standard:CBFBUY:5";
|
||||||
|
|
||||||
private static readonly XNamespace Rsm = "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100";
|
// ZUGFeRD v2 / Factur-X Namespaces
|
||||||
|
private static readonly XNamespace RsmV2 = "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100";
|
||||||
|
|
||||||
|
private static readonly XNamespace Ram = "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100";
|
||||||
private static readonly XNamespace Udt = "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100";
|
private static readonly XNamespace Udt = "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100";
|
||||||
|
|
||||||
|
// ZUGFeRD v1.0 FeRD (CrossIndustryDocument)
|
||||||
|
private static readonly XNamespace RsmFerd1 = "urn:ferd:CrossIndustryDocument:invoice:1p0";
|
||||||
|
|
||||||
|
private static readonly XNamespace RamV12 = "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12";
|
||||||
|
private static readonly XNamespace UdtV15 = "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:15";
|
||||||
|
|
||||||
|
// NACHHER:
|
||||||
public ZugferdInvoice Parse(string xml)
|
public ZugferdInvoice Parse(string xml)
|
||||||
{
|
{
|
||||||
var doc = XDocument.Parse(xml);
|
var doc = XDocument.Parse(xml);
|
||||||
var root = doc.Root!;
|
var root = doc.Root!;
|
||||||
|
|
||||||
var header = root.Element(Rsm + "ExchangedDocument");
|
if (root.Name.Namespace == RsmV1) return ParseV1(root, xml);
|
||||||
var trade = root.Element(Rsm + "SupplyChainTradeTransaction");
|
if (root.Name.Namespace == RsmFerd1) return ParseV1Ferd(root, xml);
|
||||||
|
return ParseV2(root, xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ZUGFeRD v1 (CBFBUY:5) ────────────────────────────────────────────────
|
||||||
|
private static ZugferdInvoice ParseV1(XElement root, string xml)
|
||||||
|
{
|
||||||
|
// In v1 haben Kind-Elemente KEINEN Namespace-Präfix
|
||||||
|
var ns = XNamespace.None;
|
||||||
|
var rsm = RsmV1;
|
||||||
|
|
||||||
|
var header = root.Element(rsm + "HeaderExchangedDocument");
|
||||||
|
var trade = root.Element(rsm + "SpecifiedSupplyChainTradeTransaction");
|
||||||
|
var agreement = trade?.Element(ns + "ApplicableSupplyChainTradeAgreement");
|
||||||
|
var settlement = trade?.Element(ns + "ApplicableSupplyChainTradeSettlement");
|
||||||
|
|
||||||
|
var seller = agreement?.Element(ns + "SellerTradeParty");
|
||||||
|
var buyer = agreement?.Element(ns + "BuyerTradeParty");
|
||||||
|
var summation = settlement?.Element(ns + "SpecifiedTradeSettlementMonetarySummation");
|
||||||
|
var payment = settlement?.Element(ns + "SpecifiedTradeSettlementPaymentMeans");
|
||||||
|
|
||||||
|
// SellerTaxId: das SpecifiedTaxRegistration mit schemeID="VA" nehmen
|
||||||
|
var sellerTaxId = seller?
|
||||||
|
.Elements(ns + "SpecifiedTaxRegistration")
|
||||||
|
.FirstOrDefault(e => (string?)e.Element(ns + "ID")?.Attribute("schemeID") == "VA")
|
||||||
|
?.Element(ns + "ID")?.Value ?? string.Empty;
|
||||||
|
|
||||||
|
return new ZugferdInvoice
|
||||||
|
{
|
||||||
|
InvoiceNumber = header?.Element(ns + "ID")?.Value ?? string.Empty,
|
||||||
|
InvoiceDate = ParseDate(header?.Element(ns + "IssueDateTime")?.Value),
|
||||||
|
SellerName = seller?.Element(ns + "Name")?.Value ?? string.Empty,
|
||||||
|
SellerTaxId = sellerTaxId,
|
||||||
|
BuyerName = buyer?.Element(ns + "Name")?.Value ?? string.Empty,
|
||||||
|
CurrencyCode = settlement?.Element(ns + "InvoiceCurrencyCode")?.Value ?? "EUR",
|
||||||
|
TotalAmount = ParseDecimal(summation?.Element(ns + "GrandTotalAmount")?.Value),
|
||||||
|
TaxAmount = ParseDecimal(summation?.Element(ns + "TaxTotalAmount")?.Value),
|
||||||
|
Iban = payment?.Element(ns + "PayeePartyCreditorFinancialAccount")
|
||||||
|
?.Element(ns + "IBANID")?.Value ?? string.Empty,
|
||||||
|
RawXml = xml,
|
||||||
|
ImportedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ZUGFeRD v2 / Factur-X ────────────────────────────────────────────────
|
||||||
|
private static ZugferdInvoice ParseV2(XElement root, string xml)
|
||||||
|
{
|
||||||
|
var header = root.Element(RsmV2 + "ExchangedDocument");
|
||||||
|
var trade = root.Element(RsmV2 + "SupplyChainTradeTransaction");
|
||||||
var agreement = trade?.Element(Ram + "ApplicableHeaderTradeAgreement");
|
var agreement = trade?.Element(Ram + "ApplicableHeaderTradeAgreement");
|
||||||
var settlement = trade?.Element(Ram + "ApplicableHeaderTradeSettlement");
|
var settlement = trade?.Element(Ram + "ApplicableHeaderTradeSettlement");
|
||||||
|
|
||||||
@@ -27,7 +84,6 @@ public class ZugferdParserService
|
|||||||
InvoiceNumber = header?.Element(Ram + "ID")?.Value ?? string.Empty,
|
InvoiceNumber = header?.Element(Ram + "ID")?.Value ?? string.Empty,
|
||||||
InvoiceDate = ParseDate(header?.Element(Ram + "IssueDateTime")
|
InvoiceDate = ParseDate(header?.Element(Ram + "IssueDateTime")
|
||||||
?.Element(Udt + "DateTimeString")?.Value),
|
?.Element(Udt + "DateTimeString")?.Value),
|
||||||
|
|
||||||
SellerName = agreement?.Element(Ram + "SellerTradeParty")
|
SellerName = agreement?.Element(Ram + "SellerTradeParty")
|
||||||
?.Element(Ram + "Name")?.Value ?? string.Empty,
|
?.Element(Ram + "Name")?.Value ?? string.Empty,
|
||||||
SellerTaxId = agreement?.Element(Ram + "SellerTradeParty")
|
SellerTaxId = agreement?.Element(Ram + "SellerTradeParty")
|
||||||
@@ -35,11 +91,12 @@ public class ZugferdParserService
|
|||||||
?.Element(Ram + "ID")?.Value ?? string.Empty,
|
?.Element(Ram + "ID")?.Value ?? string.Empty,
|
||||||
BuyerName = agreement?.Element(Ram + "BuyerTradeParty")
|
BuyerName = agreement?.Element(Ram + "BuyerTradeParty")
|
||||||
?.Element(Ram + "Name")?.Value ?? string.Empty,
|
?.Element(Ram + "Name")?.Value ?? string.Empty,
|
||||||
|
|
||||||
CurrencyCode = settlement?.Element(Ram + "InvoiceCurrencyCode")?.Value ?? "EUR",
|
CurrencyCode = settlement?.Element(Ram + "InvoiceCurrencyCode")?.Value ?? "EUR",
|
||||||
TotalAmount = ParseDecimal(settlement?.Element(Ram + "SpecifiedTradeSettlementHeaderMonetarySummation")
|
TotalAmount = ParseDecimal(settlement
|
||||||
|
?.Element(Ram + "SpecifiedTradeSettlementHeaderMonetarySummation")
|
||||||
?.Element(Ram + "GrandTotalAmount")?.Value),
|
?.Element(Ram + "GrandTotalAmount")?.Value),
|
||||||
TaxAmount = ParseDecimal(settlement?.Element(Ram + "SpecifiedTradeSettlementHeaderMonetarySummation")
|
TaxAmount = ParseDecimal(settlement
|
||||||
|
?.Element(Ram + "SpecifiedTradeSettlementHeaderMonetarySummation")
|
||||||
?.Element(Ram + "TaxTotalAmount")?.Value),
|
?.Element(Ram + "TaxTotalAmount")?.Value),
|
||||||
Iban = settlement?.Element(Ram + "SpecifiedTradeSettlementPaymentMeans")
|
Iban = settlement?.Element(Ram + "SpecifiedTradeSettlementPaymentMeans")
|
||||||
?.Element(Ram + "PayeePartyCreditorFinancialAccount")
|
?.Element(Ram + "PayeePartyCreditorFinancialAccount")
|
||||||
@@ -49,10 +106,51 @@ public class ZugferdParserService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── ZUGFeRD v1.0 FeRD (CrossIndustryDocument:invoice:1p0) ───────────────────
|
||||||
|
private static ZugferdInvoice ParseV1Ferd(XElement root, string xml)
|
||||||
|
{
|
||||||
|
var rsm = RsmFerd1;
|
||||||
|
var ram = RamV12;
|
||||||
|
var udt = UdtV15;
|
||||||
|
|
||||||
|
var header = root.Element(rsm + "HeaderExchangedDocument");
|
||||||
|
var trade = root.Element(rsm + "SpecifiedSupplyChainTradeTransaction");
|
||||||
|
var agreement = trade?.Element(ram + "ApplicableSupplyChainTradeAgreement");
|
||||||
|
var settlement = trade?.Element(ram + "ApplicableSupplyChainTradeSettlement");
|
||||||
|
|
||||||
|
var seller = agreement?.Element(ram + "SellerTradeParty");
|
||||||
|
var buyer = agreement?.Element(ram + "BuyerTradeParty");
|
||||||
|
var summation = settlement?.Element(ram + "SpecifiedTradeSettlementMonetarySummation");
|
||||||
|
var payment = settlement?.Element(ram + "SpecifiedTradeSettlementPaymentMeans");
|
||||||
|
|
||||||
|
var sellerTaxId = seller?
|
||||||
|
.Elements(ram + "SpecifiedTaxRegistration")
|
||||||
|
.FirstOrDefault(e => (string?)e.Element(ram + "ID")?.Attribute("schemeID") == "VA")
|
||||||
|
?.Element(ram + "ID")?.Value ?? string.Empty;
|
||||||
|
|
||||||
|
return new ZugferdInvoice
|
||||||
|
{
|
||||||
|
InvoiceNumber = header?.Element(ram + "ID")?.Value ?? string.Empty,
|
||||||
|
InvoiceDate = ParseDate(header?.Element(ram + "IssueDateTime")
|
||||||
|
?.Element(udt + "DateTimeString")?.Value),
|
||||||
|
SellerName = seller?.Element(ram + "Name")?.Value ?? string.Empty,
|
||||||
|
SellerTaxId = sellerTaxId,
|
||||||
|
BuyerName = buyer?.Element(ram + "Name")?.Value ?? string.Empty,
|
||||||
|
CurrencyCode = settlement?.Element(ram + "InvoiceCurrencyCode")?.Value ?? "EUR",
|
||||||
|
TotalAmount = ParseDecimal(summation?.Element(ram + "GrandTotalAmount")?.Value),
|
||||||
|
TaxAmount = ParseDecimal(summation?.Element(ram + "TaxTotalAmount")?.Value),
|
||||||
|
Iban = payment?.Element(ram + "PayeePartyCreditorFinancialAccount")
|
||||||
|
?.Element(ram + "IBANID")?.Value ?? string.Empty,
|
||||||
|
RawXml = xml,
|
||||||
|
ImportedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||||
private static DateTime ParseDate(string? value)
|
private static DateTime ParseDate(string? value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value)) return DateTime.MinValue;
|
if (string.IsNullOrWhiteSpace(value)) return DateTime.MinValue;
|
||||||
return DateTime.TryParseExact(value, "yyyyMMdd",
|
return DateTime.TryParseExact(value.Trim(), "yyyyMMdd",
|
||||||
System.Globalization.CultureInfo.InvariantCulture,
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
System.Globalization.DateTimeStyles.None, out var dt) ? dt : DateTime.MinValue;
|
System.Globalization.DateTimeStyles.None, out var dt) ? dt : DateTime.MinValue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user