Files
DXApp/DXApp.TemplateKitProject/Services/ZugferdParserService.cs
OlgunR c45e837c2b 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.
2026-05-22 14:01:07 +02:00

162 lines
8.7 KiB
C#

namespace DXApp.TemplateKitProject.Services;
using DXApp.TemplateKitProject.Models;
using System.Xml.Linq;
public class ZugferdParserService
{
// ZUGFeRD v1 Namespace
private static readonly XNamespace RsmV1 = "urn:un:unece:uncefact:data:standard:CBFBUY:5";
// 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";
// 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)
{
var doc = XDocument.Parse(xml);
var root = doc.Root!;
if (root.Name.Namespace == RsmV1) return ParseV1(root, xml);
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 settlement = trade?.Element(Ram + "ApplicableHeaderTradeSettlement");
return new ZugferdInvoice
{
InvoiceNumber = header?.Element(Ram + "ID")?.Value ?? string.Empty,
InvoiceDate = ParseDate(header?.Element(Ram + "IssueDateTime")
?.Element(Udt + "DateTimeString")?.Value),
SellerName = agreement?.Element(Ram + "SellerTradeParty")
?.Element(Ram + "Name")?.Value ?? string.Empty,
SellerTaxId = agreement?.Element(Ram + "SellerTradeParty")
?.Element(Ram + "SpecifiedTaxRegistration")
?.Element(Ram + "ID")?.Value ?? string.Empty,
BuyerName = agreement?.Element(Ram + "BuyerTradeParty")
?.Element(Ram + "Name")?.Value ?? string.Empty,
CurrencyCode = settlement?.Element(Ram + "InvoiceCurrencyCode")?.Value ?? "EUR",
TotalAmount = ParseDecimal(settlement
?.Element(Ram + "SpecifiedTradeSettlementHeaderMonetarySummation")
?.Element(Ram + "GrandTotalAmount")?.Value),
TaxAmount = ParseDecimal(settlement
?.Element(Ram + "SpecifiedTradeSettlementHeaderMonetarySummation")
?.Element(Ram + "TaxTotalAmount")?.Value),
Iban = settlement?.Element(Ram + "SpecifiedTradeSettlementPaymentMeans")
?.Element(Ram + "PayeePartyCreditorFinancialAccount")
?.Element(Ram + "IBANID")?.Value ?? string.Empty,
RawXml = xml,
ImportedAt = DateTime.UtcNow
};
}
// ── 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)
{
if (string.IsNullOrWhiteSpace(value)) return DateTime.MinValue;
return DateTime.TryParseExact(value.Trim(), "yyyyMMdd",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out var dt) ? dt : DateTime.MinValue;
}
private static decimal ParseDecimal(string? value)
=> decimal.TryParse(value,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var d) ? d : 0m;
}