From 8b0d6623ed2322b62209f2dc24f643683003bf69 Mon Sep 17 00:00:00 2001 From: Ivan Vaklinov Date: Sat, 18 Dec 2021 00:09:47 +0200 Subject: [PATCH] Multiple changes to do with XRechnung and other improvements: 1. Getting a profile by name disregarding teh ZUGFeRD version 2. Allowing the XRechnung version to be explicitly set in the PDF meta-data (must match the version in the attached XML). 3. Change/fix for data relationship in case of MINIMUM and BASICWL profile. See documentation ZUGFeRD211_EN/ Documentation/ZUGFeRD-2.1.1 - Specification_TA_Part-A.pdf 4. Error handling improvements (exceptions not suppressed) --- .../org/mustangproject/ZUGFeRD/Profiles.java | 27 +++++ .../ZUGFeRD/XMPSchemaZugferd.java | 13 +- .../ZUGFeRD/ZUGFeRDExporterFromA3.java | 112 +++++++++++++++--- 3 files changed, 132 insertions(+), 20 deletions(-) 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; } }