Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions ZUGFeRD.Test/XRechnungUBLTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1143,7 +1143,7 @@ public void TestReferenceXRechnung21UBL()
Assert.AreEqual(desc.OrderNo, "0815-99-1");
Assert.AreEqual(desc.Currency, CurrencyCodes.EUR);

Assert.AreEqual(desc.Buyer.Name, "Rechnungs Roulette GmbH & Co KG");
Assert.AreEqual(desc.Buyer.Name, "");
Assert.AreEqual(desc.Buyer.City, "Klein Schlappstadt a.d. Lusche");
Assert.AreEqual(desc.Buyer.Postcode, "12345");
Assert.AreEqual(desc.Buyer.Country, CountryCodes.DE);
Expand All @@ -1154,7 +1154,7 @@ public void TestReferenceXRechnung21UBL()
Assert.AreEqual(desc.BuyerContact.EmailAddress, "manfred.mustermann@rr.de");
Assert.AreEqual(desc.BuyerContact.PhoneNo, "012345 98 765 - 44");

Assert.AreEqual(desc.Seller.Name, "Harry Hirsch Holz- und Trockenbau");
Assert.AreEqual(desc.Seller.Name, "");
Assert.AreEqual(desc.Seller.City, "Klein Schlappstadt a.d. Lusche");
Assert.AreEqual(desc.Seller.Postcode, "12345");
Assert.AreEqual(desc.Seller.Country, CountryCodes.DE);
Expand Down Expand Up @@ -1355,6 +1355,21 @@ public void TestNonStandardDateTimeFormat()
} // !TestNonStandardDateTimeFormat()


/// <summary>
/// Test that InvoicePeriod is not read TradeLineItem -> InvoicePeriod
/// </summary>
[TestMethod]
public void TestDontMixInvoicePeriodWithTradeLineItem()
{
string path = @"..\..\..\..\demodata\xRechnung\01.01a-INVOICE_ubl.xml";
path = _makeSurePathIsCrossPlatformCompatible(path);

InvoiceDescriptor desc = InvoiceDescriptor.Load(path);

Assert.IsNull(desc.BillingPeriodStart);
Assert.IsNull(desc.BillingPeriodEnd);
} // !TestDontMixInvoicePeriodWithTradeLineItem()

[TestMethod]
public void TestSellerPartyDescription()
{
Expand Down
136 changes: 136 additions & 0 deletions ZUGFeRD.Test/ZUGFeRD22Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,142 @@ public void TestExtendedInvoiceWithIncludedItems()
} // !TestExtendedInvoiceWithIncludedItems()


/// <summary>
/// Roundtrip test for TradeLineItem product identification fields added in PR #863:
/// IndustryAssignedID, ModelID, BatchID, BrandName, ModelName
/// These fields are Extended profile only (CII 2.3).
/// </summary>
[TestMethod]
public void TestTradeLineItemProductFieldsRoundtrip()
{
string path = @"..\..\..\..\demodata\zugferd21\zugferd_2p1_EXTENDED_Warenrechnung-factur-x.xml";
path = _makeSurePathIsCrossPlatformCompatible(path);

Stream s = File.Open(path, FileMode.Open);
InvoiceDescriptor desc = InvoiceDescriptor.Load(s);
s.Close();

desc.TradeLineItems.Clear();

var lineItem = desc.AddTradeLineItem(
lineID: "1",
name: "Test Product",
billedQuantity: 10m,
unitCode: QuantityCodes.H87,
netUnitPrice: 5.0m,
grossUnitPrice: 5.0m,
categoryCode: TaxCategoryCodes.S,
taxPercent: 19.0m,
taxType: TaxTypes.VAT);

lineItem.SellerAssignedID = "SELLER-001";
lineItem.BuyerAssignedID = "BUYER-001";
lineItem.IndustryAssignedID = "IND-12345";
lineItem.ModelID = "MDL-67890";
lineItem.BatchID = "BATCH-2025-001";
lineItem.BrandName = "TestBrand";
lineItem.ModelName = "TestModel Pro";

MemoryStream ms = new MemoryStream();
desc.Save(ms, ZUGFeRDVersion.Version23, Profile.Extended);
ms.Seek(0, SeekOrigin.Begin);

InvoiceDescriptor loadedInvoice = InvoiceDescriptor.Load(ms);

Assert.AreEqual(1, loadedInvoice.TradeLineItems.Count);
var loadedLineItem = loadedInvoice.TradeLineItems[0];
Assert.AreEqual("Test Product", loadedLineItem.Name);
Assert.AreEqual("SELLER-001", loadedLineItem.SellerAssignedID);
Assert.AreEqual("BUYER-001", loadedLineItem.BuyerAssignedID);
Assert.AreEqual("IND-12345", loadedLineItem.IndustryAssignedID);
Assert.AreEqual("MDL-67890", loadedLineItem.ModelID);
Assert.AreEqual("BATCH-2025-001", loadedLineItem.BatchID);
Assert.AreEqual("TestBrand", loadedLineItem.BrandName);
Assert.AreEqual("TestModel Pro", loadedLineItem.ModelName);
} // !TestTradeLineItemProductFieldsRoundtrip()


/// <summary>
/// Roundtrip test for IncludedReferencedProduct fields added in PR #863:
/// GlobalID, SellerAssignedID, BuyerAssignedID, IndustryAssignedID, Description
/// </summary>
[TestMethod]
public void TestIncludedReferencedProductFieldsRoundtrip()
{
string path = @"..\..\..\..\demodata\zugferd21\zugferd_2p1_EXTENDED_Warenrechnung-factur-x.xml";
path = _makeSurePathIsCrossPlatformCompatible(path);

Stream s = File.Open(path, FileMode.Open);
InvoiceDescriptor desc = InvoiceDescriptor.Load(s);
s.Close();

desc.TradeLineItems.Clear();

var lineItem = desc.AddTradeLineItem(
lineID: "1",
name: "Main Product",
billedQuantity: 5m,
unitCode: QuantityCodes.H87,
netUnitPrice: 20.0m,
grossUnitPrice: 20.0m,
categoryCode: TaxCategoryCodes.S,
taxPercent: 19.0m,
taxType: TaxTypes.VAT);

// Add included product with all new fields set
lineItem.IncludedReferencedProducts.Add(new IncludedReferencedProduct()
{
GlobalID = new GlobalID(GlobalIDSchemeIdentifiers.EAN, "4012345999999"),
SellerAssignedID = "SEL-REF-001",
BuyerAssignedID = "BUY-REF-001",
IndustryAssignedID = "IND-REF-001",
Name = "Included Part A",
Description = "Description of included part A",
UnitQuantity = 2,
UnitCode = QuantityCodes.C62
});

// Add a second included product with minimal fields
lineItem.IncludedReferencedProducts.Add(new IncludedReferencedProduct()
{
Name = "Included Part B",
Description = "Description of included part B"
});

MemoryStream ms = new MemoryStream();
desc.Save(ms, ZUGFeRDVersion.Version23, Profile.Extended);
ms.Seek(0, SeekOrigin.Begin);

InvoiceDescriptor loadedInvoice = InvoiceDescriptor.Load(ms);

Assert.AreEqual(1, loadedInvoice.TradeLineItems.Count);
var loadedLineItem = loadedInvoice.TradeLineItems[0];
Assert.AreEqual(2, loadedLineItem.IncludedReferencedProducts.Count);

// Verify first included product (all fields)
var product1 = loadedLineItem.IncludedReferencedProducts[0];
Assert.AreEqual(GlobalIDSchemeIdentifiers.EAN, product1.GlobalID.SchemeID);
Assert.AreEqual("4012345999999", product1.GlobalID.ID);
Assert.AreEqual("SEL-REF-001", product1.SellerAssignedID);
Assert.AreEqual("BUY-REF-001", product1.BuyerAssignedID);
Assert.AreEqual("IND-REF-001", product1.IndustryAssignedID);
Assert.AreEqual("Included Part A", product1.Name);
Assert.AreEqual("Description of included part A", product1.Description);
Assert.AreEqual(2m, product1.UnitQuantity);
Assert.AreEqual(QuantityCodes.C62, product1.UnitCode);

// Verify second included product (minimal fields)
var product2 = loadedLineItem.IncludedReferencedProducts[1];
Assert.AreEqual("Included Part B", product2.Name);
Assert.AreEqual("Description of included part B", product2.Description);
Assert.AreEqual("", product2.SellerAssignedID);
Assert.AreEqual("", product2.BuyerAssignedID);
Assert.AreEqual("", product2.IndustryAssignedID);
Assert.AreEqual(false, product2.UnitQuantity.HasValue);
Assert.IsNull(product2.UnitCode);
} // !TestIncludedReferencedProductFieldsRoundtrip()


