Skip to content

ZUGFERD2PullProvider EN16931 exported Document-level charges and allowances do not include CalculationPercent (BT-101) and BasisAmount (BT-100) #1022

@rjsmith

Description

@rjsmith

Summary

The ZUGFERD2PullProvider.generateXML() method for an EN16931 - profile document fails to print out the optional document-level charge <ram:CalculationPercent> (BT-101) and <ram:BasisAmount> (BT-100) fields if they have been set in a Charge object that was added to an Invoice using the setZFCharges() method.

Found in Mustang v2.21

Problem

It is not currently possible to get the document-level charge (BG-21) charge rate and basis amount optional fields included in the generated XML. Our visible PDF invoice includes both these charge-related information, so I understand we must then also include them in the embedded factur-x.xml file for the recommended EN16931 profile ZUGFeRD hybrid PDF.

Therefore, I understand, the resulting hybrid PDF does not comply with the ZUGFeRD requirement of equivalence between the visible invoice and the factur-x XML.

Related (see below), I also notice that the code does output the equivalent of these two fields for line - item charges (BT-143 and BT-142) for the EXTENDED profile only. But my copy of the 1_FACTUR-X 1.08 - 2025 12 04 - EN FR - VF file shows that these fields ARE allowed in the EN16931 profile:

Image

(Edit) This may be related to this existing open issue. #925

Affected Code Locations

1. ZUGFERD2PullProvider.generateXML() method

The code within this document-level charges block does not check if the Charge object's percent and basisAmount properties have been set and if they have, include the relevant BT-100 and BT-101 XML elements.

		if ((trans.getZFCharges() != null) && (trans.getZFCharges().length > 0)) {
			if ((profile == Profiles.getByName("XRechnung")) || (profile == Profiles.getByName("EN16931")) || (profile == Profiles.getByName("EXTENDED"))) {
				for (IZUGFeRDAllowanceCharge charge : trans.getZFCharges()) {

I notice that both these fields ARE optionally included for line item - related charges in the getItemTotalAllowanceChargeStr() method BUT ONLY if the profile is EXTENDED :

		if ((allowance.getPercent() != null) && (profile == Profiles.getByName("Extended"))) {
			percentage = "<ram:CalculationPercent>" + vatFormat(allowance.getPercent()) + "</ram:CalculationPercent>";
			percentage += "<ram:BasisAmount>" + item.getValue() + "</ram:BasisAmount>";
		}

Also I note that the document-level allowances export code has the same problem (missing optional BT-94 and BT-93 for EN16931 and EXTENDED profiles), so should probably be fixed also.

Suggested Fix

1. ZUGFERD2PullProvider.generateXML() method

Print the optional charge rate and basis amounts fields for document-level charges if they have been set for EN16931 and EXTENDED profiles:

		if ((trans.getZFCharges() != null) && (trans.getZFCharges().length > 0)) {
			if ((profile == Profiles.getByName("XRechnung")) || (profile == Profiles.getByName("EN16931")) || (profile == Profiles.getByName("EXTENDED"))) {
				for (IZUGFeRDAllowanceCharge charge : trans.getZFCharges()) {
					xml += "<ram:SpecifiedTradeAllowanceCharge>" +
						"<ram:ChargeIndicator>" +
						"<udt:Indicator>true</udt:Indicator>" +
						"</ram:ChargeIndicator>" +
						"<ram:ActualAmount>" + currencyFormat(charge.getTotalAmount(calc)) + "</ram:ActualAmount>";
					
					// ** NEW CODE **	
					if (charge.getPercent() != null && (profile == Profiles.getByName("EN16931") || (profile == Profiles.getByName("EXTENDED")))) {
						xml += "<ram:CalculationPercent>" + vatFormat(charge.getPercent()) + "</ram:CalculationPercent>" +
							 "<ram:BasisAmount>" + currencyFormat(charge.getBasisAmount()) + "</ram:BasisAmount>";
					}
					// ** END NEW CODE		 

					if (charge.getReasonCode() != null) {
						xml += "<ram:ReasonCode>" + charge.getReasonCode() + "</ram:ReasonCode>";
					}
					if (charge.getReason() != null) {
						xml += "<ram:Reason>" + XMLTools.encodeXML(charge.getReason()) + "</ram:Reason>";
					}
					xml += "<ram:CategoryTradeTax>" +
						"<ram:TypeCode>VAT</ram:TypeCode>" +
						"<ram:CategoryCode>" + charge.getCategoryCode() + "</ram:CategoryCode>";
					if (charge.getTaxPercent() != null && !charge.getCategoryCode().equals(TaxCategoryCodeTypeConstants.UNTAXEDSERVICE)) {
						xml += "<ram:RateApplicablePercent>" + vatFormat(charge.getTaxPercent()) + "</ram:RateApplicablePercent>";
					}
					xml += "</ram:CategoryTradeTax>" +
						"</ram:SpecifiedTradeAllowanceCharge>";
				}

2. ZUGFERD2PullProvider.generateXML() method

Equivalent new code from fix # 1 in the following document-level allowances section:

		if ((trans.getZFAllowances() != null) && (trans.getZFAllowances().length > 0)) {
			if (profile == Profiles.getByName("XRechnung")) {
				for (IZUGFeRDAllowanceCharge allowance : trans.getZFAllowances()) {
...

3. ZUGFERD2PullProvider.getItemTotalAllowanceChargeStr() method

Include optional BT-142 and BT-143 line-item charge & allowance fields if defined:

		if ((allowance.getPercent() != null) && (profile == Profiles.getByName("EN16931") || profile == Profiles.getByName("Extended"))) {
			percentage = "<ram:CalculationPercent>" + vatFormat(allowance.getPercent()) + "</ram:CalculationPercent>";
			percentage += "<ram:BasisAmount>" + item.getValue() + "</ram:BasisAmount>";
		}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions