-
-
Notifications
You must be signed in to change notification settings - Fork 181
UBL fixes #852
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UBL fixes #852
Conversation
There was a problem hiding this 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".
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.
There was a problem hiding this 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
SpecifiedLineTradeSettlementprocessing - 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.
ZUGFeRD.Test/XRechnungUBLTests.cs
Outdated
|
|
||
| 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
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| 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"); |
| _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 |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| foreach (XmlNode invoiceReferencedDocumentNode in tradeLineItem.SelectNodes(".//ram:SpecifiedLineTradeSettlement/ram:InvoiceReferencedDocument", nsmgr)) | ||
| { | ||
| // TODO: process | ||
| } |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| foreach (XmlNode invoiceReferencedDocumentNode in tradeLineItem.SelectNodes(".//ram:SpecifiedLineTradeSettlement/ram:InvoiceReferencedDocument", nsmgr)) | |
| { | |
| // TODO: process | |
| } | |
| // TODO: process InvoiceReferencedDocument nodes when implementation is defined |
| XmlNode applicableTradeTaxNode = tradeLineItem.SelectSingleNode(".//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax", nsmgr); | ||
| // TODO: process |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| XmlNode applicableTradeTaxNode = tradeLineItem.SelectSingleNode(".//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax", nsmgr); | |
| // TODO: process | |
| // TODO: process ApplicableTradeTax when requirements are defined |
| EnumExtensions.StringToEnum<ChargeReasonCodes>(reasonCode)); | ||
| } | ||
| else // allowance | ||
| XmlNode billingSpecifiedPeriodNode = tradeLineItem.SelectSingleNode(".//ram:SpecifiedLineTradeSettlement/ram:BillingSpecifiedPeriod", nsmgr); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| XmlNode specifiedTradeSettlementLineMonetarySummationNode = tradeLineItem.SelectSingleNode(".//ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation", nsmgr); | ||
| // TODO: process |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| XmlNode specifiedTradeSettlementLineMonetarySummationNode = tradeLineItem.SelectSingleNode(".//ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation", nsmgr); | |
| // TODO: process | |
| // TODO: process ram:SpecifiedTradeSettlementLineMonetarySummation if needed |
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
|
@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. |
Summary
Some fixes to UBL reading and writing
Checklist
Breaking changes?