diff --git a/History.md b/History.md index 604ccbfc..4d8c1b3b 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,5 @@ +2.9.0 +======= Missing closing tag in BankDetails when there's no BIC number #339 Have a way to merge to PDF file without knowing if it is A-1 or A-3 #341 diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java b/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java index 15a35b74..553890b8 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java @@ -33,7 +33,7 @@ public class Profiles { {"BASIC", new Profile("BASIC", "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic")}, {"EN16931", new Profile("EN16931", "urn:cen.eu:en16931:2017")}, {"EXTENDED", new Profile("EXTENDED", "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended")}, - {"XRECHNUNG", new Profile("XRECHNUNG", "urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3")} + {"XRECHNUNG", new Profile("XRECHNUNG", "urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3")} // up next: urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0 }).collect(Collectors.toMap(data -> (String) data[0], data -> (Profile) data[1])); static Map zf1Map = Stream.of(new Object[][]{ diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromPDFA.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromPDFA.java index 8830eb10..48b2129b 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromPDFA.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromPDFA.java @@ -27,20 +27,37 @@ import org.apache.xmpbox.XMPMetadata; import org.apache.xmpbox.schema.PDFAIdentificationSchema; import org.apache.xmpbox.xml.DomXmpParser; import org.apache.xmpbox.xml.XmpParsingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.activation.DataSource; import java.io.*; +/*** + * Auto-detects the source PDF-A-Version and acts accordingly + * like a ZUGFeRDExporterFromA1 or ZUGFeRDExporterFromA3 + */ public class ZUGFeRDExporterFromPDFA implements IZUGFeRDExporter { + + private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRDExporterFromPDFA.class.getCanonicalName()); // log + protected IZUGFeRDExporter theExporter; - public IZUGFeRDExporter load(String pdfFilename) throws IOException { - if (getPDFAVersion(fileToByteArrayInputStream(pdfFilename)) < 2) { - theExporter = new ZUGFeRDExporterFromA1(); - } else if (getPDFAVersion(fileToByteArrayInputStream(pdfFilename)) >= 3) { + protected void determineAndSetExporter(int PDFAVersion) { + if (PDFAVersion == 3) { theExporter = new ZUGFeRDExporterFromA3(); + } else if (PDFAVersion == 1) { + theExporter = new ZUGFeRDExporterFromA1(); + } else { + throw new IllegalArgumentException("PDF-A version not supported"); } - return theExporter.load(pdfFilename); + } + protected IZUGFeRDExporter getExporter() { + if (theExporter==null) { + throw new RuntimeException("In ZUGFeRDExporterFromPDFA, source must always be loaded before other operations are performed."); + } + + return theExporter; } private byte[] fileToByteArrayInputStream(String pdfFilename) throws IOException { @@ -48,12 +65,18 @@ public class ZUGFeRDExporterFromPDFA implements IZUGFeRDExporter { return fileInputStream.readAllBytes(); } + /*** + * + * @param byteArrayInputStream + * @return 0 if unknown, 1 for PDF/A-1 or 3 for PDF/A-3 + * @throws IOException + */ private int getPDFAVersion(byte[] byteArrayInputStream) throws IOException { - // PDFBOX to be here... PDDocument document = PDDocument.load(byteArrayInputStream); PDDocumentCatalog catalog = document.getDocumentCatalog(); PDMetadata metadata = catalog.getMetadata(); - + // the PDF version we could get through the document but we want the PDF-A version, + // which is different (and can probably base on different PDF versions) if (metadata != null) { try { DomXmpParser xmpParser = new DomXmpParser(); @@ -64,7 +87,7 @@ public class ZUGFeRDExporterFromPDFA implements IZUGFeRDExporter { return pdfaSchema.getPart(); } } catch (XmpParsingException e) { - e.printStackTrace(); + LOGGER.error("XmpParsingException", e); } finally { document.close(); } @@ -72,6 +95,18 @@ public class ZUGFeRDExporterFromPDFA implements IZUGFeRDExporter { return 0; } + /*** + * Load from filename + * @param pdfFilename binary of a PDF/A1 compliant document + * @return + * @throws IOException + */ + public IZUGFeRDExporter load(String pdfFilename) throws IOException { + determineAndSetExporter(getPDFAVersion(fileToByteArrayInputStream(pdfFilename))); + return theExporter.load(pdfFilename); + } + + /** * Makes A PDF/A3a-compliant document from a PDF-A1 compliant document (on the * metadata level, this will not e.g. convert graphics to JPG-2000) @@ -81,11 +116,7 @@ public class ZUGFeRDExporterFromPDFA implements IZUGFeRDExporter { * @throws IOException (should not happen at all) */ public IZUGFeRDExporter load(byte[] pdfBinary) throws IOException { - if (getPDFAVersion(pdfBinary) >= 3) { - theExporter = new ZUGFeRDExporterFromA3(); - } else if (getPDFAVersion(pdfBinary) < 2) { - theExporter = new ZUGFeRDExporterFromA1(); - } + determineAndSetExporter(getPDFAVersion(pdfBinary)); return theExporter.load(pdfBinary); } @@ -99,88 +130,94 @@ public class ZUGFeRDExporterFromPDFA implements IZUGFeRDExporter { * @throws IOException if anything is wrong with inputstream */ public IZUGFeRDExporter load(InputStream pdfSource) throws IOException { - if (getPDFAVersion(pdfSource.readAllBytes()) >= 3) { - theExporter = new ZUGFeRDExporterFromA3(); - } else if (getPDFAVersion(pdfSource.readAllBytes()) < 2) { - theExporter = new ZUGFeRDExporterFromA1(); - } + determineAndSetExporter(getPDFAVersion(pdfSource.readAllBytes())); return theExporter.load(pdfSource); } public IZUGFeRDExporter setCreator(String creator) { - return theExporter.setCreator(creator); + + return getExporter().setCreator(creator); } public ZUGFeRDExporterFromPDFA setProfile(Profile p) { - return (ZUGFeRDExporterFromPDFA) theExporter.setProfile(p); + return (ZUGFeRDExporterFromPDFA) getExporter().setProfile(p); } public ZUGFeRDExporterFromPDFA setProfile(String profileName) { Profile p = Profiles.getByName(profileName); - return (ZUGFeRDExporterFromPDFA) theExporter.setProfile(p); + if (p==null) { + throw new RuntimeException("Profile not found."); + } + return (ZUGFeRDExporterFromPDFA) getExporter().setProfile(p); } public IZUGFeRDExporter setConformanceLevel(PDFAConformanceLevel newLevel) { - return theExporter.setConformanceLevel(newLevel); + return getExporter().setConformanceLevel(newLevel); } public IZUGFeRDExporter setProducer(String producer) { - return theExporter.setProducer(producer); + + return getExporter().setProducer(producer); } public IZUGFeRDExporter setZUGFeRDVersion(int version) { - return theExporter.setZUGFeRDVersion(version); + return getExporter().setZUGFeRDVersion(version); } public boolean ensurePDFIsValid(final DataSource dataSource) throws IOException { - return theExporter.ensurePDFIsValid(dataSource); + + return getExporter().ensurePDFIsValid(dataSource); } public IZUGFeRDExporter setXML(byte[] zugferdData) throws IOException { - return theExporter.setXML(zugferdData); + + return getExporter().setXML(zugferdData); } public IZUGFeRDExporter disableFacturX() { - return theExporter.disableFacturX(); + + return getExporter().disableFacturX(); } // public IZUGFeRDExporter setProfile(Profile zugferdConformanceLevel); public String getNamespaceForVersion(int ver) { - return theExporter.getNamespaceForVersion(ver); + + return getExporter().getNamespaceForVersion(ver); } public String getPrefixForVersion(int ver) { - return theExporter.getPrefixForVersion(ver); + + return getExporter().getPrefixForVersion(ver); } public IZUGFeRDExporter disableAutoClose(boolean disableAutoClose) { - return theExporter.disableAutoClose(disableAutoClose); + return getExporter().disableAutoClose(disableAutoClose); } public IXMLProvider getProvider() { - return theExporter.getProvider(); + return getExporter().getProvider(); } @Override public void close() throws IOException { - theExporter.close(); + getExporter().close(); } @Override public IExporter setTransaction(IExportableTransaction trans) throws IOException { - return theExporter.setTransaction(trans); + return getExporter().setTransaction(trans); } @Override public void export(String ZUGFeRDfilename) throws IOException { - theExporter.export(ZUGFeRDfilename); + getExporter().export(ZUGFeRDfilename); } @Override public void export(OutputStream output) throws IOException { - theExporter.export(output); + getExporter().export(output); } } diff --git a/validator/src/test/java/org/mustangproject/validator/ZUGFeRDValidatorTest.java b/validator/src/test/java/org/mustangproject/validator/ZUGFeRDValidatorTest.java index e62c1bd3..2069dd2c 100644 --- a/validator/src/test/java/org/mustangproject/validator/ZUGFeRDValidatorTest.java +++ b/validator/src/test/java/org/mustangproject/validator/ZUGFeRDValidatorTest.java @@ -105,10 +105,75 @@ public class ZUGFeRDValidatorTest extends ResourceCase { .isEqualTo("invalid"); } - /*** - * the XMLValidatorTests only cover the part, this one includes the root element and - * the global part as well - */ + + public void testXR23Validation() { + File tempFile = getResourceAsFile("validXRV23.xml"); + ZUGFeRDValidator zfv = new ZUGFeRDValidator(); + + String res = zfv.validate(tempFile.getAbsolutePath()); + + + assertThat(res).valueByXPath("count(//error)") + .asInt() + .isEqualTo(1);// incorrectly throws an error due to https://awv-git.de/einvoicing/factur-x/-/issues/69 + + assertThat(res).valueByXPath("count(//notice)") + .asInt() + .isEqualTo(0); + assertThat(res).valueByXPath("/validation/summary/@status") + .asString() + .isEqualTo("invalid");// expect to be valid because XR notices are, well, only notices + assertThat(res).valueByXPath("/validation/xml/summary/@status") + .asString() + .isEqualTo("invalid"); + + + } + public void testXR30Validation() { + + File tempFile = getResourceAsFile("validXRV30.xml"); + ZUGFeRDValidator zfv = new ZUGFeRDValidator(); + + String res = zfv.validate(tempFile.getAbsolutePath()); + assertThat(res).valueByXPath("count(//error)") + .asInt() + .isEqualTo(1);// incorrectly throws an error due to https://awv-git.de/einvoicing/factur-x/-/issues/69 + + assertThat(res).valueByXPath("count(//notice)") + .asInt() + .isEqualTo(0); + assertThat(res).valueByXPath("/validation/summary/@status") + .asString() + .isEqualTo("invalid");// expect to be valid because XR notices are, well, only notices + assertThat(res).valueByXPath("/validation/xml/summary/@status") + .asString() + .isEqualTo("invalid"); + + tempFile = getResourceAsFile("invalidXRV30.xml"); + zfv = new ZUGFeRDValidator(); + res = zfv.validate(tempFile.getAbsolutePath()); + + assertThat(res).valueByXPath("count(//error)") + .asInt() + .isEqualTo(4); //should be 3 + + assertThat(res).valueByXPath("count(//notice)") + .asInt() + .isEqualTo(0); // 12 notices RE XRechnung 3.0 + assertThat(res).valueByXPath("/validation/summary/@status") + .asString() + .isEqualTo("invalid");// expect to be valid + assertThat(res).valueByXPath("/validation/xml/summary/@status") + .asString() + .isEqualTo("invalid");// expect to be valid + + + } + + /*** + * the XMLValidatorTests only cover the part, this one includes the root element and + * the global part as well + */ public void testXMLValidation() { File tempFile = getResourceAsFile("validV2.xml"); ZUGFeRDValidator zfv = new ZUGFeRDValidator(); @@ -121,7 +186,7 @@ public class ZUGFeRDValidatorTest extends ResourceCase { assertThat(res).valueByXPath("count(//notice)") .asInt() - .isEqualTo(3); // 3 notices RE XRechnung + .isEqualTo(12); // 12 notices RE XRechnung 3.0 assertThat(res).valueByXPath("/validation/summary/@status") .asString() .isEqualTo("valid");// expect to be valid because XR notices are, well, only notices diff --git a/validator/src/test/resources/invalidXRV30.xml b/validator/src/test/resources/invalidXRV30.xml new file mode 100644 index 00000000..6b62123d --- /dev/null +++ b/validator/src/test/resources/invalidXRV30.xml @@ -0,0 +1,168 @@ + + + + + urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0 + + + + 123456XX + 380 + + 20160404 + + + Es gelten unsere Allgem. Geschäftsbedingungen, die Sie unter […] finden. + ADU + + + + + + Zeitschrift [...] + + Die letzte Lieferung im Rahmen des abgerechneten Abonnements erfolgt in 12/2016 Lieferung erfolgt / erfolgte direkt vom Verlag + + + + 246 + Zeitschrift [...] + Zeitschrift Inland + + 0721-880X + + + + + 6171175.1 + + + 288.79 + + + + 1 + + + + VAT + S + 7 + + + + 20160101 + + + 20161231 + + + + 288.79 + + + + + + Porto + Versandkosten + + + Porto + Versandkosten + + + + 26.07 + + + + 1 + + + + VAT + S + 7 + + + 26.07 + + + + + + + [Seller name] + 123/456/7890, HRA-Eintrag in […] + + [HRA-Eintrag] + [Seller trading name] + + + nicht vorhanden + + +49 1234-5678 + + + seller@email.de + + + + 12345 + [Seller address line 1] + [Seller city] + DE + + + seller@email.de + + + DE 123456789 + + + + [Buyer identifier] + [Buyer name] + + 12345 + [Buyer address line 1] + [Buyer city] + DE + + + buyer@info.de + + + + + + EUR + + 58 + + + DE75512108001245126199 + + + + 22.04 + VAT + 314.86 + S + 7 + + + Zahlbar sofort ohne Abzug. + + + 314.86 + 314.86 + 22.04 + 336.9 + 336.91 + + + + diff --git a/validator/src/test/resources/validXRV23.xml b/validator/src/test/resources/validXRV23.xml new file mode 100644 index 00000000..603e7a12 --- /dev/null +++ b/validator/src/test/resources/validXRV23.xml @@ -0,0 +1,162 @@ + + + + + urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3 + + + + 123456XX + 380 + + 20160404 + + + Es gelten unsere Allgem. Geschäftsbedingungen, die Sie unter […] finden. + ADU + + + + + + Zeitschrift [...] + + Die letzte Lieferung im Rahmen des abgerechneten Abonnements erfolgt in 12/2016 Lieferung erfolgt / erfolgte direkt vom Verlag + + + + 246 + Zeitschrift [...] + Zeitschrift Inland + + 0721-880X + + + + + 6171175.1 + + + 288.79 + + + + 1 + + + + VAT + S + 7 + + + + 20160101 + + + 20161231 + + + + 288.79 + + + + + + Porto + Versandkosten + + + Porto + Versandkosten + + + + 26.07 + + + + 1 + + + + VAT + S + 7 + + + 26.07 + + + + + 04011000-12345-03 + + [Seller name] + 123/456/7890, HRA-Eintrag in […] + + [HRA-Eintrag] + [Seller trading name] + + + nicht vorhanden + + +49 1234-5678 + + + seller@email.de + + + + 12345 + [Seller address line 1] + [Seller city] + DE + + + DE 123456789 + + + + [Buyer identifier] + [Buyer name] + + 12345 + [Buyer address line 1] + [Buyer city] + DE + + + + + + EUR + + 58 + + + DE75512108001245126199 + + + + 22.04 + VAT + 314.86 + S + 7 + + + Zahlbar sofort ohne Abzug. + + + 314.86 + 314.86 + 22.04 + 336.9 + 336.9 + + + + diff --git a/validator/src/test/resources/validXRV30.xml b/validator/src/test/resources/validXRV30.xml new file mode 100644 index 00000000..8587992c --- /dev/null +++ b/validator/src/test/resources/validXRV30.xml @@ -0,0 +1,171 @@ + + + + + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + + + urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0 + + + + 123456XX + 380 + + 20160404 + + + Es gelten unsere Allgem. Geschäftsbedingungen, die Sie unter […] finden. + ADU + + + + + + Zeitschrift [...] + + Die letzte Lieferung im Rahmen des abgerechneten Abonnements erfolgt in 12/2016 Lieferung erfolgt / erfolgte direkt vom Verlag + + + + 246 + Zeitschrift [...] + Zeitschrift Inland + + 0721-880X + + + + + 6171175.1 + + + 288.79 + + + + 1 + + + + VAT + S + 7 + + + + 20160101 + + + 20161231 + + + + 288.79 + + + + + + Porto + Versandkosten + + + Porto + Versandkosten + + + + 26.07 + + + + 1 + + + + VAT + S + 7 + + + 26.07 + + + + + 04011000-12345-03 + + [Seller name] + 123/456/7890, HRA-Eintrag in […] + + [HRA-Eintrag] + [Seller trading name] + + + nicht vorhanden + + +49 1234-5678 + + + seller@email.de + + + + 12345 + [Seller address line 1] + [Seller city] + DE + + + seller@email.de + + + DE 123456789 + + + + [Buyer identifier] + [Buyer name] + + 12345 + [Buyer address line 1] + [Buyer city] + DE + + + buyer@info.de + + + + + + EUR + + 58 + + + DE75512108001245126199 + + + + 22.04 + VAT + 314.86 + S + 7 + + + Zahlbar sofort ohne Abzug. + + + 314.86 + 314.86 + 22.04 + 336.9 + 336.9 + + + +