Skip to content

Ensure line item ExemptionReason and ExemptionReasonCode are not written for EN16931 profile#1122

Merged
langfr merged 6 commits into
ZUGFeRD:masterfrom
rjsmith:bugfix/996-line-item-exemption-reason
Jun 20, 2026
Merged

Ensure line item ExemptionReason and ExemptionReasonCode are not written for EN16931 profile#1122
langfr merged 6 commits into
ZUGFeRD:masterfrom
rjsmith:bugfix/996-line-item-exemption-reason

Conversation

@rjsmith

@rjsmith rjsmith commented May 19, 2026

Copy link
Copy Markdown
Contributor

This pull request is a second PR attempt to fix #996 that causes Mustang to generate invalid EN16931 - profile documents, and report warnings in the Valitool validator.

If a Mustang Product object has a defined ExemptionReason and / or ExemptionReasonCode, the ZUGFeRD2PullProvider generateXML() method currently adds ram:ExemptionReason and /or ram:ExemptionReasonCode to SpecifiedLineTradeSettlement ApplicableTradeTax, for any Profile.

However, these line item Exemption fields are only defined for Extended profile. This discrepancy is causing our test ZUGFeRD EN16931 - profile invoices generated by Mustang to fail third-party validation for non-standard tax cases that have an exemption reason that should appear in the document-level VAT Breakdown section.

Note that the referred bug ticket was previously closed by @jstaerk without explanation, and without an apparent fix, so I am contributing a fix here for review.

PR Code Changes

This PR only makes a change for EN16931 - profile invoices, to limit its impact on existing Mustang unit tests as much as possible. If the code is changed to only include line item Exemption nodes for EXTENDED profile, other existing Mustang unit tests , including for XRechnung, also break. For my purposes, my client's project only uses EN16931 - profile docs, so I do not want to make any changes that will affect other profiles in the PR.

I had to add a new enrichProductFromVATBreakdown method to the Item class used by the ZUGFeRDInvoiceImporter, to enrich per-line item Product objects with the ExemptionReason and ExemptionReasonCode, now that they may now only be present in the doc - level VAT Breakdown section (for EN16931 docs at least).

I also added an unrelated but apparently missing CategoryCode code line to the Item(NodeList itemChilds, boolean recalcPrice) constructor otherwise the ZF2PushTest.testAttachmentsExport() test fails.

Closes #996

@KingSora

Copy link
Copy Markdown

@jstaerk also having the same issue.. is this a viable fix for the issue described in the linked issue?

@langfr

langfr commented May 30, 2026

Copy link
Copy Markdown
Collaborator

testout-ZF2PushExemption.xml
testout-ZF2PushExemption.pdf

Tried to check this locally.
I added a new test to ZF2PushTest based on the new test in the PR. This creates a new ZUGFeRD-pdf with EN16931 profile.
New check in validator's LibraryTest to validate this invoice.
Validation classifies the invoice as a valid document.

<validation filename="testout-ZF2PushExemption.pdf" datetime="2026-05-30 13:03:24">
  <pdf>ValidationResult [flavour=3u, totalAssertions=11018, assertions=[], isCompliant=true]
    <info>
      <signature>Mustang</signature>
      <duration unit="ms">70</duration>
    </info>
    <summary status="valid"/>
  </pdf>  
  <xml>
    <info>
      <version>2</version>
      <profile>urn:cen.eu:en16931:2017</profile>
      <validator version="null"/>
      <rules>
        <fired>90</fired>
        <failed>0</failed>
      </rules>
      <duration unit="ms">41</duration>
    </info>
    <summary status="valid"/>
  </xml>
  <summary status="valid"/>
</validation>

Uploaded it to different web sites then:

  1. https://www.portinvoice.com/
  2. https://www.epoconsulting.com/erechnung-sap/xrechnung-validator
  3. https://ecosio.com/de/peppol-e-rechnung-xml-dokumente-validieren/
  4. https://e-rechnungs-checker.de/

Number 1 uses mustangproject as first validator and then their own "valitool".
valitool raises two warnings that BT-X-96 and BT-X-97 should not be used for format urn:cen.eu:en16931:2017, xpath
/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:ExemptionReason and /rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:ExemptionReasonCode

Number 2 is using the KoSIT Validator 1.5.0. Classifies the invoice as legal and does not even race any warnings.

Number 3 raises the two errors, "Element 'ram:ExemptionReason' is marked as not used in the given context." and "Element 'ram:ExemptionReasonCode' is marked as not used in the given context." from using schematron/2.4/FACTUR-X_EN16931.xslt.

Number 4 does not find any warnings or errors within the xml. (it raises a warning the values in xml and pdf do not match, that's expected in this test case).

So in summary, the two fields ExemptionReason and ExemptionReasonCode must not be written on line level when using the EN16931 profile, only for EXTENDED.
And it has to be clarified, why our validator does not raise any message and does not reject this file.

@langfr langfr self-requested a review May 30, 2026 13:48
@langfr

langfr commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Diving deeper into the validation.
It seems that I have to correct my previous conclusion "fields ExemptionReason and ExemptionReasonCode must not be written" to "should not be written".

When I omit a mandatory field, e.g. the invoice number, the apply of the validation schema creates this:

	<svrl:failed-assert id="FX-SCH-A-000011" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]" test="(rsm:ExchangedDocument/ram:ID !='')">
		<svrl:text>
	[BR-02]-An Invoice shall have an Invoice number (BT-1).</svrl:text>
	</svrl:failed-assert>

For ExemptionReason and ExemptionReasonCode we get this:

	<svrl:successful-report location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:IncludedSupplyChainTradeLineItem[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:SpecifiedLineTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:ApplicableTradeTax[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:ExemptionReason[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" test="true()">
		<svrl:text>
	Element 'ram:ExemptionReason' is marked as not used in the given context.</svrl:text>
	</svrl:successful-report>
	<svrl:fired-rule context="/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode"/>
	<svrl:fired-rule context="/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:ExemptionReasonCode"/>
	<svrl:successful-report location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:IncludedSupplyChainTradeLineItem[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:SpecifiedLineTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:ApplicableTradeTax[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:ExemptionReasonCode[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" test="true()">
		<svrl:text>
	Element 'ram:ExemptionReasonCode' is marked as not used in the given context.</svrl:text>
	</svrl:successful-report>

Mustang validator is only interested in failed-assert's.
The other one seems to be only informational messages, which are ignored.

@langfr langfr merged commit 34badcb into ZUGFeRD:master Jun 20, 2026
1 check 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.

ExemptionReason written to line item level violates EN16931 profile

3 participants