From d47fcde79108546de07702da53ebb91c9a3b87e6 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 24 Apr 2024 18:57:27 +0200 Subject: [PATCH 1/4] trade allowance charge overhaul --- ZUGFeRD-Test/ZUGFeRD20Tests.cs | 4 +- ZUGFeRD-Test/ZUGFeRD21Tests.cs | 17 +++++---- ZUGFeRD/InvoiceDescriptor.cs | 55 ++++++++++++++++++++++++++-- ZUGFeRD/InvoiceDescriptor1Writer.cs | 30 +++++++++------ ZUGFeRD/InvoiceDescriptor20Writer.cs | 26 ++++++++----- ZUGFeRD/InvoiceDescriptor21Writer.cs | 37 +++++++++++++------ ZUGFeRD/InvoiceValidator.cs | 4 +- ZUGFeRD/TradeAllowanceCharge.cs | 18 ++++++--- ZUGFeRD/TradeLineItem.cs | 40 ++++++++++---------- 9 files changed, 156 insertions(+), 75 deletions(-) diff --git a/ZUGFeRD-Test/ZUGFeRD20Tests.cs b/ZUGFeRD-Test/ZUGFeRD20Tests.cs index 429af5ba..3e6201cb 100644 --- a/ZUGFeRD-Test/ZUGFeRD20Tests.cs +++ b/ZUGFeRD-Test/ZUGFeRD20Tests.cs @@ -654,7 +654,7 @@ public void TestWriteAndReadExtended() Assert.AreEqual(timestamp.AddDays(14), loadedInvoice.BillingPeriodEnd); //TradeAllowanceCharges - var tradeAllowanceCharge = loadedInvoice.TradeAllowanceCharges.FirstOrDefault(i => i.Reason == "Reason for charge"); + var tradeAllowanceCharge = loadedInvoice.GetTradeAllowanceCharges().FirstOrDefault(i => i.Reason == "Reason for charge"); Assert.IsNotNull(tradeAllowanceCharge); Assert.IsTrue(tradeAllowanceCharge.ChargeIndicator); Assert.AreEqual("Reason for charge", tradeAllowanceCharge.Reason); @@ -747,7 +747,7 @@ public void TestWriteAndReadExtended() //Assert.AreEqual("987654", accountingAccount.TradeAccountID); - var lineItemTradeAllowanceCharge = loadedLineItem.TradeAllowanceCharges.FirstOrDefault(i => i.Reason == "Reason: UnitTest"); + var lineItemTradeAllowanceCharge = loadedLineItem.GetTradeAllowanceCharges().FirstOrDefault(i => i.Reason == "Reason: UnitTest"); Assert.IsNotNull(lineItemTradeAllowanceCharge); Assert.IsTrue(lineItemTradeAllowanceCharge.ChargeIndicator); Assert.AreEqual(10m, lineItemTradeAllowanceCharge.BasisAmount); diff --git a/ZUGFeRD-Test/ZUGFeRD21Tests.cs b/ZUGFeRD-Test/ZUGFeRD21Tests.cs index 1df79f0a..93d07100 100644 --- a/ZUGFeRD-Test/ZUGFeRD21Tests.cs +++ b/ZUGFeRD-Test/ZUGFeRD21Tests.cs @@ -147,17 +147,18 @@ public void TestReferenceExtendedInvoice() Assert.AreEqual(desc.TradeLineItems.Count, 6); Assert.AreEqual(desc.LineTotalAmount, 457.20m); - foreach (TradeAllowanceCharge charge in desc.TradeAllowanceCharges) + IList _tradeAllowanceCharges = desc.GetTradeAllowanceCharges(); + foreach (TradeAllowanceCharge charge in _tradeAllowanceCharges) { Assert.AreEqual(charge.Tax.TypeCode, TaxTypes.VAT); Assert.AreEqual(charge.Tax.CategoryCode, TaxCategoryCodes.S); } - Assert.AreEqual(desc.TradeAllowanceCharges.Count, 4); - Assert.AreEqual(desc.TradeAllowanceCharges[0].Tax.Percent, 19m); - Assert.AreEqual(desc.TradeAllowanceCharges[1].Tax.Percent, 7m); - Assert.AreEqual(desc.TradeAllowanceCharges[2].Tax.Percent, 19m); - Assert.AreEqual(desc.TradeAllowanceCharges[3].Tax.Percent, 7m); + Assert.AreEqual(_tradeAllowanceCharges.Count, 4); + Assert.AreEqual(_tradeAllowanceCharges[0].Tax.Percent, 19m); + Assert.AreEqual(_tradeAllowanceCharges[1].Tax.Percent, 7m); + Assert.AreEqual(_tradeAllowanceCharges[2].Tax.Percent, 19m); + Assert.AreEqual(_tradeAllowanceCharges[3].Tax.Percent, 7m); Assert.AreEqual(desc.ServiceCharges.Count, 1); Assert.AreEqual(desc.ServiceCharges[0].Tax.TypeCode, TaxTypes.VAT); @@ -1510,7 +1511,7 @@ public void TestWriteAndReadExtended() Assert.AreEqual(timestamp.AddDays(14), loadedInvoice.BillingPeriodEnd); //TradeAllowanceCharges - var tradeAllowanceCharge = loadedInvoice.TradeAllowanceCharges.FirstOrDefault(i => i.Reason == "Reason for charge"); + var tradeAllowanceCharge = loadedInvoice.GetTradeAllowanceCharges().FirstOrDefault(i => i.Reason == "Reason for charge"); Assert.IsNotNull(tradeAllowanceCharge); Assert.IsTrue(tradeAllowanceCharge.ChargeIndicator); Assert.AreEqual("Reason for charge", tradeAllowanceCharge.Reason); @@ -1602,7 +1603,7 @@ public void TestWriteAndReadExtended() Assert.AreEqual("987654", accountingAccount.TradeAccountID); - var lineItemTradeAllowanceCharge = loadedLineItem.TradeAllowanceCharges.FirstOrDefault(i => i.Reason == "Reason: UnitTest"); + var lineItemTradeAllowanceCharge = loadedLineItem.GetTradeAllowanceCharges().FirstOrDefault(i => i.Reason == "Reason: UnitTest"); Assert.IsNotNull(lineItemTradeAllowanceCharge); Assert.IsTrue(lineItemTradeAllowanceCharge.ChargeIndicator); Assert.AreEqual(10m, lineItemTradeAllowanceCharge.BasisAmount); diff --git a/ZUGFeRD/InvoiceDescriptor.cs b/ZUGFeRD/InvoiceDescriptor.cs index 984092ca..7573614a 100644 --- a/ZUGFeRD/InvoiceDescriptor.cs +++ b/ZUGFeRD/InvoiceDescriptor.cs @@ -251,9 +251,10 @@ public class InvoiceDescriptor public List ServiceCharges { get; set; } = new List(); /// - /// Detailed information on discounts and charges + /// Detailed information on discounts and charges. + /// This field is marked as private now, please use GetTradeAllowanceCharges() to retrieve all trade allowance charges /// - public List TradeAllowanceCharges { get; set; } = new List(); + private List _TradeAllowanceCharges { get; set; } = new List(); /// /// Detailed information about payment terms @@ -697,9 +698,9 @@ public void AddLogisticsServiceCharge(decimal amount, string description, TaxTyp /// VAT type code for document level allowance/ charge /// VAT type code for document level allowance/ charge /// VAT rate for the allowance - public void AddTradeAllowanceCharge(bool isDiscount, decimal basisAmount, CurrencyCodes currency, decimal actualAmount, string reason, TaxTypes taxTypeCode, TaxCategoryCodes taxCategoryCode, decimal taxPercent) + public void AddTradeAllowanceCharge(bool isDiscount, decimal? basisAmount, CurrencyCodes currency, decimal actualAmount, string reason, TaxTypes taxTypeCode, TaxCategoryCodes taxCategoryCode, decimal taxPercent) { - this.TradeAllowanceCharges.Add(new TradeAllowanceCharge() + this._TradeAllowanceCharges.Add(new TradeAllowanceCharge() { ChargeIndicator = !isDiscount, Reason = reason, @@ -707,6 +708,7 @@ public void AddTradeAllowanceCharge(bool isDiscount, decimal basisAmount, Curren ActualAmount = actualAmount, Currency = currency, Amount = actualAmount, + ChargePercentage = null, Tax = new Tax() { CategoryCode = taxCategoryCode, @@ -717,6 +719,51 @@ public void AddTradeAllowanceCharge(bool isDiscount, decimal basisAmount, Curren } // !AddTradeAllowanceCharge() + /// + /// Adds an allowance or charge on document level. + /// + /// Allowance represents a discount whereas charge represents a surcharge. + /// + /// Marks if the allowance charge is a discount. Please note that in contrary to this function, the xml file indicated a surcharge, not a discount (value will be inverted) + /// Base amount (basis of allowance) + /// Curency of the allowance + /// Actual allowance charge amount + /// Actual allowance charge amount + /// Reason for the allowance + /// VAT type code for document level allowance/ charge + /// VAT type code for document level allowance/ charge + /// VAT rate for the allowance + public void AddTradeAllowanceCharge(bool isDiscount, decimal? basisAmount, CurrencyCodes currency, decimal actualAmount, decimal chargePercentage, string reason, TaxTypes taxTypeCode, TaxCategoryCodes taxCategoryCode, decimal taxPercent) + { + this._TradeAllowanceCharges.Add(new TradeAllowanceCharge() + { + ChargeIndicator = !isDiscount, + Reason = reason, + BasisAmount = basisAmount, + ActualAmount = actualAmount, + Currency = currency, + Amount = actualAmount, + ChargePercentage = chargePercentage, + Tax = new Tax() + { + CategoryCode = taxCategoryCode, + TypeCode = taxTypeCode, + Percent = taxPercent + } + }); + } // !AddTradeAllowanceCharge() + + + /// + /// Returns all existing trade allowance charges + /// + /// + public IList GetTradeAllowanceCharges() + { + return this._TradeAllowanceCharges; + } // !GetTradeAllowanceCharges() + + public void SetTradePaymentTerms(string description, DateTime? dueDate = null) { this.PaymentTerms = new PaymentTerms() diff --git a/ZUGFeRD/InvoiceDescriptor1Writer.cs b/ZUGFeRD/InvoiceDescriptor1Writer.cs index 4b78ac1d..2e392b3a 100644 --- a/ZUGFeRD/InvoiceDescriptor1Writer.cs +++ b/ZUGFeRD/InvoiceDescriptor1Writer.cs @@ -303,19 +303,22 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) _writeOptionalTaxes(Writer); - if ((this.Descriptor.TradeAllowanceCharges != null) && (this.Descriptor.TradeAllowanceCharges.Count > 0)) + if ((this.Descriptor.GetTradeAllowanceCharges() != null) && (this.Descriptor.GetTradeAllowanceCharges().Count > 0)) { - foreach (TradeAllowanceCharge tradeAllowanceCharge in this.Descriptor.TradeAllowanceCharges) + foreach (TradeAllowanceCharge tradeAllowanceCharge in this.Descriptor.GetTradeAllowanceCharges()) { Writer.WriteStartElement("ram:SpecifiedTradeAllowanceCharge"); Writer.WriteStartElement("ram:ChargeIndicator", Profile.Comfort | Profile.Extended); Writer.WriteElementString("udt:Indicator", tradeAllowanceCharge.ChargeIndicator ? "true" : "false"); Writer.WriteEndElement(); // !ram:ChargeIndicator - Writer.WriteStartElement("ram:BasisAmount", Profile.Extended); - Writer.WriteAttributeString("currencyID", tradeAllowanceCharge.Currency.EnumToString()); - Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount)); - Writer.WriteEndElement(); + if (tradeAllowanceCharge.BasisAmount.HasValue) + { + Writer.WriteStartElement("ram:BasisAmount", Profile.Extended); + Writer.WriteAttributeString("currencyID", tradeAllowanceCharge.Currency.EnumToString()); + Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount.Value)); + Writer.WriteEndElement(); + } Writer.WriteStartElement("ram:ActualAmount", Profile.Comfort | Profile.Extended); Writer.WriteAttributeString("currencyID", tradeAllowanceCharge.Currency.EnumToString()); @@ -461,7 +464,7 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) _writeElementWithAttribute(Writer, "ram:BasisQuantity", "unitCode", tradeLineItem.UnitCode.EnumToString(), _formatDecimal(tradeLineItem.UnitQuantity.Value, 4)); } - foreach (TradeAllowanceCharge tradeAllowanceCharge in tradeLineItem.TradeAllowanceCharges) + foreach (TradeAllowanceCharge tradeAllowanceCharge in tradeLineItem.GetTradeAllowanceCharges()) { Writer.WriteStartElement("ram:AppliedTradeAllowanceCharge"); @@ -469,10 +472,13 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) Writer.WriteElementString("udt:Indicator", tradeAllowanceCharge.ChargeIndicator ? "true" : "false"); Writer.WriteEndElement(); // !ram:ChargeIndicator - Writer.WriteStartElement("ram:BasisAmount", Profile.Extended); - Writer.WriteAttributeString("currencyID", tradeAllowanceCharge.Currency.EnumToString()); - Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount, 4)); - Writer.WriteEndElement(); + if (tradeAllowanceCharge.BasisAmount.HasValue) + { + Writer.WriteStartElement("ram:BasisAmount", Profile.Extended); + Writer.WriteAttributeString("currencyID", tradeAllowanceCharge.Currency.EnumToString()); + Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount.Value, 4)); + Writer.WriteEndElement(); + } Writer.WriteStartElement("ram:ActualAmount", Profile.Comfort | Profile.Extended); Writer.WriteAttributeString("currencyID", tradeAllowanceCharge.Currency.EnumToString()); Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.ActualAmount, 4)); @@ -636,7 +642,7 @@ internal override bool Validate(InvoiceDescriptor descriptor, bool throwExceptio private void _writeOptionalAmount(ProfileAwareXmlTextWriter writer, string tagName, decimal? value, int numDecimals = 2) { - if (value.HasValue && (value.Value != decimal.MinValue)) + if (value.HasValue) { writer.WriteStartElement(tagName); writer.WriteAttributeString("currencyID", this.Descriptor.Currency.EnumToString()); diff --git a/ZUGFeRD/InvoiceDescriptor20Writer.cs b/ZUGFeRD/InvoiceDescriptor20Writer.cs index 3a2b9574..e9c19afa 100644 --- a/ZUGFeRD/InvoiceDescriptor20Writer.cs +++ b/ZUGFeRD/InvoiceDescriptor20Writer.cs @@ -232,7 +232,7 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) _writeElementWithAttribute(Writer, "ram:BasisQuantity", "unitCode", tradeLineItem.UnitCode.EnumToString(), _formatDecimal(tradeLineItem.UnitQuantity.Value, 4)); } - foreach (TradeAllowanceCharge tradeAllowanceCharge in tradeLineItem.TradeAllowanceCharges) + foreach (TradeAllowanceCharge tradeAllowanceCharge in tradeLineItem.GetTradeAllowanceCharges()) { Writer.WriteStartElement("ram:AppliedTradeAllowanceCharge"); @@ -240,9 +240,12 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) Writer.WriteElementString("udt:Indicator", tradeAllowanceCharge.ChargeIndicator ? "true" : "false"); Writer.WriteEndElement(); // !ram:ChargeIndicator - Writer.WriteStartElement("ram:BasisAmount"); - Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount, 4)); - Writer.WriteEndElement(); + if (tradeAllowanceCharge.BasisAmount.HasValue) + { + Writer.WriteStartElement("ram:BasisAmount"); + Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount.Value, 4)); + Writer.WriteEndElement(); + } Writer.WriteStartElement("ram:ActualAmount"); Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.ActualAmount, 4)); Writer.WriteEndElement(); @@ -648,18 +651,21 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) #endregion // 13. SpecifiedTradeAllowanceCharge (optional) - if ((this.Descriptor.TradeAllowanceCharges != null) && (this.Descriptor.TradeAllowanceCharges.Count > 0)) + if ((this.Descriptor.GetTradeAllowanceCharges() != null) && (this.Descriptor.GetTradeAllowanceCharges().Count > 0)) { - foreach (TradeAllowanceCharge tradeAllowanceCharge in this.Descriptor.TradeAllowanceCharges) + foreach (TradeAllowanceCharge tradeAllowanceCharge in this.Descriptor.GetTradeAllowanceCharges()) { Writer.WriteStartElement("ram:SpecifiedTradeAllowanceCharge"); Writer.WriteStartElement("ram:ChargeIndicator"); Writer.WriteElementString("udt:Indicator", tradeAllowanceCharge.ChargeIndicator ? "true" : "false"); Writer.WriteEndElement(); // !ram:ChargeIndicator - Writer.WriteStartElement("ram:BasisAmount"); - Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount)); - Writer.WriteEndElement(); + if (tradeAllowanceCharge.BasisAmount.HasValue) + { + Writer.WriteStartElement("ram:BasisAmount"); + Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount.Value)); + Writer.WriteEndElement(); + } Writer.WriteStartElement("ram:ActualAmount"); Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.ActualAmount)); @@ -770,7 +776,7 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) private void _writeOptionalAmount(ProfileAwareXmlTextWriter writer, string tagName, decimal? value, int numDecimals = 2, bool forceCurrency = false, Profile profile = Profile.Unknown) { - if (value.HasValue && (value.Value != decimal.MinValue)) + if (value.HasValue) { writer.WriteStartElement(tagName, profile); if (forceCurrency) diff --git a/ZUGFeRD/InvoiceDescriptor21Writer.cs b/ZUGFeRD/InvoiceDescriptor21Writer.cs index ece2245d..83e0687d 100644 --- a/ZUGFeRD/InvoiceDescriptor21Writer.cs +++ b/ZUGFeRD/InvoiceDescriptor21Writer.cs @@ -260,7 +260,7 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) #region GrossPriceProductTradePrice (Comfort, Extended, XRechnung) // BT-148 - if (tradeLineItem.GrossUnitPrice.HasValue || (tradeLineItem.TradeAllowanceCharges.Count > 0)) + if (tradeLineItem.GrossUnitPrice.HasValue || (tradeLineItem.GetTradeAllowanceCharges().Count > 0)) { Writer.WriteStartElement("ram:GrossPriceProductTradePrice", Profile.Comfort | Profile.Extended | Profile.XRechnung1 | Profile.XRechnung); _writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.GrossUnitPrice, 4); @@ -269,7 +269,7 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) _writeElementWithAttribute(Writer, "ram:BasisQuantity", "unitCode", tradeLineItem.UnitCode.EnumToString(), _formatDecimal(tradeLineItem.UnitQuantity.Value, 4)); } - foreach (TradeAllowanceCharge tradeAllowanceCharge in tradeLineItem.TradeAllowanceCharges) + foreach (TradeAllowanceCharge tradeAllowanceCharge in tradeLineItem.GetTradeAllowanceCharges()) { Writer.WriteStartElement("ram:AppliedTradeAllowanceCharge"); @@ -279,10 +279,22 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) Writer.WriteEndElement(); // !ram:ChargeIndicator #endregion + #region ChargePercentage + if (tradeAllowanceCharge.ChargePercentage.HasValue) + { + Writer.WriteStartElement("ram:CalculationPercent", profile: Profile.Extended | Profile.XRechnung); + Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.ChargePercentage.Value, 2)); + Writer.WriteEndElement(); + } + #endregion + #region BasisAmount - Writer.WriteStartElement("ram:BasisAmount", profile: Profile.Extended); // not in XRechnung, according to CII-SR-123 - Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount, 2)); - Writer.WriteEndElement(); + if (tradeAllowanceCharge.BasisAmount.HasValue) + { + Writer.WriteStartElement("ram:BasisAmount", profile: Profile.Extended); // not in XRechnung, according to CII-SR-123 + Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount.Value, 2)); + Writer.WriteEndElement(); + } #endregion #region ActualAmount @@ -809,18 +821,21 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) #endregion // 13. SpecifiedTradeAllowanceCharge (optional) - if ((this.Descriptor.TradeAllowanceCharges != null) && (this.Descriptor.TradeAllowanceCharges.Count > 0)) + if ((this.Descriptor.GetTradeAllowanceCharges() != null) && (this.Descriptor.GetTradeAllowanceCharges().Count > 0)) { - foreach (TradeAllowanceCharge tradeAllowanceCharge in this.Descriptor.TradeAllowanceCharges) + foreach (TradeAllowanceCharge tradeAllowanceCharge in this.Descriptor.GetTradeAllowanceCharges()) { Writer.WriteStartElement("ram:SpecifiedTradeAllowanceCharge"); Writer.WriteStartElement("ram:ChargeIndicator"); Writer.WriteElementString("udt:Indicator", tradeAllowanceCharge.ChargeIndicator ? "true" : "false"); Writer.WriteEndElement(); // !ram:ChargeIndicator - Writer.WriteStartElement("ram:BasisAmount", profile: Profile.Comfort | Profile.Extended | Profile.XRechnung1 | Profile.XRechnung); - Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount)); - Writer.WriteEndElement(); + if (tradeAllowanceCharge.BasisAmount.HasValue) + { + Writer.WriteStartElement("ram:BasisAmount", profile: Profile.Comfort | Profile.Extended | Profile.XRechnung1 | Profile.XRechnung); + Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.BasisAmount.Value)); + Writer.WriteEndElement(); + } Writer.WriteStartElement("ram:ActualAmount"); Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.ActualAmount, 2)); @@ -1036,7 +1051,7 @@ internal override bool Validate(InvoiceDescriptor descriptor, bool throwExceptio private void _writeOptionalAmount(ProfileAwareXmlTextWriter writer, string tagName, decimal? value, int numDecimals = 2, bool forceCurrency = false, Profile profile = Profile.Unknown) { - if (value.HasValue && (value.Value != decimal.MinValue)) + if (value.HasValue) { writer.WriteStartElement(tagName, profile); if (forceCurrency) diff --git a/ZUGFeRD/InvoiceValidator.cs b/ZUGFeRD/InvoiceValidator.cs index 0acb77a2..a57c49ec 100644 --- a/ZUGFeRD/InvoiceValidator.cs +++ b/ZUGFeRD/InvoiceValidator.cs @@ -98,7 +98,7 @@ public static List Validate(InvoiceDescriptor descriptor) retval.Add("Adding tax amounts from invoice allowance charge..."); decimal allowanceTotal = 0.0m; - foreach (TradeAllowanceCharge charge in descriptor.TradeAllowanceCharges) + foreach (TradeAllowanceCharge charge in descriptor.GetTradeAllowanceCharges()) { retval.Add(String.Format("==> added {0:0.00} to {1:0.00}%", -charge.Amount, charge.Tax.Percent)); @@ -149,7 +149,7 @@ public static List Validate(InvoiceDescriptor descriptor) } decimal _allowanceTotal = 0m; - foreach(TradeAllowanceCharge allowance in descriptor.TradeAllowanceCharges) + foreach(TradeAllowanceCharge allowance in descriptor.GetTradeAllowanceCharges()) { _allowanceTotal += allowance.ActualAmount; } diff --git a/ZUGFeRD/TradeAllowanceCharge.cs b/ZUGFeRD/TradeAllowanceCharge.cs index db3c61bb..7ce22327 100644 --- a/ZUGFeRD/TradeAllowanceCharge.cs +++ b/ZUGFeRD/TradeAllowanceCharge.cs @@ -20,6 +20,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Xml.Linq; namespace s2industries.ZUGFeRD { @@ -49,26 +50,33 @@ public class TradeAllowanceCharge : Charge /// /// In case of a discount (BG-27) the value of the ChargeIndicators has to be "false". In case of a surcharge (BG-28) the value of the ChargeIndicators has to be "true". /// - public bool ChargeIndicator { get; set; } + public bool ChargeIndicator { get; internal set; } /// /// The reason for the surcharge or discount in written form /// - public string Reason { get; set; } + public string Reason { get; internal set; } /// /// The base amount that may be used in conjunction with the percentage of the invoice line discount to calculate the amount of the invoice line discount /// - public decimal BasisAmount { get; set; } + public decimal? BasisAmount { get; internal set; } /// /// Currency that is used for representing BasisAmount and ActualAmount /// - public CurrencyCodes Currency { get; set; } + public CurrencyCodes Currency { get; internal set; } /// /// The amount of the discount / surcharge or discount without VAT /// - public decimal ActualAmount { get; set; } + public decimal ActualAmount { get; internal set; } + + /// + /// The percentage that may be used in conjunction with the document level discount base amount, to calculate the + /// document level discount amount. + /// BT-101 + /// + public decimal? ChargePercentage { get; internal set; } } } diff --git a/ZUGFeRD/TradeLineItem.cs b/ZUGFeRD/TradeLineItem.cs index 803cc81d..072dcaa9 100644 --- a/ZUGFeRD/TradeLineItem.cs +++ b/ZUGFeRD/TradeLineItem.cs @@ -34,7 +34,7 @@ public class TradeLineItem /// The global identifier of the article is a globally unique identifier of the product being assigned to it by its /// producer, bases on the rules of a global standardisation body. /// - public GlobalID GlobalID { get; set; } + public GlobalID GlobalID { get; set; } = new GlobalID(); /// /// An identification of the item assigned by the seller. @@ -147,36 +147,24 @@ public class TradeLineItem /// /// Details of an additional document reference /// - public List AdditionalReferencedDocuments { get; set; } + public List AdditionalReferencedDocuments { get; set; } = new List(); - /// + /// /// A group of business terms providing information about the applicable surcharges or discounts on the total amount of the invoice + /// + /// Now private. Please use GetTradeAllowanceCharges() instead /// - public List TradeAllowanceCharges { get; set; } + private List _TradeAllowanceCharges { get; set; } = new List(); /// /// Detailed information on the accounting reference /// - public List ReceivableSpecifiedTradeAccountingAccounts { get; set; } + public List ReceivableSpecifiedTradeAccountingAccounts { get; set; } = new List(); /// /// Additional product information /// - public List ApplicableProductCharacteristics { get; set; } - - /// - /// Initializes a new/ empty trade line item - /// - public TradeLineItem() - { - this.NetUnitPrice = decimal.MinValue; - this.GrossUnitPrice = decimal.MinValue; - this.GlobalID = new GlobalID(); - this.TradeAllowanceCharges = new List(); - this.AdditionalReferencedDocuments = new List(); - this.ReceivableSpecifiedTradeAccountingAccounts = new List(); - this.ApplicableProductCharacteristics = new List(); - } + public List ApplicableProductCharacteristics { get; set; } = new List(); /// @@ -189,7 +177,7 @@ public TradeLineItem() /// Reason for the allowance or surcharge public void AddTradeAllowanceCharge(bool isDiscount, CurrencyCodes currency, decimal basisAmount, decimal actualAmount, string reason) { - this.TradeAllowanceCharges.Add(new TradeAllowanceCharge() + this._TradeAllowanceCharges.Add(new TradeAllowanceCharge() { ChargeIndicator = !isDiscount, Currency = currency, @@ -200,6 +188,16 @@ public void AddTradeAllowanceCharge(bool isDiscount, CurrencyCodes currency, dec } // !AddTradeAllowanceCharge() + /// + /// Returns all trade allowance charges for the trade line item + /// + /// + public IList GetTradeAllowanceCharges() + { + return this._TradeAllowanceCharges; + } // !GetTradeAllowanceCharges() + + public void SetDeliveryNoteReferencedDocument(string deliveryNoteId, DateTime? deliveryNoteDate) { this.DeliveryNoteReferencedDocument = new DeliveryNoteReferencedDocument() From 7935c0d9da1492cbe3b37698320e31a6b9374707 Mon Sep 17 00:00:00 2001 From: Stephan Date: Sun, 28 Apr 2024 19:01:59 +0200 Subject: [PATCH 2/4] bug fix --- ZUGFeRD/IInvoiceDescriptorReader.cs | 2 +- ZUGFeRD/InvoiceDescriptor21Reader.cs | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ZUGFeRD/IInvoiceDescriptorReader.cs b/ZUGFeRD/IInvoiceDescriptorReader.cs index 51d35692..1c4a530c 100644 --- a/ZUGFeRD/IInvoiceDescriptorReader.cs +++ b/ZUGFeRD/IInvoiceDescriptorReader.cs @@ -137,7 +137,7 @@ protected static int _nodeAsInt(XmlNode node, string xpath, XmlNamespaceManager /// /// reads the value from given xpath and interprets the value as decimal /// - protected static decimal? _nodeAsDecimal(XmlNode node, string xpath, XmlNamespaceManager nsmgr = null, decimal? defaultValue = null) + protected static decimal? _nodeAsDecimal(XmlNode node, string xpath, XmlNamespaceManager nsmgr = null, decimal? defaultValue = default(decimal?)) { if (node == null) { diff --git a/ZUGFeRD/InvoiceDescriptor21Reader.cs b/ZUGFeRD/InvoiceDescriptor21Reader.cs index cc37ae80..a9c4f2c3 100644 --- a/ZUGFeRD/InvoiceDescriptor21Reader.cs +++ b/ZUGFeRD/InvoiceDescriptor21Reader.cs @@ -301,9 +301,10 @@ public override InvoiceDescriptor Load(Stream stream) foreach (XmlNode node in doc.SelectNodes("//ram:SpecifiedTradeAllowanceCharge", nsmgr)) { retval.AddTradeAllowanceCharge(!_nodeAsBool(node, ".//ram:ChargeIndicator", nsmgr), // wichtig: das not (!) beachten - _nodeAsDecimal(node, ".//ram:BasisAmount", nsmgr, 0).Value, + _nodeAsDecimal(node, ".//ram:BasisAmount", nsmgr, null).Value, retval.Currency, _nodeAsDecimal(node, ".//ram:ActualAmount", nsmgr, 0).Value, + _nodeAsDecimal(node, ".//ram:CalculationPercent", nsmgr, null).Value, _nodeAsString(node, ".//ram:Reason", nsmgr), default(TaxTypes).FromString(_nodeAsString(node, ".//ram:CategoryTradeTax/ram:TypeCode", nsmgr)), default(TaxCategoryCodes).FromString(_nodeAsString(node, ".//ram:CategoryTradeTax/ram:CategoryCode", nsmgr)), @@ -520,17 +521,18 @@ private static TradeLineItem _parseTradeLineItem(XmlNode tradeLineItem, XmlNames foreach (XmlNode appliedTradeAllowanceChargeNode in appliedTradeAllowanceChargeNodes) { bool chargeIndicator = _nodeAsBool(appliedTradeAllowanceChargeNode, "./ram:ChargeIndicator/udt:Indicator", nsmgr); - decimal basisAmount = _nodeAsDecimal(appliedTradeAllowanceChargeNode, "./ram:BasisAmount", nsmgr, 0).Value; + decimal basisAmount = _nodeAsDecimal(appliedTradeAllowanceChargeNode, "./ram:BasisAmount", nsmgr, null).Value; string basisAmountCurrency = _nodeAsString(appliedTradeAllowanceChargeNode, "./ram:BasisAmount/@currencyID", nsmgr); decimal actualAmount = _nodeAsDecimal(appliedTradeAllowanceChargeNode, "./ram:ActualAmount", nsmgr, 0).Value; string actualAmountCurrency = _nodeAsString(appliedTradeAllowanceChargeNode, "./ram:ActualAmount/@currencyID", nsmgr); string reason = _nodeAsString(appliedTradeAllowanceChargeNode, "./ram:Reason", nsmgr); + decimal chargePercentage = _nodeAsDecimal(appliedTradeAllowanceChargeNode, "./ram:CalculationPercent", nsmgr, null).Value; item.AddTradeAllowanceCharge(!chargeIndicator, // wichtig: das not (!) beachten - default(CurrencyCodes).FromString(basisAmountCurrency), - basisAmount, - actualAmount, - reason); + default(CurrencyCodes).FromString(basisAmountCurrency), + basisAmount, + actualAmount, + reason); } if (item.UnitCode == QuantityCodes.Unknown) From f62317cca93f9fcef33392648026c1103db8d7bc Mon Sep 17 00:00:00 2001 From: Stephan Date: Sun, 28 Apr 2024 20:08:23 +0200 Subject: [PATCH 3/4] another bug fix --- ZUGFeRD/InvoiceDescriptor.cs | 4 ++-- ZUGFeRD/InvoiceDescriptor21Reader.cs | 9 +++++---- ZUGFeRD/InvoiceDescriptor21Writer.cs | 21 +++++++++++++++++++-- ZUGFeRD/TradeLineItem.cs | 25 ++++++++++++++++++++++++- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/ZUGFeRD/InvoiceDescriptor.cs b/ZUGFeRD/InvoiceDescriptor.cs index 7573614a..531b16f0 100644 --- a/ZUGFeRD/InvoiceDescriptor.cs +++ b/ZUGFeRD/InvoiceDescriptor.cs @@ -728,12 +728,12 @@ public void AddTradeAllowanceCharge(bool isDiscount, decimal? basisAmount, Curre /// Base amount (basis of allowance) /// Curency of the allowance /// Actual allowance charge amount - /// Actual allowance charge amount + /// Actual allowance charge percentage /// Reason for the allowance /// VAT type code for document level allowance/ charge /// VAT type code for document level allowance/ charge /// VAT rate for the allowance - public void AddTradeAllowanceCharge(bool isDiscount, decimal? basisAmount, CurrencyCodes currency, decimal actualAmount, decimal chargePercentage, string reason, TaxTypes taxTypeCode, TaxCategoryCodes taxCategoryCode, decimal taxPercent) + public void AddTradeAllowanceCharge(bool isDiscount, decimal? basisAmount, CurrencyCodes currency, decimal actualAmount, decimal? chargePercentage, string reason, TaxTypes taxTypeCode, TaxCategoryCodes taxCategoryCode, decimal taxPercent) { this._TradeAllowanceCharges.Add(new TradeAllowanceCharge() { diff --git a/ZUGFeRD/InvoiceDescriptor21Reader.cs b/ZUGFeRD/InvoiceDescriptor21Reader.cs index a9c4f2c3..d747349a 100644 --- a/ZUGFeRD/InvoiceDescriptor21Reader.cs +++ b/ZUGFeRD/InvoiceDescriptor21Reader.cs @@ -301,10 +301,10 @@ public override InvoiceDescriptor Load(Stream stream) foreach (XmlNode node in doc.SelectNodes("//ram:SpecifiedTradeAllowanceCharge", nsmgr)) { retval.AddTradeAllowanceCharge(!_nodeAsBool(node, ".//ram:ChargeIndicator", nsmgr), // wichtig: das not (!) beachten - _nodeAsDecimal(node, ".//ram:BasisAmount", nsmgr, null).Value, + _nodeAsDecimal(node, ".//ram:BasisAmount", nsmgr), retval.Currency, _nodeAsDecimal(node, ".//ram:ActualAmount", nsmgr, 0).Value, - _nodeAsDecimal(node, ".//ram:CalculationPercent", nsmgr, null).Value, + _nodeAsDecimal(node, ".//ram:CalculationPercent", nsmgr), _nodeAsString(node, ".//ram:Reason", nsmgr), default(TaxTypes).FromString(_nodeAsString(node, ".//ram:CategoryTradeTax/ram:TypeCode", nsmgr)), default(TaxCategoryCodes).FromString(_nodeAsString(node, ".//ram:CategoryTradeTax/ram:CategoryCode", nsmgr)), @@ -521,17 +521,18 @@ private static TradeLineItem _parseTradeLineItem(XmlNode tradeLineItem, XmlNames foreach (XmlNode appliedTradeAllowanceChargeNode in appliedTradeAllowanceChargeNodes) { bool chargeIndicator = _nodeAsBool(appliedTradeAllowanceChargeNode, "./ram:ChargeIndicator/udt:Indicator", nsmgr); - decimal basisAmount = _nodeAsDecimal(appliedTradeAllowanceChargeNode, "./ram:BasisAmount", nsmgr, null).Value; + decimal? basisAmount = _nodeAsDecimal(appliedTradeAllowanceChargeNode, "./ram:BasisAmount", nsmgr, null); string basisAmountCurrency = _nodeAsString(appliedTradeAllowanceChargeNode, "./ram:BasisAmount/@currencyID", nsmgr); decimal actualAmount = _nodeAsDecimal(appliedTradeAllowanceChargeNode, "./ram:ActualAmount", nsmgr, 0).Value; string actualAmountCurrency = _nodeAsString(appliedTradeAllowanceChargeNode, "./ram:ActualAmount/@currencyID", nsmgr); string reason = _nodeAsString(appliedTradeAllowanceChargeNode, "./ram:Reason", nsmgr); - decimal chargePercentage = _nodeAsDecimal(appliedTradeAllowanceChargeNode, "./ram:CalculationPercent", nsmgr, null).Value; + decimal? chargePercentage = _nodeAsDecimal(appliedTradeAllowanceChargeNode, "./ram:CalculationPercent", nsmgr, null); item.AddTradeAllowanceCharge(!chargeIndicator, // wichtig: das not (!) beachten default(CurrencyCodes).FromString(basisAmountCurrency), basisAmount, actualAmount, + chargePercentage, reason); } diff --git a/ZUGFeRD/InvoiceDescriptor21Writer.cs b/ZUGFeRD/InvoiceDescriptor21Writer.cs index 83e0687d..736a347f 100644 --- a/ZUGFeRD/InvoiceDescriptor21Writer.cs +++ b/ZUGFeRD/InvoiceDescriptor21Writer.cs @@ -303,6 +303,15 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) Writer.WriteEndElement(); #endregion + #region ChargePercentage + if (tradeAllowanceCharge.ChargePercentage.HasValue) + { + Writer.WriteStartElement("ram:CalculationPercent", profile: Profile.Extended | Profile.XRechnung); + Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.ChargePercentage.Value, 2)); + Writer.WriteEndElement(); + } + #endregion + Writer.WriteOptionalElementString("ram:Reason", tradeAllowanceCharge.Reason, Profile.Extended); // not in XRechnung according to CII-SR-128 Writer.WriteEndElement(); // !AppliedTradeAllowanceCharge @@ -821,15 +830,23 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream) #endregion // 13. SpecifiedTradeAllowanceCharge (optional) - if ((this.Descriptor.GetTradeAllowanceCharges() != null) && (this.Descriptor.GetTradeAllowanceCharges().Count > 0)) + IList _allowanceCharges = this.Descriptor.GetTradeAllowanceCharges(); + if (_allowanceCharges?.Count > 0) { - foreach (TradeAllowanceCharge tradeAllowanceCharge in this.Descriptor.GetTradeAllowanceCharges()) + foreach (TradeAllowanceCharge tradeAllowanceCharge in _allowanceCharges) { Writer.WriteStartElement("ram:SpecifiedTradeAllowanceCharge"); Writer.WriteStartElement("ram:ChargeIndicator"); Writer.WriteElementString("udt:Indicator", tradeAllowanceCharge.ChargeIndicator ? "true" : "false"); Writer.WriteEndElement(); // !ram:ChargeIndicator + if (tradeAllowanceCharge.ChargePercentage.HasValue) + { + Writer.WriteStartElement("ram:CalculationPercent", profile: Profile.Extended | Profile.XRechnung1 | Profile.XRechnung); + Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.ChargePercentage.Value)); + Writer.WriteEndElement(); + } + if (tradeAllowanceCharge.BasisAmount.HasValue) { Writer.WriteStartElement("ram:BasisAmount", profile: Profile.Comfort | Profile.Extended | Profile.XRechnung1 | Profile.XRechnung); diff --git a/ZUGFeRD/TradeLineItem.cs b/ZUGFeRD/TradeLineItem.cs index 072dcaa9..72e8c1b2 100644 --- a/ZUGFeRD/TradeLineItem.cs +++ b/ZUGFeRD/TradeLineItem.cs @@ -175,7 +175,7 @@ public class TradeLineItem /// Basis aount for the allowance or surcharge, typicalls the net amount of the item /// The actual allowance or surcharge amount /// Reason for the allowance or surcharge - public void AddTradeAllowanceCharge(bool isDiscount, CurrencyCodes currency, decimal basisAmount, decimal actualAmount, string reason) + public void AddTradeAllowanceCharge(bool isDiscount, CurrencyCodes currency, decimal? basisAmount, decimal actualAmount, string reason) { this._TradeAllowanceCharges.Add(new TradeAllowanceCharge() { @@ -188,6 +188,29 @@ public void AddTradeAllowanceCharge(bool isDiscount, CurrencyCodes currency, dec } // !AddTradeAllowanceCharge() + /// + /// As an allowance or charge on item level, attaching it to the corresponding item. + /// + /// Marks if its an allowance (true) or charge (false). Please note that the xml will present inversed values + /// Currency of the allowance or surcharge + /// Basis aount for the allowance or surcharge, typicalls the net amount of the item + /// The actual allowance or surcharge amount + /// Actual allowance or surcharge charge percentage + /// Reason for the allowance or surcharge + public void AddTradeAllowanceCharge(bool isDiscount, CurrencyCodes currency, decimal? basisAmount, decimal actualAmount, decimal? chargePercentage, string reason) + { + this._TradeAllowanceCharges.Add(new TradeAllowanceCharge() + { + ChargeIndicator = !isDiscount, + Currency = currency, + ActualAmount = actualAmount, + BasisAmount = basisAmount, + ChargePercentage = chargePercentage, + Reason = reason + }); + } // !AddTradeAllowanceCharge() + + /// /// Returns all trade allowance charges for the trade line item /// From 5bfe626d2db97da0c565836e339b2ccd9485c7fd Mon Sep 17 00:00:00 2001 From: Stephan Date: Sun, 28 Apr 2024 20:08:56 +0200 Subject: [PATCH 4/4] Tests for trade allowance charge with CalculationPercent --- ZUGFeRD-Test/ZUGFeRD21Tests.cs | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/ZUGFeRD-Test/ZUGFeRD21Tests.cs b/ZUGFeRD-Test/ZUGFeRD21Tests.cs index 93d07100..339f7190 100644 --- a/ZUGFeRD-Test/ZUGFeRD21Tests.cs +++ b/ZUGFeRD-Test/ZUGFeRD21Tests.cs @@ -1781,5 +1781,48 @@ public void TestBasisQuantityMultiple() XmlNode node = doc.SelectSingleNode("//ram:SpecifiedTradeSettlementLineMonetarySummation//ram:LineTotalAmount", nsmgr); Assert.AreEqual("27.50", node.InnerText); } // !TestBasisQuantityMultiple() + + + [TestMethod] + public void TestTradeAllowanceChargeWithoutExplicitPercentage() + { + InvoiceDescriptor invoice = InvoiceProvider.CreateInvoice(); + + // fake values, does not matter for our test case + invoice.AddTradeAllowanceCharge(true, 100, CurrencyCodes.EUR, 10, "", TaxTypes.VAT, TaxCategoryCodes.S, 19); + + MemoryStream ms = new MemoryStream(); + invoice.Save(ms, ZUGFeRDVersion.Version21, Profile.Extended); + ms.Position = 0; + + InvoiceDescriptor loadedInvoice = InvoiceDescriptor.Load(ms); + IList allowanceCharges = loadedInvoice.GetTradeAllowanceCharges(); + + Assert.IsTrue(allowanceCharges.Count == 1); + Assert.AreEqual(allowanceCharges[0].BasisAmount, 100m); + Assert.AreEqual(allowanceCharges[0].Amount, 10m); + Assert.AreEqual(allowanceCharges[0].ChargePercentage, null); + } // !TestTradeAllowanceChargeWithoutExplicitPercentage() + + + [TestMethod] + public void TestTradeAllowanceChargeWithExplicitPercentage() + { + InvoiceDescriptor invoice = InvoiceProvider.CreateInvoice(); + + // fake values, does not matter for our test case + invoice.AddTradeAllowanceCharge(true, 100, CurrencyCodes.EUR, 10, 12, "", TaxTypes.VAT, TaxCategoryCodes.S, 19); + + MemoryStream ms = new MemoryStream(); + invoice.Save(ms, ZUGFeRDVersion.Version21, Profile.Extended); + ms.Position = 0; + InvoiceDescriptor loadedInvoice = InvoiceDescriptor.Load(ms); + IList allowanceCharges = loadedInvoice.GetTradeAllowanceCharges(); + + Assert.IsTrue(allowanceCharges.Count == 1); + Assert.AreEqual(allowanceCharges[0].BasisAmount, 100m); + Assert.AreEqual(allowanceCharges[0].Amount, 10m); + Assert.AreEqual(allowanceCharges[0].ChargePercentage, 12); + } // !TestTradeAllowanceChargeWithExplicitPercentage() } } \ No newline at end of file