Skip to content

Conversation

@stephanstapel
Copy link
Owner

Summary

Some fixes to UBL reading and writing

Checklist

  • Unit tests added/updated

Breaking changes?

  • No
  • Yes (explain impact & migration)

Copilot AI review requested due to automatic review settings December 16, 2025 22:57
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1063 to +1067
if ((party.SpecifiedLegalOrganization != null) || !String.IsNullOrWhiteSpace(party.Description))
{
//Party legal registration identifier (BT-30)
_Writer.WriteElementString("cbc", "CompanyID", party.GlobalID.ID);
}
writer.WriteStartElement("cac", "PartyLegalEntity");

if (party.Description != null)
{
//Party additional legal information (BT-33)
_Writer.WriteElementString("cbc", "CompanyLegalForm", party.Description);
}
if (party.SpecifiedLegalOrganization != null)

Choose a reason for hiding this comment

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

P1 Badge Do not drop PartyLegalEntity when no legal org is set

The UBL writer now wraps the entire PartyLegalEntity block in a check for SpecifiedLegalOrganization or Description only. Invoices that only populate the party name (and possibly a GlobalID, which was previously written here) now emit no cac:PartyLegalEntity/cbc:RegistrationName at all, even though EN16931 requires the registration name for accounting parties. Such invoices will produce non-compliant UBL output and omit seller/buyer identifiers whenever the descriptor doesn’t also set SpecifiedLegalOrganization or Description.

Useful? React with 👍 / 👎.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request implements several fixes for UBL (Universal Business Language) reading and writing functionality, focusing on improving null safety, implementing business rules, and enhancing party identification handling.

  • Fixed null reference handling in SpecifiedLineTradeSettlement processing
  • Implemented business rule BR-O-05 for tax category code 'O'
  • Enhanced party identification with scheme ID support for buyer/seller organizations

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 12 comments.

File Description
ZUGFeRD/InvoiceDescriptor23CIIReader.cs Added null check for SpecifiedLineTradeSettlement to prevent processing when the node doesn't exist; improved code formatting
ZUGFeRD/InvoiceDescriptor22UBLWriter.cs Implemented BR-O-05 rule for tax category 'O', added scheme ID attributes for party identifications, refactored PartyLegalEntity writing to use SpecifiedLegalOrganization
ZUGFeRD.Test/ZUGFeRD22Tests.cs Updated test data file path to use newer EXTENDED example
ZUGFeRD.Test/XRechnungUBLTests.cs Added comprehensive test for buyer/seller scheme IDs in UBL format, verifying round-trip serialization/deserialization

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1346 to 1366

desc.Buyer.SpecifiedLegalOrganization = new LegalOrganization(
GlobalIDSchemeIdentifiers.DUNS,
"123456789",
"Buyer Company Ltd.");
desc.Seller.SpecifiedLegalOrganization = new LegalOrganization(
GlobalIDSchemeIdentifiers.AbnScheme,
"987654321",
"Seller Company Ltd.");


MemoryStream ms = new MemoryStream();
desc.Save(ms, ZUGFeRDVersion.Version23, Profile.XRechnung, ZUGFeRDFormats.UBL);
ms.Seek(0, SeekOrigin.Begin);
InvoiceDescriptor loadedInvoice = InvoiceDescriptor.Load(ms);

Assert.AreEqual(loadedInvoice.Buyer.ID.SchemeID, GlobalIDSchemeIdentifiers.DUNS);
Assert.AreEqual(loadedInvoice.Buyer.ID.ID, "123456789");
Assert.AreEqual(loadedInvoice.Seller.ID.SchemeID, GlobalIDSchemeIdentifiers.AbnScheme);
Assert.AreEqual(loadedInvoice.Seller.ID.ID, "987654321");

Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

This line contains trailing whitespace. Consider removing it to maintain code cleanliness.

