diff --git a/.gitignore b/.gitignore
index ece11c66..2702c983 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ target/
pom.xml.*
bin/
/.gradle/
+log/
# ignore intelliJ foobar
*.iml
@@ -14,4 +15,4 @@ bin/
# ignore test files
ZUGFeRD-*-test
-dependency-reduced-pom.xml
\ No newline at end of file
+dependency-reduced-pom.xml
diff --git a/Mustang-CLI/pom.xml b/Mustang-CLI/pom.xml
index 13e84ff6..7b096ff8 100644
--- a/Mustang-CLI/pom.xml
+++ b/Mustang-CLI/pom.xml
@@ -181,6 +181,19 @@
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 2.22.2
+
+
+
+ integration-test
+ verify
+
+
+
+
diff --git a/Mustang-CLI/src/test/java/org/mustangproject/commandline/CliIT.java b/Mustang-CLI/src/test/java/org/mustangproject/commandline/CliIT.java
new file mode 100644
index 00000000..43a1a2d1
--- /dev/null
+++ b/Mustang-CLI/src/test/java/org/mustangproject/commandline/CliIT.java
@@ -0,0 +1,47 @@
+package org.mustangproject.commandline;
+
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertTrue;
+
+public class CliIT {
+
+ @Test
+ public void testCii2Ubl() throws Exception {
+ Path output = Paths.get("target/ubl.xml");
+ Files.deleteIfExists(output);
+ Path jar = Files.newDirectoryStream(Paths.get("target"), "Mustang-CLI-*.jar").iterator().next();
+ ProcessBuilder pb = new ProcessBuilder("java", "-jar", jar.toString(),
+ "--action", "ubl", "--source", "src/test/resources/cii.xml", "--out",
+ output.toString());
+ pb.redirectErrorStream(true);
+ Process process = pb.start();
+ String result = getOutput(process);
+ process.waitFor(10, TimeUnit.SECONDS);
+ if (!result.isEmpty()) {
+ System.out.println(result);
+ }
+ assertTrue(new String(Files.readAllBytes(output), StandardCharsets.UTF_8).contains("Invoice"));
+ }
+
+ private String getOutput(Process process) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+ StringBuilder builder = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ builder.append(line);
+ builder.append(System.getProperty("line.separator"));
+ }
+ return builder.toString();
+ }
+
+}
diff --git a/Mustang-CLI/src/test/resources/cii.xml b/Mustang-CLI/src/test/resources/cii.xml
new file mode 100644
index 00000000..44ce2bd5
--- /dev/null
+++ b/Mustang-CLI/src/test/resources/cii.xml
@@ -0,0 +1,191 @@
+
+
+
+
+
+ urn:cen.eu:en16931:2017
+
+
+
+ RE-20201121/508
+ 380
+
+ 20201121
+
+
+
+
+
+ 1
+
+
+ Design (hours)
+ Of a sample invoice
+
+
+
+ 160.0000
+ 1.0000
+
+
+ 160.0000
+ 1.0000
+
+
+
+ 1.0000
+
+
+
+ VAT
+ S
+ 7.00
+
+
+ 160.00
+
+
+
+
+
+ 2
+
+
+ Ballons
+ various colors, ~2000ml
+
+
+
+ 0.7900
+ 1.0000
+
+
+ 0.7900
+ 1.0000
+
+
+
+ 400.0000
+
+
+
+ VAT
+ S
+ 19.00
+
+
+ 316.00
+
+
+
+
+
+ 3
+
+
+ Hot air „heiße Luft“ (litres)
+
+
+
+
+ 0.0250
+ 1.0000
+
+
+ 0.0250
+ 1.0000
+
+
+
+ 800.0000
+
+
+
+ VAT
+ S
+ 19.00
+
+
+ 20.00
+
+
+
+
+ AB321
+
+ Bei Spiel GmbH
+
+ 12345
+ Ecke 12
+ Stadthausen
+ DE
+
+
+ DE136695976
+
+
+
+ 2
+ Theodor Est
+
+ 88802
+ Bahnstr. 42
+ Spielkreis
+ DE
+
+
+
+
+
+
+ 20201110
+
+
+
+
+ RE-20201121/508
+ EUR
+
+ 42
+ Bank transfer
+
+ DE88200800000970375700
+ Max Mustermann
+
+
+ COBADEFFXXX
+
+
+
+ 11.20
+ VAT
+ 160.00
+ S
+ 7.00
+
+
+ 63.84
+ VAT
+ 336.00
+ S
+ 19.00
+
+
+ Zahlbar ohne Abzug bis 12.12.2020
+
+ 20201212
+
+
+
+ 496.00
+ 0.00
+ 0.00
+ 496.00
+ 75.04
+ 571.04
+ 0.00
+ 571.04
+
+
+
+
diff --git a/library/pom.xml b/library/pom.xml
index a5131c9d..072fcd43 100644
--- a/library/pom.xml
+++ b/library/pom.xml
@@ -122,6 +122,7 @@
org.xmlunit
xmlunit-assertj
2.6.3
+ test
@@ -153,7 +154,7 @@
maven-shade-plugin
3.2.1
- false
+ true
false
diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java b/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java
index dc0d6e4f..f91a6979 100644
--- a/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java
+++ b/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java
@@ -57,4 +57,31 @@ public class Profiles {
return getByName(name, ZUGFeRDExporterFromA3.DefaultZUGFeRDVersion);
}
+
+ /**
+ * Obtains a profile by name, disregarding version. First searches among
+ * 1.x profiles and fatre that among 2.x profiles.
+ *
+ * @param name profile name to search for
+ *
+ * @return a profile matching the requested name
+ */
+ public static Profile getByNameDisregardingVersion(String name)
+ {
+ Profile result = null;
+
+ result = zf1Map.get(name.toUpperCase());
+
+ if (result == null)
+ {
+ result = zf2Map.get(name.toUpperCase());
+ }
+
+ if (result==null)
+ {
+ throw new RuntimeException("Profile " + name + " not found");
+ }
+
+ return result;
+ }
}
diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java b/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java
index bb3e3a75..c8eadb32 100644
--- a/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java
+++ b/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java
@@ -40,8 +40,10 @@ public class XMPSchemaZugferd extends XMPSchema {
* @param URN the xml URI for the XMP
* @param prefix the xml namespace prefix for the XMP, zf for ZUGFeRD, fx for Factur-X
* @param filename the filename of the invoice
+ * @param version meta-data version to set. May be null, and then it is set automatically
*/
- public XMPSchemaZugferd(XMPMetadata metadata, int zfVersion, boolean isFacturX, Profile conformanceLevel, String URN, String prefix, String filename) {
+ public XMPSchemaZugferd(XMPMetadata metadata, int zfVersion, boolean isFacturX, Profile conformanceLevel,
+ String URN, String prefix, String filename, String version) {
super(metadata, URN, prefix, "ZUGFeRD Schema");
setAboutAsSimple("");
@@ -50,9 +52,12 @@ public class XMPSchemaZugferd extends XMPSchema {
setTextPropertyValue("ConformanceLevel", conformanceLevelValue);
setTextPropertyValue("DocumentType", "INVOICE");
setTextPropertyValue("DocumentFileName", filename);
- String version="1.0";
- if ((zfVersion==2)&&(!isFacturX)) {
- version="2p0";
+
+ if (version == null) {
+ version = "1.0";
+ if ((zfVersion==2)&&(!isFacturX)) {
+ version="2p0";
+ }
}
setTextPropertyValue("Version", version);
}
diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java
index 4113811d..f74f8d00 100644
--- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java
+++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java
@@ -108,7 +108,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
@Deprecated
protected String subject;
- private PDDocument doc;
+ protected PDDocument doc;
private HashMap additionalXMLs = new HashMap();
@@ -116,6 +116,11 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
protected int ZFVersion = DefaultZUGFeRDVersion;
private boolean attachZUGFeRDHeaders = true;
+ // Specific metaData version in case of XRechnung. We need it to be settable
+ // by the caller if necessary.
+ protected String XRechnungVersion = null; // Default XRechnung as of late 2021 is 2p0
+
+
/**
* 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)
@@ -162,13 +167,18 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
* @return the URN of the namespace
*/
public String getNamespaceForVersion(int ver) {
+ // In the case of XRechnung, it is the same as Factur-X
+ if ((ver >= 2) && (this.profile != null) &&
+ this.profile.getName().equalsIgnoreCase(Profiles.getByName("XRECHNUNG").getName()))
+ {
+ return "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#";
+ } else
if (isFacturX) {
return "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#";
} else if (ver == 1) {
return "urn:ferd:pdfa:CrossIndustryDocument:invoice:1p0#";
} else if (ver == 2) {
return "urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0#";
-
} else {
throw new IllegalArgumentException("Version not supported");
}
@@ -180,6 +190,12 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
* @return the namespace prefix as string, without colon
*/
public String getPrefixForVersion(int ver) {
+ // In the case of XRechnung, it is the same as Factur-X
+ if ((ver >= 2) && (this.profile != null) &&
+ this.profile.getName().equalsIgnoreCase(Profiles.getByName("XRECHNUNG").getName()))
+ {
+ return "fx";
+ } else
if (isFacturX) {
return "fx";
} else {
@@ -187,6 +203,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
}
}
+
/***
* internal helper: return the name of the file attachment for the given zf/fx version
* @param ver the zf/fx version
@@ -194,13 +211,17 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
* @return the filename of the file to be embedded
*/
public String getFilenameForVersion(int ver, Profile profile) {
- if (isFacturX) {
+ boolean isXRechnung =
+ (ver >= 2) && (this.profile != null) &&
+ this.profile.getName().equalsIgnoreCase(Profiles.getByName("XRECHNUNG").getName());
+
+ if (isFacturX && (!isXRechnung)) {
return "factur-x.xml";
} else {
if (ver == 1) {
return "ZUGFeRD-invoice.xml";
} else {
- if (profile.getName().equals("XRECHNUNG")) {
+ if (isXRechnung) {
return "xrechnung.xml";
} else {
return "zugferd-invoice.xml";
@@ -222,6 +243,19 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
return this;
}
+ /**
+ * Sets a specific XRechnung version from outside. This version needs to be present in the
+ * meta-data as well. The caller may wish to generate a specific version of XRechnung
+ * depending on the time period etc.
+ *
+ * @param XRechnungVersion the XRechnung version
+ */
+ public void setXRechnungSpecificVersion(String XRechnungVersion)
+ {
+ this.XRechnungVersion = XRechnungVersion;
+ }
+
+
/***
* Generate ZF2.0/2.1 files with filename zugferd-invoice.xml instead of factur-x.xml
*/
@@ -302,6 +336,24 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
}
}
+
+ /**
+ * Embeds an external file (generic - any type allowed) in the PDF.
+ * The embedding is done in the default PDF document.
+ *
+ * @param filename name of the file that will become attachment name in the PDF
+ * @param relationship how the file relates to the content, e.g. "Alternative"
+ * @param description Human-readable description of the file content
+ * @param subType type of the data e.g. could be "text/xml" - mime like
+ * @param data the binary data of the file/attachment
+ * @throws java.io.IOException if anything is wrong with filename
+ */
+ public void PDFAttachGenericFile(String filename, String relationship, String description,
+ String subType, byte[] data) throws IOException {
+ PDFAttachGenericFile(this.doc, filename, relationship, description, subType, data);
+ }
+
+
/**
* Embeds an external file (generic - any type allowed) in the PDF.
*
@@ -468,10 +520,19 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
*/
protected void addXMP(XMPMetadata metadata) {
+ String metaDataVersion = null; // default will be used
+
+ // The XRechnung version may be settable from outside.
+ if ((this.XRechnungVersion != null) && (this.profile != null) &&
+ this.profile.getName().equalsIgnoreCase(Profiles.getByName("XRECHNUNG").getName()))
+ {
+ metaDataVersion = this.XRechnungVersion;
+ }
+
if (attachZUGFeRDHeaders) {
XMPSchemaZugferd zf = new XMPSchemaZugferd(metadata, ZFVersion, isFacturX, xmlProvider.getProfile(),
getNamespaceForVersion(ZFVersion), getPrefixForVersion(ZFVersion),
- getFilenameForVersion(ZFVersion, xmlProvider.getProfile()));
+ getFilenameForVersion(ZFVersion, xmlProvider.getProfile()), metaDataVersion);
metadata.addSchema(zf);
}
@@ -481,8 +542,9 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
metadata.addSchema(pdfaex);
}
- private void removeCidSet(PDDocumentCatalog catalog, PDDocument doc) {
-
+ private void removeCidSet(PDDocumentCatalog catalog, PDDocument doc)
+ throws IOException
+ {
// https://github.com/ZUGFeRD/mustangproject/issues/249
COSName cidSet = COSName.getPDFName("CIDSet");
@@ -503,12 +565,12 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
if (typedFont.getDescendantFont() instanceof PDCIDFontType2) {
PDCIDFontType2 f = (PDCIDFontType2) typedFont.getDescendantFont();
PDFontDescriptor fontDescriptor = pdFont.getFontDescriptor();
-
+
fontDescriptor.getCOSObject().removeItem(cidSet);
}
}
} catch (IOException e) {
- e.printStackTrace();
+ throw e;
}
// do stuff with the font
}
@@ -568,7 +630,21 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
prepareDocument();
xmlProvider.generateXML(trans);
String filename = getFilenameForVersion(ZFVersion, xmlProvider.getProfile());
- PDFAttachGenericFile(doc, filename, "Alternative",
+
+ String relationship = "Alternative";
+ // ZUGFeRD 2.1.1 Technical Supplement | Part A | 2.2.2. Data Relationship
+ // See documentation ZUGFeRD211_EN/Documentation/ZUGFeRD-2.1.1 - Specification_TA_Part-A.pdf
+ // https://www.ferd-net.de/standards/zugferd-2.1.1/index.html
+ if (ZFVersion >= 2)
+ {
+ if (this.profile.getName().equalsIgnoreCase(Profiles.getByName("MINIMUM").getName()) ||
+ this.profile.getName().equalsIgnoreCase(Profiles.getByName("BASICWL").getName()))
+ {
+ relationship = "Data";
+ }
+ }
+
+ PDFAttachGenericFile(doc, filename, relationship,
"Invoice metadata conforming to ZUGFeRD standard (http://www.ferd-net.de/front_content.php?idcat=231&lang=4)",
"text/xml", xmlProvider.getXML());
@@ -583,14 +659,16 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
* Reads the XMPMetadata from the PDDocument, if it exists.
* Otherwise creates XMPMetadata.
*/
- protected XMPMetadata getXmpMetadata() {
+ protected XMPMetadata getXmpMetadata()
+ throws IOException
+ {
PDMetadata meta = doc.getDocumentCatalog().getMetadata();
- if (meta != null) {
+ if ((meta != null) && (meta.getLength() > 0)) {
try {
DomXmpParser xmpParser = new DomXmpParser();
return xmpParser.parse(meta.toByteArray());
- } catch (XmpParsingException | IOException e) {
- // TODO use logging or handle the error somehow
+ } catch (XmpParsingException e) {
+ throw new IOException(e);
}
}
return XMPMetadata.createXMPMetadata();
@@ -724,7 +802,9 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
/**
* Adds an OutputIntent and the sRGB color profile if no OutputIntent exist
*/
- protected void addSRGBOutputIntend() {
+ protected void addSRGBOutputIntend()
+ throws IOException
+ {
if (!doc.getDocumentCatalog().getOutputIntents().isEmpty()) {
return;
}
@@ -740,7 +820,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
doc.getDocumentCatalog().addOutputIntent(intent);
}
} catch (IOException e) {
- // TODO use logging or handle the error somehow
+ throw e;
}
}
diff --git a/validator/pom.xml b/validator/pom.xml
index 09333153..401d0440 100644
--- a/validator/pom.xml
+++ b/validator/pom.xml
@@ -74,6 +74,11 @@
ph-schematron
5.6.0
+
+ com.helger
+ ph-jaxb
+ 9.5.4
+
org.mustangproject
library
@@ -83,6 +88,7 @@
org.xmlunit
xmlunit-core
2.6.3
+ test
org.xmlunit