diff --git a/History.md b/History.md index 2c6aaace..a6ceeb0a 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,8 @@ + +- Fixed an error validation UBL files +- unknown root elements will now throw type 3 errors +- added some tests + 2.3.1 ======= 2021-10-25 diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java index c586fe0d..56db2c11 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java @@ -759,45 +759,45 @@ public class ZUGFeRDImporter { for (int j = 0; j < nodes.getLength(); j++) { n = nodes.item(j); final short nodeType = n.getNodeType(); - if (nodeType==Node.ELEMENT_NODE){ - switch (n.getNodeName()) { - case "ram:PostcodeCode": + if ((nodeType==Node.ELEMENT_NODE)&&(n.getLocalName()!=null)){ + switch (n.getLocalName()) { + case "PostcodeCode": address.setPostCodeCode(""); if (n.getFirstChild() != null) { address.setPostCodeCode(n.getFirstChild().getNodeValue()); } break; - case "ram:LineOne": + case "LineOne": address.setLineOne(""); if (n.getFirstChild() != null) { address.setLineOne(n.getFirstChild().getNodeValue()); } break; - case "ram:LineTwo": + case "LineTwo": address.setLineTwo(""); if (n.getFirstChild() != null) { address.setLineTwo(n.getFirstChild().getNodeValue()); } break; - case "ram:LineThree": + case "LineThree": address.setLineThree(""); if (n.getFirstChild() != null) { address.setLineThree(n.getFirstChild().getNodeValue()); } break; - case "ram:CityName": + case "CityName": address.setCityName(""); if (n.getFirstChild() != null) { address.setCityName(n.getFirstChild().getNodeValue()); } break; - case "ram:CountryID": + case "CountryID": address.setCountryID(""); if (n.getFirstChild() != null) { address.setCountryID(n.getFirstChild().getNodeValue()); } break; - case "ram:CountrySubDivisionName": + case "CountrySubDivisionName": address.setCountrySubDivisionName(""); if (n.getFirstChild() != null) { address.setCountrySubDivisionName(n.getFirstChild().getNodeValue()); @@ -827,99 +827,100 @@ public class ZUGFeRDImporter { for (int i = 0; i < nl.getLength(); i++) { final Node nn = nl.item(i); Node node = null; - switch (nn.getNodeName()) { - case "ram:SpecifiedLineTradeAgreement": - case "ram:SpecifiedSupplyChainTradeAgreement": + if (nn.getLocalName()!=null) { + switch (nn.getLocalName()) { + case "SpecifiedLineTradeAgreement": + case "SpecifiedSupplyChainTradeAgreement": - node = getNodeByName(nn.getChildNodes(), "ram:NetPriceProductTradePrice"); - if (node != null) { - final NodeList tradeAgreementChildren = node.getChildNodes(); - node = getNodeByName(tradeAgreementChildren, "ram:ChargeAmount"); - lineItem.setPrice(tryBigDecimal(getNodeValue(node))); - node = getNodeByName(tradeAgreementChildren, "ram:BasisQuantity"); - if(node != null && node.getAttributes()!=null) { - final Node unitCodeAttribute = node.getAttributes().getNamedItem("unitCode"); - if(unitCodeAttribute != null) { - lineItem.getProduct().setUnit(unitCodeAttribute.getNodeValue()); + node = getNodeByName(nn.getChildNodes(), "NetPriceProductTradePrice"); + if (node != null) { + final NodeList tradeAgreementChildren = node.getChildNodes(); + node = getNodeByName(tradeAgreementChildren, "ChargeAmount"); + lineItem.setPrice(tryBigDecimal(getNodeValue(node))); + node = getNodeByName(tradeAgreementChildren, "BasisQuantity"); + if (node != null && node.getAttributes() != null) { + final Node unitCodeAttribute = node.getAttributes().getNamedItem("unitCode"); + if (unitCodeAttribute != null) { + lineItem.getProduct().setUnit(unitCodeAttribute.getNodeValue()); + } } } - } - - node = getNodeByName(nn.getChildNodes(), "ram:GrossPriceProductTradePrice"); - if (node != null) { - node = getNodeByName(node.getChildNodes(), "ram:ChargeAmount"); - lineItem.setGrossPrice(tryBigDecimal(getNodeValue(node))); - } - break; - case "ram:AssociatedDocumentLineDocument": + node = getNodeByName(nn.getChildNodes(), "GrossPriceProductTradePrice"); + if (node != null) { + node = getNodeByName(node.getChildNodes(), "ChargeAmount"); + lineItem.setGrossPrice(tryBigDecimal(getNodeValue(node))); + } + break; - node = getNodeByName(nn.getChildNodes(), "ram:LineID"); - lineItem.setId(getNodeValue(node)); - break; + case "AssociatedDocumentLineDocument": - case "ram:SpecifiedTradeProduct": + node = getNodeByName(nn.getChildNodes(), "LineID"); + lineItem.setId(getNodeValue(node)); + break; - node = getNodeByName(nn.getChildNodes(), "ram:SellerAssignedID"); - lineItem.getProduct().setSellerAssignedID(getNodeValue(node)); + case "SpecifiedTradeProduct": - node = getNodeByName(nn.getChildNodes(), "ram:BuyerAssignedID"); - lineItem.getProduct().setBuyerAssignedID(getNodeValue(node)); + node = getNodeByName(nn.getChildNodes(), "SellerAssignedID"); + lineItem.getProduct().setSellerAssignedID(getNodeValue(node)); - node = getNodeByName(nn.getChildNodes(), "ram:Name"); - lineItem.getProduct().setName(getNodeValue(node)); + node = getNodeByName(nn.getChildNodes(), "BuyerAssignedID"); + lineItem.getProduct().setBuyerAssignedID(getNodeValue(node)); - node = getNodeByName(nn.getChildNodes(), "ram:Description"); - lineItem.getProduct().setDescription(getNodeValue(node)); - break; + node = getNodeByName(nn.getChildNodes(), "Name"); + lineItem.getProduct().setName(getNodeValue(node)); - case "ram:SpecifiedLineTradeDelivery": - case "ram:SpecifiedSupplyChainTradeDelivery": - node = getNodeByName(nn.getChildNodes(), "ram:BilledQuantity"); - lineItem.setQuantity(tryBigDecimal(getNodeValue(node))); - break; + node = getNodeByName(nn.getChildNodes(), "Description"); + lineItem.getProduct().setDescription(getNodeValue(node)); + break; - case "ram:SpecifiedLineTradeSettlement": + case "SpecifiedLineTradeDelivery": + case "SpecifiedSupplyChainTradeDelivery": + node = getNodeByName(nn.getChildNodes(), "BilledQuantity"); + lineItem.setQuantity(tryBigDecimal(getNodeValue(node))); + break; - node = getNodeByName(nn.getChildNodes(), "ram:ApplicableTradeTax"); - if (node != null) { - node = getNodeByName(node.getChildNodes(), "ram:RateApplicablePercent"); - lineItem.getProduct().setVATPercent(tryBigDecimal(getNodeValue(node))); - } + case "SpecifiedLineTradeSettlement": + node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax"); + if (node != null) { + node = getNodeByName(node.getChildNodes(), "RateApplicablePercent"); + lineItem.getProduct().setVATPercent(tryBigDecimal(getNodeValue(node))); + } - node = getNodeByName(nn.getChildNodes(), "ram:ApplicableTradeTax"); - if (node != null) { - node = getNodeByName(node.getChildNodes(), "ram:CalculatedAmount"); - lineItem.setTax(tryBigDecimal(getNodeValue(node))); - } + node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax"); + if (node != null) { + node = getNodeByName(node.getChildNodes(), "CalculatedAmount"); + lineItem.setTax(tryBigDecimal(getNodeValue(node))); + } - node = getNodeByName(nn.getChildNodes(), "ram:SpecifiedTradeSettlementLineMonetarySummation"); - if (node != null) { - node = getNodeByName(node.getChildNodes(), "ram:LineTotalAmount"); - lineItem.setLineTotalAmount(tryBigDecimal(getNodeValue(node))); - } - break; - case "ram:SpecifiedSupplyChainTradeSettlement": - //ZF 1! + node = getNodeByName(nn.getChildNodes(), "SpecifiedTradeSettlementLineMonetarySummation"); + if (node != null) { + node = getNodeByName(node.getChildNodes(), "LineTotalAmount"); + lineItem.setLineTotalAmount(tryBigDecimal(getNodeValue(node))); + } + break; + case "SpecifiedSupplyChainTradeSettlement": + //ZF 1! - node = getNodeByName(nn.getChildNodes(), "ram:ApplicableTradeTax"); - if (node != null) { - node = getNodeByName(node.getChildNodes(), "ram:ApplicablePercent"); - lineItem.getProduct().setVATPercent(tryBigDecimal(getNodeValue(node))); - } + node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax"); + if (node != null) { + node = getNodeByName(node.getChildNodes(), "ApplicablePercent"); + lineItem.getProduct().setVATPercent(tryBigDecimal(getNodeValue(node))); + } - node = getNodeByName(nn.getChildNodes(), "ram:ApplicableTradeTax"); - if (node != null) { - node = getNodeByName(node.getChildNodes(), "ram:CalculatedAmount"); - lineItem.setTax(tryBigDecimal(getNodeValue(node))); - } + node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax"); + if (node != null) { + node = getNodeByName(node.getChildNodes(), "CalculatedAmount"); + lineItem.setTax(tryBigDecimal(getNodeValue(node))); + } - node = getNodeByName(nn.getChildNodes(), "ram:SpecifiedTradeSettlementMonetarySummation"); - if (node != null) { - node = getNodeByName(node.getChildNodes(), "ram:LineTotalAmount"); - lineItem.setLineTotalAmount(tryBigDecimal(getNodeValue(node))); - } - break; + node = getNodeByName(nn.getChildNodes(), "SpecifiedTradeSettlementMonetarySummation"); + if (node != null) { + node = getNodeByName(node.getChildNodes(), "LineTotalAmount"); + lineItem.setLineTotalAmount(tryBigDecimal(getNodeValue(node))); + } + break; + } } } lineItemList.add(lineItem); @@ -959,7 +960,7 @@ public class ZUGFeRDImporter { */ private Node getNodeByName(NodeList nl, String name) { for (int i = 0; i < nl.getLength(); i++) { - if (nl.item(i).getNodeName() == name) { + if ((nl.item(i).getLocalName()!=null)&&(nl.item(i).getLocalName().equals(name))) { return nl.item(i); } else if (nl.item(i).getChildNodes().getLength() > 0) { final Node node = getNodeByName(nl.item(i).getChildNodes(), name); diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java index 9652c535..1be3c717 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java @@ -30,6 +30,8 @@ import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpressionException; +import java.io.BufferedWriter; +import java.io.FileWriter; import java.io.IOException; import java.io.StringReader; import java.math.BigDecimal; @@ -126,4 +128,42 @@ public class ZF2ZInvoiceImporterTest extends ResourceCase { } + public void testItemReferencedDocumentsImport() { + ZUGFeRDInvoiceImporter zii=new ZUGFeRDInvoiceImporter(); + + DocumentBuilderFactory db = DocumentBuilderFactory.newInstance(); + + boolean hasExceptions=false; + Invoice invoice=null; + try { + zii.doRecalculateItemPricesFromLineTotals(); + zii.doIgnoreCalculationErrors(); + zii.fromXML(new String(Files.readAllBytes(Paths.get(getResourceAsFile("factur-x-testImport-corrected.xml").getAbsolutePath())), StandardCharsets.UTF_8)); + invoice=zii.extractInvoice(); + } catch (XPathExpressionException | ParseException | IOException e) { + hasExceptions=true; + } + assertFalse(hasExceptions); + assertNotNull(invoice.getZFItems()[0].getReferencedDocuments()); + assertEquals(2, invoice.getZFItems()[0].getReferencedDocuments().length); + assertEquals("33807818630-5", invoice.getZFItems()[0].getReferencedDocuments()[0].getIssuerAssignedID()); + assertEquals("PL", invoice.getZFItems()[0].getReferencedDocuments()[1].getReferenceTypeCode()); + + ZUGFeRD2PullProvider zf2p = new ZUGFeRD2PullProvider(); + zf2p.setProfile(Profiles.getByName("Extended")); + zf2p.generateXML(invoice); + String theXML = new String(zf2p.getXML()); + try { + BufferedWriter writer = new BufferedWriter(new FileWriter("c:\\Users\\jstaerk\\Desktop\\xrechnung-written.xml")); + writer.write(theXML); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + TransactionCalculator tc=new TransactionCalculator(invoice); +// assertEquals(new BigDecimal("1284.66"),tc.getGrandTotal()); + assertEquals(new BigDecimal("1284.40"),tc.getGrandTotal()); + } + } diff --git a/validator/src/main/java/org/mustangproject/validator/XMLValidator.java b/validator/src/main/java/org/mustangproject/validator/XMLValidator.java index 0221a410..5b603b1f 100644 --- a/validator/src/main/java/org/mustangproject/validator/XMLValidator.java +++ b/validator/src/main/java/org/mustangproject/validator/XMLValidator.java @@ -181,6 +181,7 @@ public class XMLValidator extends Validator { boolean isEN16931 = false; boolean isExtended = false; boolean isXRechnung = false; + String xsltFilename = null; // urn:ferd:CrossIndustryDocument:invoice:1p0:extended, // urn:ferd:CrossIndustryDocument:invoice:1p0:comfort, @@ -188,7 +189,7 @@ public class XMLValidator extends Validator { // urn:cen.eu:en16931:2017 // urn:cen.eu:en16931:2017:compliant:factur-x.eu:1p0:basic - if (root.getNodeName().equalsIgnoreCase("rsm:SCRDMCCBDACIOMessageStructure")) { + if (root.getLocalName().equalsIgnoreCase("SCRDMCCBDACIOMessageStructure")) { context.setGeneration("1"); isOrderX=true; isBasic = context.getProfile().contains("basic"); @@ -197,7 +198,7 @@ public class XMLValidator extends Validator { validateSchema(zfXML.getBytes(StandardCharsets.UTF_8), "OX_10/comfort/SCRDMCCBDACIOMessageStructure_100pD20B.xsd", 99, EPart.ox); xsltFilename = "/xslt/OX_10/comfort/SCRDMCCBDACIOMessageStructure_100pD20B_COMFORT.xslt"; - } else if (root.getNodeName().equalsIgnoreCase("rsm:CrossIndustryInvoice")) { // ZUGFeRD 2.0 or Factur-X + } else if (root.getLocalName().equalsIgnoreCase("CrossIndustryInvoice")) { // ZUGFeRD 2.0 or Factur-X context.setGeneration("2"); isMiniumum = context.getProfile().contains("minimum"); @@ -252,7 +253,7 @@ 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.getNodeName().equalsIgnoreCase("Invoice")) { + } else if (root.getLocalName().equalsIgnoreCase("Invoice")) { context.setGeneration("2"); context.setFormat("UBL"); // UBL @@ -260,7 +261,7 @@ public class XMLValidator extends Validator { validateSchema(zfXML.getBytes(StandardCharsets.UTF_8), "UBL_21/maindoc/UBL-Invoice-2.1.xsd", 18, EPart.fx); xsltFilename = "/xslt/UBL_21/EN16931-UBL-validation.xsl"; XrechnungSeverity = ESeverity.error; - } else { // ZUGFeRD 1.0 + } else if (root.getLocalName().equalsIgnoreCase("CrossIndustryDocument")) { // ZUGFeRD 1.0 context.setGeneration("1"); // if ((!matchesURI(context.getProfile(), "urn:ferd:CrossIndustryDocument:invoice:1p0:basic")) @@ -272,6 +273,9 @@ public class XMLValidator extends Validator { 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)); } if (context.getFormat().equals("CII")) { diff --git a/validator/src/test/java/org/mustangproject/validator/XMLValidatorTest.java b/validator/src/test/java/org/mustangproject/validator/XMLValidatorTest.java index a9419f36..96c7582f 100644 --- a/validator/src/test/java/org/mustangproject/validator/XMLValidatorTest.java +++ b/validator/src/test/java/org/mustangproject/validator/XMLValidatorTest.java @@ -6,6 +6,8 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import javax.xml.transform.Source; @@ -25,6 +27,7 @@ public class XMLValidatorTest extends ResourceCase { File tempFile = getResourceAsFile("invalidV2.xml"); Source source; String content; + boolean noException = true; try { xv.setFilename(tempFile.getAbsolutePath()); @@ -70,8 +73,10 @@ public class XMLValidatorTest extends ResourceCase { xv.validate(); } catch (IrrecoverableValidationError e) { - // ignore, will be in XML output anyway + noException = false; //expecting a fatal error, i.e. an exception } + assertFalse(noException); + noException=true;// moving on... assertTrue(xv.getXMLResult().contains("" + xv.getXMLResult() + "").build(); content = xpath.evaluate("/validation/summary/@status", source); assertEquals("invalid", content); - } catch (IrrecoverableValidationError e) { // ignore, will be in XML output anyway + noException = false; } + assertTrue(noException); + try { + ctx.clear(); + tempFile = getResourceAsFile("invalidV2Root.xml"); + xv.setFilename(tempFile.getAbsolutePath()); + xv.validate(); + + } catch (IrrecoverableValidationError e) { + // do expect this! + noException = false; + } + assertFalse(noException); + noException=true; } @@ -222,4 +240,28 @@ public class XMLValidatorTest extends ResourceCase { } + public void testXRValidationUBL() { + ValidationContext ctx = new ValidationContext(null); + XMLValidator xv = new XMLValidator(ctx); + XPathEngine xpath = new JAXPXPathEngine(); + + boolean noExceptions = true; + File tempFile = getResourceAsFile("01.01a-INVOICE_ubl.xml"); + try { + xv.setFilename(tempFile.getAbsolutePath()); + xv.validate(); + + Source source = Input.fromString("" + xv.getXMLResult() + "").build(); + String content = xpath.evaluate("/validation/summary/@status", source); + // assertEquals("valid", content); + + + } catch (IrrecoverableValidationError e) { + + noExceptions = false; + } + assertTrue(noExceptions); + + } + } diff --git a/validator/src/test/resources/01.01a-INVOICE_ubl.xml b/validator/src/test/resources/01.01a-INVOICE_ubl.xml new file mode 100644 index 00000000..0f797879 --- /dev/null +++ b/validator/src/test/resources/01.01a-INVOICE_ubl.xml @@ -0,0 +1,142 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.1 + 123456XX + 2016-04-04+01:00 + 380 + #ADU#Es gelten unsere Allgem. Geschäftsbedingungen, die Sie unter […] finden. + EUR + 04011000-12345-34 + + + + [Seller trading name] + + + [Seller address line 1] + [Seller city] + 12345 + + DE + + + + DE 123456789 + + VAT + + + + [Seller name] + [HRA-Eintrag] + 123/456/7890, HRA-Eintrag in […] + + + nicht vorhanden + +49 1234-5678 + seller@email.de + + + + + + + [Buyer identifier] + + + [Buyer address line 1] + [Buyer city] + 12345 + + DE + + + + [Buyer name] + + + + + 58 + + + DE75512108001245126199 + + + + Zahlbar sofort ohne Abzug. + + + 22.04 + + 314.86 + 22.04 + + S + 7 + + VAT + + + + + + 314.86 + 314.86 + 336.9 + 336.9 + + + Zeitschrift [...] + Die letzte Lieferung im Rahmen des abgerechneten Abonnements erfolgt in 12/2016 Lieferung erfolgt / erfolgte direkt vom Verlag + 1 + 288.79 + + 2016-01-01+01:00 + 2016-12-31+01:00 + + + 6171175.1 + + + Zeitschrift Inland + Zeitschrift [...] + + 246 + + + 0721-880X + + + S + 7 + + VAT + + + + + 288.79 + + + + Porto + Versandkosten + 1 + 26.07 + + Porto + Versandkosten + + S + 7 + + VAT + + + + + 26.07 + + + diff --git a/validator/src/test/resources/invalidV2Root.xml b/validator/src/test/resources/invalidV2Root.xml new file mode 100644 index 00000000..2119d2e0 --- /dev/null +++ b/validator/src/test/resources/invalidV2Root.xml @@ -0,0 +1,186 @@ + + + + + urn:cen.eu:en16931:2017#compliant:factur-x.eu:1p0:en16931 + + + + RE-20171118/506 + 380 + 20171118 + + + + + 1 + + + Künstlerische Gestaltung (Stunde): Einer Beispielrechnung + + + + + 160.0000 + 1.0000 + + + 160.0000 + 1.0000 + + + + 1.0000 + + + + VAT + S + 7.00 + + + 160.00 + + + + + + 2 + + + Luftballon: Bunt, ca. 500ml + + + + + 0.7900 + 1.0000 + + + 0.7900 + 1.0000 + + + + 400.0000 + + + + VAT + S + 19.00 + + + 316.00 + + + + + + 3 + + + Heiße Luft pro Liter + + + + + 0.1000 + 1.0000 + + + 0.1000 + 1.0000 + + + + 200.0000 + + + + VAT + S + 19.00 + + + 20.00 + + + + + + Bei Spiel GmbH + + 12345 + Ecke 12 + Stadthausen + DE + + + 22/815/0815/4 + + + DE136695976 + + + + Theodor Est + + 88802 + Bahnstr. 42 + Spielkreis + DE + + + DE999999999 + + + + + + 20171117 + + + + RE-20171118/506 + EUR + + 42 + Überweisung + + DE88 2008 0000 0970 3757 00 + + + COBADEFFXXX + + + + 11.20 + VAT + 160.00 + S + 7.00 + + + 63.84 + VAT + 336.00 + S + 19.00 + + + Zahlbar ohne Abzug bis 09.12.2017 + 20171209 + + + 496.00 + 0.00 + 0.00 + 496.00 + 75.04 + 571.04 + 571.04 + + + +