Add SubInvoiceLine support for ZUGFeRD Extended profile
- Parse ParentLineID and LineStatusReasonCode from XML - Filter GROUP/INFORMATION lines from calculation (only DETAIL counts) - Add hierarchy validation for GROUP line totals - Add tests for sub invoice line import and validation
This commit is contained in:
@@ -5,6 +5,7 @@ import java.io.PrintWriter;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
@@ -24,6 +25,8 @@ import javax.xml.xpath.XPathFactory;
|
||||
|
||||
import org.mustangproject.CalculatedInvoice;
|
||||
import org.mustangproject.XMLTools;
|
||||
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem;
|
||||
import org.mustangproject.ZUGFeRD.LineCalculator;
|
||||
import org.mustangproject.ZUGFeRD.ZUGFeRDInvoiceImporter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -454,6 +457,9 @@ public class XMLValidator extends Validator {
|
||||
CalculatedInvoice ci=new CalculatedInvoice();
|
||||
zi.extractInto(ci);
|
||||
|
||||
// check sub invoice line hierarchy if present
|
||||
checkSubInvoiceLineHierarchy(ci, context);
|
||||
|
||||
} catch ( ArithmeticException e) {
|
||||
try {
|
||||
context.addResultItem(new ValidationResultItem(ESeverity.warning, "Arithmetical issue:"+e.getMessage()).setSection(10));
|
||||
@@ -469,6 +475,77 @@ public class XMLValidator extends Validator {
|
||||
|
||||
}
|
||||
|
||||
/***
|
||||
* validates that GROUP line totals match the sum of their DETAIL child lines
|
||||
*/
|
||||
private void checkSubInvoiceLineHierarchy(CalculatedInvoice invoice, ValidationContext context) {
|
||||
IZUGFeRDExportableItem[] items = invoice.getZFItems();
|
||||
if (items == null || items.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if we have any sub invoice lines at all
|
||||
boolean hasSubInvoiceLines = false;
|
||||
for (IZUGFeRDExportableItem item : items) {
|
||||
if (item.getLineStatusReasonCode() != null) {
|
||||
hasSubInvoiceLines = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasSubInvoiceLines) {
|
||||
return;
|
||||
}
|
||||
|
||||
// build a map of line ID to item for quick lookup
|
||||
java.util.HashMap<String, IZUGFeRDExportableItem> itemMap = new java.util.HashMap<>();
|
||||
for (IZUGFeRDExportableItem item : items) {
|
||||
if (item.getId() != null) {
|
||||
itemMap.put(item.getId(), item);
|
||||
}
|
||||
}
|
||||
|
||||
// for each GROUP line, sum up the DETAIL children and compare
|
||||
for (IZUGFeRDExportableItem item : items) {
|
||||
if ("GROUP".equals(item.getLineStatusReasonCode())) {
|
||||
String groupId = item.getId();
|
||||
if (groupId == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// sum up direct DETAIL children
|
||||
BigDecimal childSum = BigDecimal.ZERO;
|
||||
for (IZUGFeRDExportableItem child : items) {
|
||||
if (groupId.equals(child.getParentLineID()) && "DETAIL".equals(child.getLineStatusReasonCode())) {
|
||||
LineCalculator lc = child.getCalculation();
|
||||
childSum = childSum.add(lc.getItemTotalNetAmount());
|
||||
}
|
||||
}
|
||||
|
||||
// also sum up nested GROUP children (their totals should already include their DETAIL children)
|
||||
for (IZUGFeRDExportableItem child : items) {
|
||||
if (groupId.equals(child.getParentLineID()) && "GROUP".equals(child.getLineStatusReasonCode())) {
|
||||
LineCalculator lc = child.getCalculation();
|
||||
childSum = childSum.add(lc.getItemTotalNetAmount());
|
||||
}
|
||||
}
|
||||
|
||||
// compare with GROUP total
|
||||
LineCalculator groupLc = item.getCalculation();
|
||||
BigDecimal groupTotal = groupLc.getItemTotalNetAmount();
|
||||
if (childSum.compareTo(groupTotal) != 0) {
|
||||
try {
|
||||
context.addResultItem(new ValidationResultItem(ESeverity.warning,
|
||||
"Sub invoice line hierarchy mismatch: GROUP line " + groupId +
|
||||
" has total " + groupTotal + " but sum of child lines is " + childSum)
|
||||
.setSection(10));
|
||||
} catch (IrrecoverableValidationError ie) {
|
||||
LOGGER.error(ie.getMessage(), ie);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void validateXR(String xml, ESeverity errorImpact) throws IrrecoverableValidationError {
|
||||
|
||||
//Guideline ID=urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_1.2 or
|
||||
|
||||
@@ -373,4 +373,28 @@ public class XMLValidatorTest extends ResourceCase {
|
||||
|
||||
}
|
||||
|
||||
public void testSubInvoiceLineHierarchy() {
|
||||
final ValidationContext ctx = new ValidationContext(null);
|
||||
final XMLValidator xv = new XMLValidator(ctx);
|
||||
final XPathEngine xpath = new JAXPXPathEngine();
|
||||
|
||||
// test invalid hierarchy: GROUP sum does not match DETAIL children sum
|
||||
// GROUP 01 has LineTotalAmount=999, but children sum to 1050 (600+450)
|
||||
File tempFile = getResourceAsFile("invalidSubInvoiceLineHierarchy.xml");
|
||||
try {
|
||||
xv.setFilename(tempFile.getAbsolutePath());
|
||||
xv.validate();
|
||||
|
||||
String s = "<validation>" + xv.getXMLResult() + "</validation>";
|
||||
// hierarchy mismatch should produce at least one warning
|
||||
assertThat(s).valueByXPath("count(//warning)")
|
||||
.asInt()
|
||||
.isGreaterThanOrEqualTo(1);
|
||||
|
||||
} catch (final IrrecoverableValidationError e) {
|
||||
// ignore, will be in XML output anyway
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user