From 3c61b254b02f2d64e7272d325366a6fe08fefe3b Mon Sep 17 00:00:00 2001 From: ufoch <> Date: Wed, 10 Sep 2025 14:57:37 +0200 Subject: [PATCH 1/3] Added BT-19 AccountingCost to UBL output --- ZUGFeRD/InvoiceDescriptor22UBLWriter.cs | 32 ++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/ZUGFeRD/InvoiceDescriptor22UBLWriter.cs b/ZUGFeRD/InvoiceDescriptor22UBLWriter.cs index 2aa67d03..3573462a 100644 --- a/ZUGFeRD/InvoiceDescriptor22UBLWriter.cs +++ b/ZUGFeRD/InvoiceDescriptor22UBLWriter.cs @@ -137,6 +137,22 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream, ZUGFeRDFo _Writer.WriteElementString("cbc", "TaxCurrencyCode", this._Descriptor.TaxCurrency.Value.EnumToString()); } + // BT-19 + if (this._Descriptor.AnyReceivableSpecifiedTradeAccountingAccounts()) + { + foreach (ReceivableSpecifiedTradeAccountingAccount traceAccountingAccount in this._Descriptor.GetReceivableSpecifiedTradeAccountingAccounts()) + { + if (string.IsNullOrWhiteSpace(traceAccountingAccount.TradeAccountID)) + { + continue; + } + + _Writer.WriteOptionalElementString("cbc", "AccountingCost", traceAccountingAccount.TradeAccountID); + + break; // Cardinality 0..1 + } + } + _Writer.WriteOptionalElementString("cbc", "BuyerReference", this._Descriptor.ReferenceOrderNo); if (this._Descriptor.BillingPeriodEnd.HasValue || this._Descriptor.BillingPeriodEnd.HasValue) @@ -458,7 +474,7 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream, ZUGFeRDFo } #region AllowanceCharge - foreach(TradeAllowance allowance in descriptor.GetTradeAllowances()) + foreach (TradeAllowance allowance in descriptor.GetTradeAllowances()) { _WriteDocumentLevelAllowanceCharges(_Writer, allowance); } // !foreach(allowance) @@ -678,7 +694,7 @@ private void _WriteTradeLineItem(TradeLineItem tradeLineItem, bool isInvoice = t { _WriteItemLevelSpecifiedTradeAllowanceCharge(specifiedTradeAllowanceCharge); } - + foreach (var specifiedTradeAllowanceCharge in tradeLineItem.GetSpecifiedTradeCharges()) { _WriteItemLevelSpecifiedTradeAllowanceCharge(specifiedTradeAllowanceCharge); @@ -726,8 +742,8 @@ private void _WriteTradeLineItem(TradeLineItem tradeLineItem, bool isInvoice = t _WriteComment(_Writer, options, InvoiceCommentConstants.NetPriceProductTradePriceComment); _Writer.WriteStartElement("cbc", "PriceAmount"); _Writer.WriteAttributeString("currencyID", this._Descriptor.Currency.EnumToString()); - // UBL-DT-01 explicitly excempts the price amount from the 2 decimal rule for amount elements, - // thus allowing for 4 decimal places (needed for e.g. fuel prices) + // UBL-DT-01 explicitly excempts the price amount from the 2 decimal rule for amount elements, + // thus allowing for 4 decimal places (needed for e.g. fuel prices) _Writer.WriteValue(_formatDecimal(tradeLineItem.NetUnitPrice, 4)); _Writer.WriteEndElement(); @@ -753,7 +769,7 @@ private void _WriteTradeLineItem(TradeLineItem tradeLineItem, bool isInvoice = t else { _Writer.WriteElementString("cbc", "ChargeIndicator", "true"); - } + } _Writer.WriteStartElement("cbc", "Amount"); // BT-147 _Writer.WriteAttributeString("currencyID", this._Descriptor.Currency.EnumToString()); @@ -799,7 +815,7 @@ private void _WriteItemLevelSpecifiedTradeAllowanceCharge(AbstractTradeAllowance _Writer.WriteOptionalElementString("cbc", "AllowanceChargeReasonCode", charge.ReasonCode.EnumToString()); // BT-145 break; } - + _Writer.WriteOptionalElementString("cbc", "AllowanceChargeReason", specifiedTradeAllowanceCharge.Reason); // BT-139, BT-144 @@ -884,7 +900,7 @@ private void _writeOptionalParty(ProfileAwareXmlTextWriter writer, PartyTypes pa { switch (partyType) { - case PartyTypes.SellerTradeParty: + case PartyTypes.SellerTradeParty: writer.WriteStartElement("cac", "AccountingSupplierParty", this._Descriptor.Profile); break; case PartyTypes.BuyerTradeParty: @@ -1096,7 +1112,7 @@ private void _writeNotes(ProfileAwareXmlTextWriter writer, List notes) } } } // !_writeNotes() - + private void _writeOptionalAmount(ProfileAwareXmlTextWriter writer, string prefix, string tagName, decimal? value, int numDecimals = 2, bool forceCurrency = false, Profile profile = Profile.Unknown) { if (!value.HasValue) From 36489f65ac2e246a2e2e53e712d042eac2c596ab Mon Sep 17 00:00:00 2001 From: ufoch <> Date: Wed, 12 Nov 2025 13:40:42 +0100 Subject: [PATCH 2/3] Added test case for Accounting Cost --- ZUGFeRD.Test/ZUGFeRD22Tests.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ZUGFeRD.Test/ZUGFeRD22Tests.cs b/ZUGFeRD.Test/ZUGFeRD22Tests.cs index ee3d9041..1a2db864 100644 --- a/ZUGFeRD.Test/ZUGFeRD22Tests.cs +++ b/ZUGFeRD.Test/ZUGFeRD22Tests.cs @@ -3623,5 +3623,26 @@ public void TestRSMInvoice() Assert.AreEqual(desc.InvoiceNo, "0815-99-1-a"); Assert.AreEqual(desc.InvoiceDate, new DateTime(2020, 06, 21)); } // !TestRSMInvoice() + + [TestMethod] + public void TestAccountingCost() + { + var d = new InvoiceDescriptor(); + d.Type = InvoiceType.Invoice; + d.InvoiceNo = "471103"; + d.Currency = CurrencyCodes.EUR; + d.InvoiceDate = new DateTime(2025, 11, 11); + d.AddReceivableSpecifiedTradeAccountingAccount("BRE"); + + using (var stream = new MemoryStream()) + { + d.Save(stream, ZUGFeRDVersion.Version23, Profile.XRechnung, ZUGFeRDFormats.UBL); + + stream.Seek(0, SeekOrigin.Begin); + + var d2 = InvoiceDescriptor.Load(stream); + Assert.IsTrue(d2.GetReceivableSpecifiedTradeAccountingAccounts().Any(account => account.TradeAccountID == "BRE")); + } + } // !TestAccountingCost() } } From b4e806025166063d38b0e8040d393cb137eaaf07 Mon Sep 17 00:00:00 2001 From: ufoch <> Date: Wed, 12 Nov 2025 13:43:08 +0100 Subject: [PATCH 3/3] Added UBL reader for Accounting Cost --- ZUGFeRD/InvoiceDescriptor22UblReader.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/ZUGFeRD/InvoiceDescriptor22UblReader.cs b/ZUGFeRD/InvoiceDescriptor22UblReader.cs index 084a0fe8..3e00132d 100644 --- a/ZUGFeRD/InvoiceDescriptor22UblReader.cs +++ b/ZUGFeRD/InvoiceDescriptor22UblReader.cs @@ -406,14 +406,12 @@ public override InvoiceDescriptor Load(Stream stream) retval.TotalPrepaidAmount = XmlUtils.NodeAsDecimal(doc.DocumentElement, "//cac:LegalMonetaryTotal/cbc:PrepaidAmount", nsmgr); retval.DuePayableAmount = XmlUtils.NodeAsDecimal(doc.DocumentElement, "//cac:LegalMonetaryTotal/cbc:PayableAmount", nsmgr); - // TODO: Find value //foreach (XmlNode node in doc.SelectNodes("//ram:ApplicableHeaderTradeSettlement/ram:ReceivableSpecifiedTradeAccountingAccount", nsmgr)) - //{ - // retval._ReceivableSpecifiedTradeAccountingAccounts.Add(new ReceivableSpecifiedTradeAccountingAccount() - // { - // TradeAccountID = XmlUtils.NodeAsString(node, ".//ram:ID", nsmgr), - // TradeAccountTypeCode = (AccountingAccountTypeCodes)_nodeAsInt(node, ".//ram:TypeCode", nsmgr), - // }); - //} + foreach (XmlNode node in doc.SelectNodes("//cbc:AccountingCost", nsmgr)) + { + string content = XmlUtils.NodeAsString(node, ".", nsmgr); + if (string.IsNullOrWhiteSpace(content)) continue; + retval.AddReceivableSpecifiedTradeAccountingAccount(content); + } // TODO: Find value //retval.OrderDate = XmlUtils.NodeAsDateTime(doc.DocumentElement, "//cac:OrderReference/ram:BuyerOrderReferencedDocument/ram:FormattedIssueDateTime/qdt:DateTimeString", nsmgr); retval.OrderNo = XmlUtils.NodeAsString(doc.DocumentElement, "//cac:OrderReference/cbc:ID", nsmgr);