Suggested change
desc.Buyer.SpecifiedLegalOrganization = new LegalOrganization(
GlobalIDSchemeIdentifiers.DUNS,
"123456789",
"Buyer Company Ltd.");
desc.Seller.SpecifiedLegalOrganization = new LegalOrganization(
GlobalIDSchemeIdentifiers.AbnScheme,
"987654321",
"Seller Company Ltd.");
MemoryStream ms = new MemoryStream();
desc.Save(ms, ZUGFeRDVersion.Version23, Profile.XRechnung, ZUGFeRDFormats.UBL);
ms.Seek(0, SeekOrigin.Begin);
InvoiceDescriptor loadedInvoice = InvoiceDescriptor.Load(ms);
Assert.AreEqual(loadedInvoice.Buyer.ID.SchemeID, GlobalIDSchemeIdentifiers.DUNS);
Assert.AreEqual(loadedInvoice.Buyer.ID.ID, "123456789");
Assert.AreEqual(loadedInvoice.Seller.ID.SchemeID, GlobalIDSchemeIdentifiers.AbnScheme);
Assert.AreEqual(loadedInvoice.Seller.ID.ID, "987654321");
desc.Buyer.SpecifiedLegalOrganization = new LegalOrganization(
GlobalIDSchemeIdentifiers.DUNS,
"123456789",
"Buyer Company Ltd.");
desc.Seller.SpecifiedLegalOrganization = new LegalOrganization(
GlobalIDSchemeIdentifiers.AbnScheme,
"987654321",
"Seller Company Ltd.");
MemoryStream ms = new MemoryStream();
desc.Save(ms, ZUGFeRDVersion.Version23, Profile.XRechnung, ZUGFeRDFormats.UBL);
ms.Seek(0, SeekOrigin.Begin);
InvoiceDescriptor loadedInvoice = InvoiceDescriptor.Load(ms);
Assert.AreEqual(loadedInvoice.Buyer.ID.SchemeID, GlobalIDSchemeIdentifiers.DUNS);
Assert.AreEqual(loadedInvoice.Buyer.ID.ID, "123456789");
Assert.AreEqual(loadedInvoice.Seller.ID.SchemeID, GlobalIDSchemeIdentifiers.AbnScheme);
Assert.AreEqual(loadedInvoice.Seller.ID.ID, "987654321");

Copilot uses AI. Check for mistakes.
Comment on lines +1072 to +1079
_Writer.WriteStartElement("cbc", "CompanyID");

writer.WriteEndElement(); //!PartyLegalEntity
if (party.SpecifiedLegalOrganization.ID.SchemeID.HasValue)
{
_Writer.WriteAttributeString("schemeID", party.SpecifiedLegalOrganization.ID.SchemeID.Value.EnumToString());
}
_Writer.WriteValue(party.SpecifiedLegalOrganization.ID.ID);
_Writer.WriteEndElement(); // !CompanyID
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

Potential null reference exception when accessing party.SpecifiedLegalOrganization.ID. The code checks if party.SpecifiedLegalOrganization is not null, but doesn't verify that party.SpecifiedLegalOrganization.ID is not null before accessing ID.SchemeID and ID.ID. According to the LegalOrganization class, the ID property can be null (initialized to null in the default constructor). Consider adding a null check for party.SpecifiedLegalOrganization.ID before accessing its properties.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +670 to +673
foreach (XmlNode invoiceReferencedDocumentNode in tradeLineItem.SelectNodes(".//ram:SpecifiedLineTradeSettlement/ram:InvoiceReferencedDocument", nsmgr))
{
// TODO: process
}
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

This assignment to invoiceReferencedDocumentNode is useless, since its value is never read.

Suggested change
foreach (XmlNode invoiceReferencedDocumentNode in tradeLineItem.SelectNodes(".//ram:SpecifiedLineTradeSettlement/ram:InvoiceReferencedDocument", nsmgr))
{
// TODO: process
}
// TODO: process InvoiceReferencedDocument nodes when implementation is defined

Copilot uses AI. Check for mistakes.
Comment on lines +631 to +632
XmlNode applicableTradeTaxNode = tradeLineItem.SelectSingleNode(".//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax", nsmgr);
// TODO: process
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

This assignment to applicableTradeTaxNode is useless, since its value is never read.

Suggested change
XmlNode applicableTradeTaxNode = tradeLineItem.SelectSingleNode(".//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax", nsmgr);
// TODO: process
// TODO: process ApplicableTradeTax when requirements are defined

Copilot uses AI. Check for mistakes.
EnumExtensions.StringToEnum<ChargeReasonCodes>(reasonCode));
}
else // allowance
XmlNode billingSpecifiedPeriodNode = tradeLineItem.SelectSingleNode(".//ram:SpecifiedLineTradeSettlement/ram:BillingSpecifiedPeriod", nsmgr);
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

This assignment to billingSpecifiedPeriodNode is useless, since its value is never read.

Copilot uses AI. Check for mistakes.
Comment on lines +667 to 668
XmlNode specifiedTradeSettlementLineMonetarySummationNode = tradeLineItem.SelectSingleNode(".//ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation", nsmgr);
// TODO: process
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

This assignment to specifiedTradeSettlementLineMonetarySummationNode is useless, since its value is never read.

Suggested change
XmlNode specifiedTradeSettlementLineMonetarySummationNode = tradeLineItem.SelectSingleNode(".//ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation", nsmgr);
// TODO: process
// TODO: process ram:SpecifiedTradeSettlementLineMonetarySummation if needed

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI commented Dec 16, 2025

@stephanstapel I've opened a new pull request, #853, to work on those changes. Once the pull request is ready, I'll request review from you.

@stephanstapel stephanstapel merged commit cf5f0ab into master Dec 16, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants