From 98f0f140aab7f503616facbc583737ece7bae098 Mon Sep 17 00:00:00 2001 From: jstaerk Date: Sat, 7 Dec 2024 10:35:43 +0100 Subject: [PATCH] closes #606 --- History.md | 15 +-- .../ZUGFeRD/ZUGFeRDInvoiceImporter.java | 2 + .../mustangproject/ZUGFeRD/ZF2PushTest.java | 2 + .../validator/XMLValidator.java | 103 +++++++++--------- 4 files changed, 63 insertions(+), 59 deletions(-) diff --git a/History.md b/History.md index ccc1d053..0c3e518a 100644 --- a/History.md +++ b/History.md @@ -1,12 +1,6 @@ -595 -591 -577 -575 -573 -601 - 2.15.1 ======= + - #566 Failed to parse PDF - Could not reproduce the invoice ? be able to access ID in error message - closes #579 prepaidamount is only read in UBL @@ -18,6 +12,13 @@ - allow jackson to run over more classes, e.g., DirectDebit, bean contructor for direct debit - allow json includedNotes - support importing duePayableAmount +- #595 Treat all fatal XR schematron rules as errors, not as warnings. +- #591 Import IncludedNotes on item extraction +- #577 dom4j: exclude all (optional) dependencies to avoid potential conflicts. +- #575 Fix compilation problems. +- #573 Feature/category code +- #601 exceptions in metrics +- #606 also parse BT-25 2.15.0 diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java index abff63bb..ffcfaac1 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java @@ -808,6 +808,7 @@ public class ZUGFeRDInvoiceImporter { } else { zpp.setDespatchAdviceReferencedDocumentID(extractString("//*[local-name()=\"DespatchDocumentReference\"]/*[local-name()=\"ID\"]")); } + zpp.setInvoiceReferencedDocumentID(extractString("//*[local-name()=\"InvoiceReferencedDocument\"]/*[local-name()=\"IssuerAssignedID\"]|//*[local-name()=\"BillingReference\"]/*[local-name()=\"InvoiceDocumentReference\"]/*[local-name()=\"ID\"]")); zpp.setOwnOrganisationName(extractString("//*[local-name()=\"SellerTradeParty\"]/*[local-name()=\"Name\"]|//*[local-name()=\"AccountingSupplierParty\"]/*[local-name()=\"Party\"]/*[local-name()=\"PartyName\"]").trim()); @@ -929,6 +930,7 @@ public class ZUGFeRDInvoiceImporter { } + TransactionCalculator tc = new TransactionCalculator(zpp); String expectedStringTotalGross = tc.getGrandTotal() .subtract(Objects.requireNonNullElse(zpp.getTotalPrepaidAmount(), BigDecimal.ZERO)).toPlainString(); diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java index 6ac57f72..d435e58e 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java @@ -539,6 +539,7 @@ public class ZF2PushTest extends TestCase { .addAllowance(new Allowance(new BigDecimal(0.2)).setReason("discount").setTaxPercent(new BigDecimal(16))) .addCashDiscount(new CashDiscount(new BigDecimal(2), 14)) .setDeliveryDate(sdf.parse("2020-11-02")).setNumber(number).setVATDueDateTypeCode(EventTimeCodeTypeConstants.PAYMENT_DATE) + .setInvoiceReferencedDocumentID("abc123") ); } catch (ParseException e) { e.printStackTrace(); @@ -582,6 +583,7 @@ public class ZF2PushTest extends TestCase { try { Invoice i = zii.extractInvoice(); + assertEquals("abc123", i.getInvoiceReferencedDocumentID()); assertEquals("4304171000002", i.getRecipient().getGlobalID()); assertEquals("2001015001325", i.getZFItems()[0].getProduct().getGlobalID()); assertEquals(orgID, i.getSender().getID()); diff --git a/validator/src/main/java/org/mustangproject/validator/XMLValidator.java b/validator/src/main/java/org/mustangproject/validator/XMLValidator.java index 336701b3..77061ab9 100644 --- a/validator/src/main/java/org/mustangproject/validator/XMLValidator.java +++ b/validator/src/main/java/org/mustangproject/validator/XMLValidator.java @@ -1,4 +1,5 @@ package org.mustangproject.validator; + import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; @@ -69,15 +70,13 @@ public class XMLValidator extends Validator { final ValidationResultItem vri = new ValidationResultItem(ESeverity.exception, e.getMessage()).setSection(9) .setPart(EPart.fx); try (final StringWriter sw = new StringWriter(); - final PrintWriter pw = new PrintWriter(sw)) - { - e.printStackTrace(pw); - vri.setStacktrace(sw.toString()); - context.addResultItem(vri); + final PrintWriter pw = new PrintWriter(sw)) { + e.printStackTrace(pw); + vri.setStacktrace(sw.toString()); + context.addResultItem(vri); + } catch (IOException ex) { + throw new UncheckedIOException(ex); } - catch (IOException ex) { - throw new UncheckedIOException (ex); - } } } @@ -125,9 +124,9 @@ public class XMLValidator extends Validator { if (zfXML.isEmpty()) { final ValidationResultItem res = new ValidationResultItem(ESeverity.exception, - "XML data not found in " + filename - + ": did you specify a pdf or xml file and does the xml file contain an embedded XML file?") - .setSection(3); + "XML data not found in " + filename + + ": did you specify a pdf or xml file and does the xml file contain an embedded XML file?") + .setSection(3); context.addResultItem(res); } else { @@ -170,7 +169,7 @@ public class XMLValidator extends Validator { // Create XPath object final XPath xpath = xpathFactory.newXPath(); final XPathExpression expr = xpath.compile( - "(//*[local-name()=\"GuidelineSpecifiedDocumentContextParameter\"]/*[local-name()=\"ID\"])/text()|//*[local-name()=\"CustomizationID\"]/text()"); + "(//*[local-name()=\"GuidelineSpecifiedDocumentContextParameter\"]/*[local-name()=\"ID\"])/text()|//*[local-name()=\"CustomizationID\"]/text()"); // evaluate expression result on XML document ndList = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); @@ -191,7 +190,7 @@ public class XMLValidator extends Validator { boolean isExtended = false; boolean isXRechnung = false; String currentZFVersionDir = "ZF_232"; - int mainSchematronSectionErrorTypeCode=4; + int mainSchematronSectionErrorTypeCode = 4; String xsltFilename = null; // urn:ferd:CrossIndustryDocument:invoice:1p0:extended, // urn:ferd:CrossIndustryDocument:invoice:1p0:comfort, @@ -224,7 +223,7 @@ public class XMLValidator extends Validator { isBasic = false;// basicwl also contains the string basic... } isEN16931 = matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017:compliant:factur-x.eu:1p0:en16931") - || matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017"); + || matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017"); isExtended = context.getProfile().contains("extended"); isXRechnung = context.getProfile().contains("xrechnung"); @@ -269,16 +268,16 @@ public class XMLValidator extends Validator { // saxon java net.sf.saxon.Transform -o tcdl2.0.tsdtf.sch.tmp.xsl -s // tcdl2.0.tsdtf.sch iso_svrl.xsl - } else if (root.getLocalName().equalsIgnoreCase("Invoice") || root.getLocalName().equalsIgnoreCase("CreditNote") ) { + } else if (root.getLocalName().equalsIgnoreCase("Invoice") || root.getLocalName().equalsIgnoreCase("CreditNote")) { context.setGeneration("2"); context.setFormat("UBL"); isXRechnung = context.getProfile().contains("xrechnung"); // UBL LOGGER.debug("UBL"); - validateSchema(zfXML.getBytes(StandardCharsets.UTF_8), "UBL_21/maindoc/UBL-"+root.getLocalName()+"-2.1.xsd", 18, EPart.fx); + validateSchema(zfXML.getBytes(StandardCharsets.UTF_8), "UBL_21/maindoc/UBL-" + root.getLocalName() + "-2.1.xsd", 18, EPart.fx); xsltFilename = "/xslt/en16931schematron/EN16931-UBL-validation.xslt"; - mainSchematronSectionErrorTypeCode=24; + mainSchematronSectionErrorTypeCode = 24; if (isXRechnung) { validateSchematron(zfXML, xsltFilename, 24, ESeverity.error); @@ -287,14 +286,14 @@ public class XMLValidator extends Validator { XRechnung is a EN16931 subset so the validation vis a vis FACTUR-X_EN16931.xslt=schematron also has to pass * */ //validateSchema(zfXML.getBytes(StandardCharsets.UTF_8), "ZF_211/EN16931/FACTUR-X_EN16931.xsd", 18, EPart.fx); - String xrVersion=context.getProfile().substring(context.getProfile().length()-3).replace(".",""); - if (!xrVersion.equals("12")&&!xrVersion.equals("20")&&!xrVersion.equals("21")&&!xrVersion.equals("22")&&!xrVersion.equals("23")&&!xrVersion.equals("30")) { + String xrVersion = context.getProfile().substring(context.getProfile().length() - 3).replace(".", ""); + if (!xrVersion.equals("12") && !xrVersion.equals("20") && !xrVersion.equals("21") && !xrVersion.equals("22") && !xrVersion.equals("23") && !xrVersion.equals("30")) { throw new Exception("Unsupported XR version"); } - LOGGER.debug("is XRechnung v"+xrVersion); - xsltFilename = "/xslt/XR_"+xrVersion+"/XRechnung-UBL-validation.xslt"; + LOGGER.debug("is XRechnung v" + xrVersion); + xsltFilename = "/xslt/XR_" + xrVersion + "/XRechnung-UBL-validation.xslt"; XrechnungSeverity = ESeverity.error; - mainSchematronSectionErrorTypeCode=27; + mainSchematronSectionErrorTypeCode = 27; } @@ -302,58 +301,58 @@ public class XMLValidator extends Validator { context.setGeneration("1"); // if ((!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:basic")) - && (!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:comfort")) - && (!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:extended"))) { + && (!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:comfort")) + && (!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:extended"))) { context.addResultItem(new ValidationResultItem(ESeverity.error, "Unsupported profile type") - .setSection(25).setPart(EPart.fx)); + .setSection(25).setPart(EPart.fx)); } validateSchema(zfXML.getBytes(StandardCharsets.UTF_8), "ZF_10/ZUGFeRD1p0.xsd", 18, EPart.fx); xsltFilename = "/xslt/ZUGFeRD_1p0.xslt"; } else { // unknown document root context.addResultItem(new ValidationResultItem(ESeverity.fatal, "Unsupported root element") - .setSection(3).setPart(EPart.fx)); + .setSection(3).setPart(EPart.fx)); } if (context.getFormat().equals("CII")) { if (context.getGeneration().equals("2")) { if ((!matchesURI(context.getProfile(), "urn:factur-x.eu:1p0:minimum")) - && (!matchesURI(context.getProfile(), "urn:zugferd.de:2p0:minimum")) - && (!matchesURI(context.getProfile(), "urn:factur-x.eu:1p0:basicwl")) - && (!matchesURI(context.getProfile(), "urn:zugferd.de:2p0:basicwl")) - && (!matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic")) - && (!matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic")) - && (!matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017")) - && (!matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended")) - && (!matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended"))) { + && (!matchesURI(context.getProfile(), "urn:zugferd.de:2p0:minimum")) + && (!matchesURI(context.getProfile(), "urn:factur-x.eu:1p0:basicwl")) + && (!matchesURI(context.getProfile(), "urn:zugferd.de:2p0:basicwl")) + && (!matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic")) + && (!matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic")) + && (!matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017")) + && (!matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended")) + && (!matchesURI(context.getProfile(), "urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended"))) { context.addResultItem( - new ValidationResultItem(ESeverity.error, "Unsupported profile type " + context.getProfile()) - .setSection(25).setPart(EPart.fx)); + new ValidationResultItem(ESeverity.error, "Unsupported profile type " + context.getProfile()) + .setSection(25).setPart(EPart.fx)); } } else /** v1 */ { if (isOrderX) { //order-x 1.0 if ((!matchesURI(context.getProfile(), "urn:order-x.eu:1p0:basic")) - && (!matchesURI(context.getProfile(), "urn:order-x.eu:1p0:comfort")) - && (!matchesURI(context.getProfile(), "urn:order-x.eu:1p0:extended"))) { + && (!matchesURI(context.getProfile(), "urn:order-x.eu:1p0:comfort")) + && (!matchesURI(context.getProfile(), "urn:order-x.eu:1p0:extended"))) { //zf 1.0 context.addResultItem(new ValidationResultItem(ESeverity.error, "Unsupported profile type") - .setSection(25).setPart(EPart.fx)); + .setSection(25).setPart(EPart.fx)); } } else if ((!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:basic")) - && (!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:comfort")) - && (!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:extended"))) { + && (!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:comfort")) + && (!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:extended"))) { //zf 1.0 context.addResultItem(new ValidationResultItem(ESeverity.error, "Unsupported profile type") - .setSection(25).setPart(EPart.fx)); + .setSection(25).setPart(EPart.fx)); } } } - if (xsltFilename!=null) { + if (xsltFilename != null) { // main schematron validation validateSchematron(zfXML, xsltFilename, mainSchematronSectionErrorTypeCode, ESeverity.error); @@ -362,7 +361,7 @@ public class XMLValidator extends Validator { if (context.getFormat().equals("CII")) { if (context.getGeneration().equals("2") - && (isBasic || isEN16931 || isXRechnung)) { + && (isBasic || isEN16931 || isXRechnung)) { //additionally validate against CEN validateSchematron(zfXML, "/xslt/en16931schematron/EN16931-CII-validation.xslt", 24, ESeverity.error); if (!disableNotices || XrechnungSeverity != ESeverity.notice) { @@ -376,7 +375,7 @@ public class XMLValidator extends Validator { throw er; } catch (final Exception e) { final ValidationResultItem vri = new ValidationResultItem(ESeverity.exception, e.getMessage()).setSection(22) - .setPart(EPart.fx); + .setPart(EPart.fx); final StringWriter sw = new StringWriter(); final PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); @@ -388,8 +387,8 @@ public class XMLValidator extends Validator { final long endTime = Calendar.getInstance().getTimeInMillis(); context.addCustomXML("" + ((context.getGeneration() != null) ? context.getGeneration() : "invalid") - + "" + ((context.getProfile() != null) ? context.getProfile() : "invalid") + - "" + firedRules + "" + failedRules + "" + "" + (endTime - startXMLTime) + ""); + + "" + ((context.getProfile() != null) ? context.getProfile() : "invalid") + + "" + firedRules + "" + failedRules + "" + "" + (endTime - startXMLTime) + ""); } @@ -434,7 +433,7 @@ public class XMLValidator extends Validator { final SchematronOutputType sout; try { sout = aResSCH - .applySchematronValidationToSVRL(new StreamSource(new StringReader(xml))); + .applySchematronValidationToSVRL(new StreamSource(new StringReader(xml))); } catch (final Exception e) { throw new IrrecoverableValidationError(e.getMessage()); } @@ -471,7 +470,7 @@ public class XMLValidator extends Validator { if (defaultSeverity == ESeverity.notice) { severity = defaultSeverity; } else if (currentFailNode.getAttributes().getNamedItem("flag") != null - && currentFailNode.getAttributes().getNamedItem("flag").getNodeValue().equals("warning")) { + && currentFailNode.getAttributes().getNamedItem("flag").getNodeValue().equals("warning")) { // the XR issues warnings with flag=warning severity = ESeverity.warning; } else { @@ -493,8 +492,8 @@ public class XMLValidator extends Validator { LOGGER.info("FailedAssert ", thisFailText); context.addResultItem(new ValidationResultItem(severity, thisFailText + thisFailIDStr + " from " + xsltFilename + ")") - .setLocation(thisFailLocation).setCriterion(thisFailTest).setSection(section).setID(thisFailID) - .setPart(EPart.fx)); + .setLocation(thisFailLocation).setCriterion(thisFailTest).setSection(section).setID(thisFailID) + .setPart(EPart.fx)); failedRules++; } @@ -525,7 +524,7 @@ public class XMLValidator extends Validator { if (firedRules == 0) { context.addResultItem(new ValidationResultItem(ESeverity.error, "No rules matched, XML to minimal?").setSection(26) - .setPart(EPart.fx)); + .setPart(EPart.fx)); } // for (String currentString : sout.getText()) {