[TestMethod]
public void TestReferenceEReportingFacturXInvoice()
{
Expand Down
10 changes: 6 additions & 4 deletions ZUGFeRD/InvoiceDescriptor1Reader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ public override InvoiceDescriptor Load(Stream stream)
};
retval.PaymentMeans = paymentMeans;

retval.BillingPeriodStart = XmlUtils.NodeAsDateTime(doc.DocumentElement, "//ram:ApplicableHeaderTradeSettlement/ram:BillingSpecifiedPeriod/ram:StartDateTime", nsmgr);
retval.BillingPeriodEnd = XmlUtils.NodeAsDateTime(doc.DocumentElement, "//ram:ApplicableHeaderTradeSettlement/ram:BillingSpecifiedPeriod/ram:EndDateTime", nsmgr);
retval.BillingPeriodStart = XmlUtils.NodeAsDateTime(doc.DocumentElement, "//ram:ApplicableSupplyChainTradeSettlement/ram:BillingSpecifiedPeriod/ram:StartDateTime", nsmgr);
retval.BillingPeriodEnd = XmlUtils.NodeAsDateTime(doc.DocumentElement, "//ram:ApplicableSupplyChainTradeSettlement/ram:BillingSpecifiedPeriod/ram:EndDateTime", nsmgr);

XmlNodeList creditorFinancialAccountNodes = doc.SelectNodes("//ram:ApplicableSupplyChainTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans/ram:PayeePartyCreditorFinancialAccount", nsmgr);
XmlNodeList creditorFinancialInstitutions = doc.SelectNodes("//ram:ApplicableSupplyChainTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans/ram:PayeeSpecifiedCreditorFinancialInstitution", nsmgr);
Expand Down Expand Up @@ -261,7 +261,7 @@ public override InvoiceDescriptor Load(Stream stream)
decimal? penaltyPercent = XmlUtils.NodeAsDecimal(node, ".//ram:ApplicableTradePaymentPenaltyTerms/ram:CalculationPercent", nsmgr, null);
int? penaltyDueDays = null; // XmlUtils.NodeAsInt(node, ".//ram:ApplicableTradePaymentPenaltyTerms/ram:BasisPeriodMeasure", nsmgr);
decimal? penaltyBaseAmount = XmlUtils.NodeAsDecimal(node, ".//ram:ApplicableTradePaymentPenaltyTerms/ram:BasisAmount", nsmgr, null);
decimal? penaltyActualAmount = XmlUtils.NodeAsDecimal(node, ".//ram:ApplicableTradePaymentDiscountTerms/ram:ActualPenaltyAmount", nsmgr, null);
decimal? penaltyActualAmount = XmlUtils.NodeAsDecimal(node, ".//ram:ApplicableTradePaymentPenaltyTerms/ram:ActualPenaltyAmount", nsmgr, null);
PaymentTermsType? paymentTermsType = discountPercent.HasValue ? PaymentTermsType.Skonto :
penaltyPercent.HasValue ? PaymentTermsType.Verzug :
(PaymentTermsType?)null;
Expand Down Expand Up @@ -478,12 +478,14 @@ private static Party _nodeAsParty(XmlNode baseNode, string xpath, XmlNamespaceMa

if (!String.IsNullOrWhiteSpace(lineTwo))
{
retval.Street2 = lineOne;
retval.ContactName = lineOne;
retval.Street = lineOne;
retval.Street = lineTwo;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to leave the mapping as it was for consistency purpose.

Street = lineTwo
Street2 = lineOne

}
else
{
retval.Street = lineOne;
retval.Street2 = null;
retval.ContactName = null;
}

Expand Down
6 changes: 4 additions & 2 deletions ZUGFeRD/InvoiceDescriptor1Writer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -952,8 +952,10 @@
_writeOptionalContact(writer, "ram", "DefinedTradeContact", Contact);
writer.WriteStartElement("ram", "PostalTradeAddress");
writer.WriteOptionalElementString("ram", "PostcodeCode", Party.Postcode);
writer.WriteOptionalElementString("ram", "LineOne", string.IsNullOrWhiteSpace(Party.ContactName) ? Party.Street : Party.ContactName);
if (!string.IsNullOrWhiteSpace(Party.ContactName)) { writer.WriteOptionalElementString("ram", "LineTwo", Party.Street); }
string lineOneValue = !string.IsNullOrWhiteSpace(Party.Street2) ? Party.Street2 : (!string.IsNullOrWhiteSpace(Party.ContactName) ? Party.ContactName : Party.Street);

Check warning on line 955 in ZUGFeRD/InvoiceDescriptor1Writer.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=stephanstapel_ZUGFeRD-csharp&issues=AZzYyi8Do20_9UCACfsD&open=AZzYyi8Do20_9UCACfsD&pullRequest=889
string lineTwoValue = (!string.IsNullOrWhiteSpace(Party.Street2) || !string.IsNullOrWhiteSpace(Party.ContactName)) ? Party.Street : null;
writer.WriteOptionalElementString("ram", "LineOne", lineOneValue);
writer.WriteOptionalElementString("ram", "LineTwo", lineTwoValue);
writer.WriteOptionalElementString("ram", "CityName", Party.City);

if (Party.Country.HasValue)
Expand Down
5 changes: 3 additions & 2 deletions ZUGFeRD/InvoiceDescriptor20Reader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ public override InvoiceDescriptor Load(Stream stream)
decimal? penaltyPercent = XmlUtils.NodeAsDecimal(node, ".//ram:ApplicableTradePaymentPenaltyTerms/ram:CalculationPercent", nsmgr, null);
int? penaltyDueDays = null; // XmlUtils.NodeAsInt(node, ".//ram:ApplicableTradePaymentPenaltyTerms/ram:BasisPeriodMeasure", nsmgr);
decimal? penaltyBaseAmount = XmlUtils.NodeAsDecimal(node, ".//ram:ApplicableTradePaymentPenaltyTerms/ram:BasisAmount", nsmgr, null);
decimal? penaltyActualAmount = XmlUtils.NodeAsDecimal(node, ".//ram:ApplicableTradePaymentDiscountTerms/ram:ActualPenaltyAmount", nsmgr, null);
decimal? penaltyActualAmount = XmlUtils.NodeAsDecimal(node, ".//ram:ApplicableTradePaymentPenaltyTerms/ram:ActualPenaltyAmount", nsmgr, null);
PaymentTermsType? paymentTermsType = discountPercent.HasValue ? PaymentTermsType.Skonto :
penaltyPercent.HasValue ? PaymentTermsType.Verzug :
(PaymentTermsType?)null;
Expand Down Expand Up @@ -644,13 +644,14 @@ private static Party _nodeAsParty(XmlNode baseNode, string xpath, XmlNamespaceMa

if (!String.IsNullOrWhiteSpace(lineTwo))
{
retval.Street2 = lineOne;
retval.ContactName = lineOne;
retval.Street = lineTwo;
}
else
{
retval.Street = lineOne;
retval.ContactName = null;
retval.Street2 = null;
}

retval.AddressLine3 = XmlUtils.NodeAsString(node, "ram:PostalTradeAddress/ram:LineThree", nsmgr);
Expand Down
6 changes: 4 additions & 2 deletions ZUGFeRD/InvoiceDescriptor20Writer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1247,8 +1247,10 @@

writer.WriteStartElement("ram", "PostalTradeAddress");
writer.WriteOptionalElementString("ram", "PostcodeCode", party.Postcode);
writer.WriteOptionalElementString("ram", "LineOne", string.IsNullOrWhiteSpace(party.ContactName) ? party.Street : party.ContactName);
if (!string.IsNullOrWhiteSpace(party.ContactName)) { writer.WriteOptionalElementString("ram", "LineTwo", party.Street); }
string lineOneValue = !string.IsNullOrWhiteSpace(party.Street2) ? party.Street2 : (!string.IsNullOrWhiteSpace(party.ContactName) ? party.ContactName : party.Street);

Check warning on line 1250 in ZUGFeRD/InvoiceDescriptor20Writer.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=stephanstapel_ZUGFeRD-csharp&issues=AZzYyi6Go20_9UCACfsC&open=AZzYyi6Go20_9UCACfsC&pullRequest=889
string lineTwoValue = (!string.IsNullOrWhiteSpace(party.Street2) || !string.IsNullOrWhiteSpace(party.ContactName)) ? party.Street : null;
writer.WriteOptionalElementString("ram", "LineOne", lineOneValue);
writer.WriteOptionalElementString("ram", "LineTwo", lineTwoValue);

writer.WriteOptionalElementString("ram", "LineThree", party.AddressLine3); // BT-163

Expand Down
12 changes: 9 additions & 3 deletions ZUGFeRD/InvoiceDescriptor22UBLWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public override void Save(InvoiceDescriptor descriptor, Stream stream, ZUGFeRDFo

_Writer.WriteOptionalElementString("cbc", "BuyerReference", this._Descriptor.ReferenceOrderNo);

if (this._Descriptor.BillingPeriodEnd.HasValue || this._Descriptor.BillingPeriodEnd.HasValue)
if (this._Descriptor.BillingPeriodStart.HasValue || this._Descriptor.BillingPeriodEnd.HasValue)
{
_Writer.WriteStartElement("cac", "InvoicePeriod");

Expand Down Expand Up @@ -960,7 +960,7 @@ private void _writeOptionalParty(ProfileAwareXmlTextWriter writer, PartyTypes pa
if (partyType != PartyTypes.SellerTaxRepresentativeTradeParty)
writer.WriteStartElement("cac", "Party", this._Descriptor.Profile);

if (ElectronicAddress != null)
if (!String.IsNullOrWhiteSpace(ElectronicAddress?.Address))
{
writer.WriteStartElement("cbc", "EndpointID");
writer.WriteAttributeString("schemeID", ElectronicAddress.ElectronicAddressSchemeID.EnumToString());
Expand Down Expand Up @@ -1037,7 +1037,13 @@ private void _writeOptionalParty(ProfileAwareXmlTextWriter writer, PartyTypes pa

writer.WriteStartElement("cac", "PostalAddress");
_Writer.WriteOptionalElementString("cbc", "StreetName", party.Street);
_Writer.WriteOptionalElementString("cbc", "AdditionalStreetName", party.AddressLine3);
_Writer.WriteOptionalElementString("cbc", "AdditionalStreetName", party.Street2);
if (!string.IsNullOrWhiteSpace(party.AddressLine3))
{
writer.WriteStartElement("cac", "AddressLine");
_Writer.WriteOptionalElementString("cbc", "Line", party.AddressLine3);
writer.WriteEndElement(); //!AddressLine
}
_Writer.WriteElementString("cbc", "CityName", party.City);
_Writer.WriteElementString("cbc", "PostalZone", party.Postcode);
_Writer.WriteOptionalElementString("cbc", "CountrySubentity", party.CountrySubdivisionName);
Expand Down
Loading
Loading