diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA1.java b/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA1.java new file mode 100644 index 00000000..fc25c64f --- /dev/null +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA1.java @@ -0,0 +1,124 @@ +/** ********************************************************************** + * + * Copyright 2018 Jochen Staerk + * + * Use is subject to license terms. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + *********************************************************************** */ +package org.mustangproject.ZUGFeRD; + + +import org.apache.pdfbox.preflight.PreflightDocument; +import org.apache.pdfbox.preflight.exception.ValidationException; +import org.apache.pdfbox.preflight.parser.PreflightParser; + +import javax.activation.DataSource; +import java.io.IOException; +import java.io.InputStream; + +public class OXExporterFromA1 extends OXExporterFromA3 implements IZUGFeRDExporter { + protected boolean ignorePDFAErrors = false; + + public OXExporterFromA1 ignorePDFAErrors() { + this.ignorePDFAErrors = true; + return this; + } + + private static boolean isValidA1(DataSource dataSource) throws IOException { + return getPDFAParserValidationResult(new PreflightParser(dataSource)); + } + + private static boolean getPDFAParserValidationResult(PreflightParser parser) throws IOException { + /* + * Parse the PDF file with PreflightParser that inherits from the + * NonSequentialParser. Some additional controls are present to check a set of + * PDF/A requirements. (Stream length consistency, EOL after some Keyword...) + */ + parser.parse();// might add a Format.PDF_A1A as parameter and iterate through A1 and A3 + + try (PreflightDocument document = parser.getPreflightDocument()) { + /* + * Once the syntax validation is done, the parser can provide a + * PreflightDocument (that inherits from PDDocument) This document process the + * end of PDF/A validation. + */ + + document.validate(); + + // Get validation result + return document.getResult().isValid(); + } catch (ValidationException e) { + /* + * the parse method can throw a SyntaxValidationException if the PDF file can't + * be parsed. In this case, the exception contains an instance of + * ValidationResult + */ + return false; + } + } + + + public OXExporterFromA1 setProfile(Profile p) { + return (OXExporterFromA1)super.setProfile(p); + } + public OXExporterFromA1 setProfile(String profileName) { + return (OXExporterFromA1)super.setProfile(profileName); + } + + public boolean ensurePDFIsValid(final DataSource dataSource) throws IOException { + if (!ignorePDFAErrors && !isValidA1(dataSource)) { + throw new IOException("File is not a valid PDF/A input file"); + } + return true; + } + + public OXExporterFromA1() { + setZUGFeRDVersion(ZUGFeRDExporterFromA3.DefaultZUGFeRDVersion); + + } + + public OXExporterFromA1 load(String pdfFilename) throws IOException { + return (OXExporterFromA1) super.load(pdfFilename); + } + public OXExporterFromA1 load(byte[] pdfBinary) throws IOException { + return (OXExporterFromA1) super.load(pdfBinary); + } + public OXExporterFromA1 load(InputStream pdfSource) throws IOException{ + return (OXExporterFromA1) super.load(pdfSource); + } + public OXExporterFromA1 setCreator(String creator) { + return (OXExporterFromA1) super.setCreator(creator); + } + public OXExporterFromA1 setConformanceLevel(PDFAConformanceLevel newLevel) { + return (OXExporterFromA1) super.setConformanceLevel(newLevel); + } + public OXExporterFromA1 setProducer(String producer){ + return (OXExporterFromA1) super.setProducer(producer); + } + public OXExporterFromA1 setZUGFeRDVersion(int version){ + return (OXExporterFromA1) super.setZUGFeRDVersion(version); + } + public OXExporterFromA1 setXML(byte[] zugferdData) throws IOException{ + return (OXExporterFromA1) super.setXML(zugferdData); + } + + public OXExporterFromA1 disableAutoClose(boolean disableAutoClose){ + return (OXExporterFromA1) super.disableAutoClose(disableAutoClose); + } + public OXExporterFromA1 convertOnly() { + setAttachZUGFeRDHeaders(false); + return this; + } + +} diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA3.java b/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA3.java new file mode 100644 index 00000000..9d7073e4 --- /dev/null +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA3.java @@ -0,0 +1,659 @@ +/** + * ********************************************************************* + *

+ * Copyright 2018 Jochen Staerk + *

+ * Use is subject to license terms. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *

+ * See the License for the specific language governing permissions and + * limitations under the License. + *

+ * ********************************************************************** + */ +package org.mustangproject.ZUGFeRD; + +import org.apache.pdfbox.cos.*; +import org.apache.pdfbox.io.IOUtils; +import org.apache.pdfbox.pdmodel.*; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDMarkInfo; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; +import org.apache.pdfbox.pdmodel.font.PDCIDFontType2; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDFontDescriptor; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent; +import org.apache.pdfbox.preflight.utils.ByteArrayDataSource; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.schema.AdobePDFSchema; +import org.apache.xmpbox.schema.DublinCoreSchema; +import org.apache.xmpbox.schema.PDFAIdentificationSchema; +import org.apache.xmpbox.schema.XMPBasicSchema; +import org.apache.xmpbox.type.ArrayProperty; +import org.apache.xmpbox.type.BadFieldValueException; +import org.apache.xmpbox.xml.DomXmpParser; +import org.apache.xmpbox.xml.XmpParsingException; +import org.apache.xmpbox.xml.XmpSerializer; +import org.mustangproject.FileAttachment; + +import javax.activation.DataSource; +import javax.activation.FileDataSource; +import javax.xml.transform.TransformerException; +import java.io.*; +import java.util.*; + +public class OXExporterFromA3 extends ZUGFeRDExporterFromA3 { + + protected PDFAConformanceLevel conformanceLevel = PDFAConformanceLevel.UNICODE; + protected ArrayList fileAttachments = new ArrayList(); + + /** + * This flag controls whether or not the metadata is overwritten, or kind of merged. + * The merging probably needs to be overhauled, but for my purpose it was good enough. + */ + protected boolean overwrite = true; + + private boolean disableAutoClose; + private boolean fileAttached = false; + private Profile profile = null; + private boolean documentPrepared = false; + + /** + * Data (XML invoice) to be added to the ZUGFeRD PDF. It may be externally set, + * in which case passing a IZUGFeRDExportableTransaction is not necessary. By + * default it is null meaning the caller needs to pass a + * IZUGFeRDExportableTransaction for the XML to be populated. + */ + protected PDMetadata metadata = null; + /** + * Producer attribute for PDF + */ + protected String producer = "mustangproject"; + /** + * Author/Creator attribute for PDF + */ + protected String creator = "mustangproject"; + /** + * CreatorTool + */ + protected String creatorTool = "mustangproject"; + + /** + * @deprecated author is never set yet + */ + @Deprecated + protected String author; + /** + * @deprecated title is never set yet + */ + @Deprecated + protected String title; + /** + * @deprecated subject is never set yet + */ + @Deprecated + protected String subject; + + + private HashMap additionalXMLs = new HashMap(); + + + private boolean attachZUGFeRDHeaders = true; + + /** + * 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) + * + * @param pdfFilename filename of an PDF/A1 compliant document + */ + public OXExporterFromA3 load(String pdfFilename) throws IOException { + + ensurePDFIsValid(new FileDataSource(pdfFilename)); + try (FileInputStream pdf = new FileInputStream(pdfFilename)) { + return load(readAllBytes(pdf)); + } + } + + public IXMLProvider getProvider() { + return xmlProvider; + } + + public OXExporterFromA3 setProfile(Profile p) { + this.profile = p; + if (xmlProvider != null) { + xmlProvider.setProfile(p); + } + return this; + } + + public OXExporterFromA3 setProfile(String profilename) { + this.profile = Profiles.getByName(profilename); + + if (xmlProvider != null) { + xmlProvider.setProfile(this.profile); + } + return this; + } + + public OXExporterFromA3 addAdditionalFile(String name, byte[] content) { + fileAttachments.add(new FileAttachment(name, "text/xml", "Supplement", content).setDescription("ZUGFeRD extension/additional data")); + return this; + } + + + + /** + * 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) + * + * @param pdfBinary binary of a PDF/A1 compliant document + */ + public OXExporterFromA3 load(byte[] pdfBinary) throws IOException { + ensurePDFIsValid(new ByteArrayDataSource(new ByteArrayInputStream(pdfBinary))); + doc = PDDocument.load(pdfBinary); + return this; + } + + public OXExporterFromA3() { + super(); + } + + public void attachFile(FileAttachment file) { + fileAttachments.add(file); + } + + public void attachFile(String filename, byte[] data, String mimetype, String relation) { + FileAttachment fa = new FileAttachment(filename, mimetype, relation, data); + fileAttachments.add(fa); + } + + /*** + * Perform the final export to a now ZUGFeRD-enriched PDF file + * @param ZUGFeRDfilename the pdf file name + * @throws IOException if anything is wrong in the target location + */ + public void export(String ZUGFeRDfilename) throws IOException { + if (!documentPrepared) { + prepareDocument(); + } + if ((!fileAttached) && (attachZUGFeRDHeaders)) { + throw new IOException( + "File must be attached (usually with setTransaction) before perfoming this operation"); + } + doc.save(ZUGFeRDfilename); + if (!disableAutoClose) { + close(); + } + } + + @Override + public void close() throws IOException { + if (doc != null) { + doc.close(); + } + } + + + /*** + * Perform the final export to a now ZUGFeRD-enriched PDF file as OutputStream + * @param output the OutputStream + * @throws IOException if anything is wrong in the OutputStream + */ + public void export(OutputStream output) throws IOException { + if (!documentPrepared) { + prepareDocument(); + } + if ((!fileAttached) && (attachZUGFeRDHeaders)) { + throw new IOException( + "File must be attached (usually with setTransaction) before perfoming this operation"); + } + doc.save(output); + if (!disableAutoClose) { + close(); + } + } + + /** + * Embeds an external file (generic - any type allowed) in the PDF. + * + * @param doc PDDocument to attach the file to. + * @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 IOException if anything is wrong with filename + */ + public void PDFAttachGenericFile(PDDocument doc, String filename, String relationship, String description, + String subType, byte[] data) throws IOException { + fileAttached = true; + + PDComplexFileSpecification fs = new PDComplexFileSpecification(); + fs.setFile(filename); + + COSDictionary dict = fs.getCOSObject(); + dict.setName("AFRelationship", relationship); + dict.setString("UF", filename); + dict.setString("Desc", description); + + ByteArrayInputStream fakeFile = new ByteArrayInputStream(data); + PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile); +// ef.addCompression(); + ef.setSubtype(subType); + ef.setSize(data.length); + ef.setCreationDate(new GregorianCalendar()); + + ef.setModDate(GregorianCalendar.getInstance()); + + fs.setEmbeddedFile(ef); + + // In addition make sure the embedded file is set under /UF + dict = fs.getCOSObject(); + COSDictionary efDict = (COSDictionary) dict.getDictionaryObject(COSName.EF); + COSBase lowerLevelFile = efDict.getItem(COSName.F); + efDict.setItem(COSName.UF, lowerLevelFile); + + // now add the entry to the embedded file tree and set in the document. + PDDocumentNameDictionary names = new PDDocumentNameDictionary(doc.getDocumentCatalog()); + PDEmbeddedFilesNameTreeNode efTree = names.getEmbeddedFiles(); + if (efTree == null) { + efTree = new PDEmbeddedFilesNameTreeNode(); + } + + Map namesMap = new HashMap<>(); + + Map oldNamesMap = efTree.getNames(); + if (oldNamesMap != null) { + for (String key : oldNamesMap.keySet()) { + namesMap.put(key, oldNamesMap.get(key)); + } + } + namesMap.put(filename, fs); + efTree.setNames(namesMap); + + names.setEmbeddedFiles(efTree); + doc.getDocumentCatalog().setNames(names); + + // AF entry (Array) in catalog with the FileSpec + COSBase AFEntry = (COSBase) doc.getDocumentCatalog().getCOSObject().getItem("AF"); + if ((AFEntry == null)) { + COSArray cosArray = new COSArray(); + cosArray.add(fs); + doc.getDocumentCatalog().getCOSObject().setItem("AF", cosArray); + } else if (AFEntry instanceof COSArray) { + COSArray cosArray = (COSArray) AFEntry; + cosArray.add(fs); + doc.getDocumentCatalog().getCOSObject().setItem("AF", cosArray); + } else if ((AFEntry instanceof COSObject) && + ((COSObject) AFEntry).getObject() instanceof COSArray) { + COSArray cosArray = (COSArray) ((COSObject) AFEntry).getObject(); + cosArray.add(fs); + } else { + throw new IOException("Unexpected object type for PDFDocument/Catalog/COSDictionary/Item(AF)"); + } + } + + /** + * Sets the ZUGFeRD XML data to be attached as a single byte array. This is + * useful for use-cases where the XML has already been produced by some external + * API or component. + * + * @param zugferdData XML data to be set as a byte array (XML file in raw form). + * @throws IOException (should not happen) + */ + public OXExporterFromA3 setXML(byte[] zugferdData) throws IOException { + CustomXMLProvider cus = new CustomXMLProvider(); + cus.setXML(zugferdData); + this.setXMLProvider(cus); + prepare(); + return this; + } + + + /** + * 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) + * + * @param pdfSource source to read a PDF/A1 compliant document from + */ + public OXExporterFromA3 load(InputStream pdfSource) throws IOException { + return load(readAllBytes(pdfSource)); + } + + public boolean ensurePDFIsValid(final DataSource dataSource) throws IOException { + return true; + } + + private static byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + IOUtils.copy(in, buffer); + return buffer.toByteArray(); + } + + + /** + * All files are PDF/A-3, setConformance refers to the level conformance. + *

+ * PDF/A-3 has three coformance levels, called "A", "U" and "B". + *

+ * PDF/A-3-B where B means only visually preservable, U -standard for Mustang- + * means visually and unicode preservable and A means full compliance, i.e. + * visually, unicode and structurally preservable and tagged PDF, i.e. useful + * metainformation for blind people. + *

+ * Feel free to pass "A" as new level if you know what you are doing :-) + */ + public OXExporterFromA3 setConformanceLevel(PDFAConformanceLevel newLevel) { + conformanceLevel = newLevel; + return this; + } + + + public OXExporterFromA3 setCreator(String creator) { + this.creator = creator; + return this; + } + + public OXExporterFromA3 setCreatorTool(String creatorTool) { + this.creatorTool = creatorTool; + return this; + } + + public OXExporterFromA3 setProducer(String producer) { + this.producer = producer; + return this; + } + + protected OXExporterFromA3 setAttachZUGFeRDHeaders(boolean attachHeaders) { + this.attachZUGFeRDHeaders = attachHeaders; + return this; + } + + + /** + * This will add both the RDF-indication which embedded file is Zugferd and the + * neccessary PDF/A schema extension description to be able to add this + * information to RDF + * + * @param metadata the PDFbox XMPMetadata object + */ + protected void addXMP(XMPMetadata metadata) { + + if (attachZUGFeRDHeaders) { + XMPSchemaZugferd zf = new XMPSchemaZugferd(metadata, 1, true, xmlProvider.getProfile(), + "urn:factur-x:pdfa:CrossIndustryDocument:1p0#", "fx", + "order-x.xml"); + + metadata.addSchema(zf); + } + + XMPSchemaPDFAExtensions pdfaex = new XMPSchemaPDFAExtensions(this, metadata, 1, attachZUGFeRDHeaders); + pdfaex.setZUGFeRDVersion(1); + metadata.addSchema(pdfaex); + } + + + /** + * Embeds the Zugferd XML structure in a file named ZUGFeRD-invoice.xml. + * + * @param trans a IZUGFeRDExportableTransaction that provides the data-model to + * populate the XML. This parameter may be null, if so the XML data + * should hav ebeen set via + * setZUGFeRDXMLData(byte[] zugferdData) + * @throws IOException if anything is wrong with already loaded PDF + */ + public IExporter setTransaction(IExportableTransaction trans) throws IOException { + this.trans = trans; + return prepare(); + } + + public IExporter prepare() throws IOException { + prepareDocument(); + xmlProvider.generateXML(trans); + String filename = "order-x.xml"; + PDFAttachGenericFile(doc, filename, "Alternative", + "Invoice metadata conforming to ZUGFeRD standard (http://www.ferd-net.de/front_content.php?idcat=231&lang=4)", + "text/xml", xmlProvider.getXML()); + + for (FileAttachment attachment : fileAttachments) { + PDFAttachGenericFile(doc, attachment.getFilename(), attachment.getRelation(), attachment.getDescription(), attachment.getMimetype(), attachment.getData()); + } + + return this; + } + + /** + * Reads the XMPMetadata from the PDDocument, if it exists. + * Otherwise creates XMPMetadata. + */ + protected XMPMetadata getXmpMetadata() { + PDMetadata meta = doc.getDocumentCatalog().getMetadata(); + if (meta != null) { + try { + DomXmpParser xmpParser = new DomXmpParser(); + return xmpParser.parse(meta.toByteArray()); + } catch (XmpParsingException | IOException e) { + // TODO use logging or handle the error somehow + } + } + return XMPMetadata.createXMPMetadata(); + } + + protected byte[] serializeXmpMetadata(XMPMetadata xmpMetadata) throws TransformerException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmpMetadata, buffer, true); // see https://github.com/ZUGFeRD/mustangproject/issues/44 + return buffer.toByteArray(); + } + + /** + * Sets the producer if the overwrite flag is set or the producer is not already set. + * Sets the PDFVersion to 1.4 if the field is empty. + */ + protected void writeAdobePDFSchema(XMPMetadata xmp) { + AdobePDFSchema pdf = getAdobePDFSchema(xmp); + if (overwrite || isEmpty(pdf.getProducer())) + pdf.setProducer(producer); + } + + /** + * Returns the AdobePDFSchema from the XMPMetadata if it exists. + * If the overwrite flag is set or no AdobePDFSchema exists in the XMPMetadata, it is created, added and returned. + */ + protected AdobePDFSchema getAdobePDFSchema(XMPMetadata xmp) { + AdobePDFSchema pdf = xmp.getAdobePDFSchema(); + if (pdf != null) + if (overwrite) + xmp.removeSchema(pdf); + else + return pdf; + return xmp.createAndAddAdobePDFSchema(); + } + + protected void writePDFAIdentificationSchema(XMPMetadata xmp) { + PDFAIdentificationSchema pdfaid = getPDFAIdentificationSchema(xmp); + if (overwrite || isEmpty(pdfaid.getConformance())) { + try { + pdfaid.setConformance(conformanceLevel.getLetter()); + } catch (BadFieldValueException ex) { + // This should be impossible, because it would occur only if an illegal + // conformance level is supplied, + // however the enum enforces that the conformance level is valid. + throw new Error(ex); + } + } + pdfaid.setPart(3); + } + + protected PDFAIdentificationSchema getPDFAIdentificationSchema(XMPMetadata xmp) { + PDFAIdentificationSchema pdfaid = xmp.getPDFIdentificationSchema(); + if (pdfaid != null) + if (overwrite) + xmp.removeSchema(pdfaid); + else + return pdfaid; + return xmp.createAndAddPFAIdentificationSchema(); + } + + protected void writeDublinCoreSchema(XMPMetadata xmp) { + DublinCoreSchema dc = getDublinCoreSchema(xmp); + if (dc.getFormat() == null) + dc.setFormat("application/pdf"); + if ((overwrite || dc.getCreators() == null || dc.getCreators().isEmpty()) && creator != null) + dc.addCreator(creator); + if ((overwrite || dc.getDates() == null || dc.getDates().isEmpty()) && creator != null) + dc.addDate(Calendar.getInstance()); + + ArrayProperty titleProperty = dc.getTitleProperty(); + if (titleProperty != null) { + if (overwrite && !isEmpty(title)) { + dc.removeProperty(titleProperty); + dc.setTitle(title); + } else if (titleProperty.getElementsAsString().stream().anyMatch("Untitled"::equalsIgnoreCase)) { + // remove unfitting ghostscript default + dc.removeProperty(titleProperty); + } + } else if (!isEmpty(title)) { + dc.setTitle(title); + } + } + + protected DublinCoreSchema getDublinCoreSchema(XMPMetadata xmp) { + DublinCoreSchema dc = xmp.getDublinCoreSchema(); + if (dc != null) + if (overwrite) + xmp.removeSchema(dc); + else + return dc; + return xmp.createAndAddDublinCoreSchema(); + } + + protected void writeXMLBasicSchema(XMPMetadata xmp) { + XMPBasicSchema xsb = getXmpBasicSchema(xmp); + if (overwrite || isEmpty(xsb.getCreatorTool()) || "UnknownApplication".equals(xsb.getCreatorTool())) + xsb.setCreatorTool(creatorTool); + if (overwrite || xsb.getCreateDate() == null) + xsb.setCreateDate(GregorianCalendar.getInstance()); + } + + protected XMPBasicSchema getXmpBasicSchema(XMPMetadata xmp) { + XMPBasicSchema xsb = xmp.getXMPBasicSchema(); + if (xsb != null) + if (overwrite) + xmp.removeSchema(xsb); + else + return xsb; + return xmp.createAndAddXMPBasicSchema(); + } + + protected void writeDocumentInformation() { + String fullProducer = producer + " (via mustangproject.org " + Version.VERSION + ")"; + PDDocumentInformation info = doc.getDocumentInformation(); + if (overwrite || info.getCreationDate() == null) + info.setCreationDate(Calendar.getInstance()); + if (overwrite || info.getModificationDate() == null) + info.setModificationDate(Calendar.getInstance()); + if (overwrite || (isEmpty(info.getAuthor()) && !isEmpty(author))) + info.setAuthor(author); + if (overwrite || (isEmpty(info.getProducer()) && !isEmpty(fullProducer))) + info.setProducer(fullProducer); + if (overwrite || (isEmpty(info.getCreator()) && !isEmpty(creator))) + info.setCreator(creator); + if (overwrite || (isEmpty(info.getTitle()) && !isEmpty(title))) + info.setTitle(title); + if (overwrite || (isEmpty(info.getSubject()) && !isEmpty(subject))) + info.setSubject(subject); + } + + /** + * Adds an OutputIntent and the sRGB color profile if no OutputIntent exist + */ + protected void addSRGBOutputIntend() { + if (!doc.getDocumentCatalog().getOutputIntents().isEmpty()) { + return; + } + + try { + InputStream colorProfile = Thread.currentThread().getContextClassLoader().getResourceAsStream("sRGB.icc"); + if (colorProfile != null) { + PDOutputIntent intent = new PDOutputIntent(doc, colorProfile); + intent.setInfo("sRGB IEC61966-2.1"); + intent.setOutputCondition("sRGB IEC61966-2.1"); + intent.setOutputConditionIdentifier("sRGB IEC61966-2.1"); + intent.setRegistryName("http://www.color.org"); + doc.getDocumentCatalog().addOutputIntent(intent); + } + } catch (IOException e) { + // TODO use logging or handle the error somehow + } + } + + /** + * Adds a MarkInfo element to the PDF if it doesn't already exist and sets it as marked. + */ + protected void setMarked() { + PDDocumentCatalog catalog = doc.getDocumentCatalog(); + if (catalog.getMarkInfo() == null) { + catalog.setMarkInfo(new PDMarkInfo(doc.getPages().getCOSObject())); + } + catalog.getMarkInfo().setMarked(true); + } + + /** + * Adds a StructureTreeRoot element to the PDF if it doesn't already exist. + */ + protected void addStructureTreeRoot() { + if (doc.getDocumentCatalog().getStructureTreeRoot() == null) { + doc.getDocumentCatalog().setStructureTreeRoot(new PDStructureTreeRoot()); + } + } + + + /** + * @return if pdf file will be automatically closed after adding ZF + */ + public boolean isAutoCloseDisabled() { + return disableAutoClose; + } + + /** + * @param disableAutoClose prevent PDF file from being closed after adding ZF + */ + public OXExporterFromA3 disableAutoClose(boolean disableAutoClose) { + this.disableAutoClose = disableAutoClose; + return this; + } + + protected void setXMLProvider(IXMLProvider p) { + this.xmlProvider = p; + if (profile != null) { + xmlProvider.setProfile(profile); + } + } + + @Override + public OXExporterFromA3 setZUGFeRDVersion(int version) { + OXPullProvider z2p = new OXPullProvider(); + setXMLProvider(z2p); + return this; + } + + /** + * Utility method inspired by apache commons-lang3 StringUtils. + * + * @param string the string to test + * @return true if the string is null or empty + */ + private boolean isEmpty(String string) { + return string == null || string.isEmpty(); + } +} diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/OXPullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/OXPullProvider.java new file mode 100644 index 00000000..efb6f26b --- /dev/null +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/OXPullProvider.java @@ -0,0 +1,585 @@ +/** + * ********************************************************************* + *

+ * Copyright 2018 Jochen Staerk + *

+ * Use is subject to license terms. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *

+ * See the License for the specific language governing permissions and + * limitations under the License. + *

+ * ********************************************************************** + */ +package org.mustangproject.ZUGFeRD; + +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.XMLWriter; +import org.mustangproject.FileAttachment; +import org.mustangproject.XMLTools; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Base64; +import java.util.Date; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.mustangproject.ZUGFeRD.ZUGFeRDDateFormat.DATE; +import static org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants.CORRECTEDINVOICE; +import static org.mustangproject.ZUGFeRD.model.TaxCategoryCodeTypeConstants.CATEGORY_CODES_WITH_EXEMPTION_REASON; + +public class OXPullProvider extends ZUGFeRD2PullProvider implements IXMLProvider { + + protected IExportableTransaction trans; + protected TransactionCalculator calc; + private String paymentTermsDescription; + protected Profile profile = Profiles.getByName("EN16931"); + + + + + + @Override + public void generateXML(IExportableTransaction trans) { + this.trans = trans; + this.calc = new TransactionCalculator(trans); + + boolean hasDueDate = false; + final SimpleDateFormat germanDateFormat = new SimpleDateFormat("dd.MM.yyyy"); + + String exemptionReason = ""; + + if (trans.getPaymentTermDescription() != null) { + paymentTermsDescription = trans.getPaymentTermDescription(); + } + + if ((paymentTermsDescription == null) && (trans.getDocumentCode() != CORRECTEDINVOICE)/* && (trans.getDocumentCode() != DocumentCodeTypeConstants.CREDITNOTE)*/) { + paymentTermsDescription = "Zahlbar ohne Abzug bis " + germanDateFormat.format(trans.getDueDate()); + + } + + String senderReg = ""; + if (trans.getOwnOrganisationFullPlaintextInfo() != null) { + senderReg = "" + "\n" + " \n" + + XMLTools.encodeXML(trans.getOwnOrganisationFullPlaintextInfo()) + " \n" + + "REG\n" + "\n"; + + } + + String rebateAgreement = ""; + if (trans.rebateAgreementExists()) { + rebateAgreement = "\n" + " " + + "Es bestehen Rabatt- und Bonusvereinbarungen.\n" + + "AAK\n" + "\n"; + } + + String subjectNote = ""; + if (trans.getSubjectNote() != null) { + subjectNote = "\n" + " " + + XMLTools.encodeXML(trans.getSubjectNote()) + "\n" + + "\n"; + } + + String typecode = "380"; + if (trans.getDocumentCode() != null) { + typecode = trans.getDocumentCode(); + } + String notes = ""; + if (trans.getNotes() != null) { + for (final String currentNote : trans.getNotes()) { + notes = notes + "" + XMLTools.encodeXML(currentNote) + ""; + + } + } + String xml = "\n" + + +"\n" + // + " + // xsi:schemaLocation=\"urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100 + // ../Schema/ZUGFeRD1p0.xsd\"" + + " xmlns:ram=\"urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100\"" + + " xmlns:udt=\"urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100\"" + + " xmlns:qdt=\"urn:un:unece:uncefact:data:standard:QualifiedDataType:100\">\n" + + " \n" + // + " + // "+testBooleanStr+"\n" + // + + " \n" + + " " + getProfile().getID() + "\n" + + " \n" + + " \n" + + " \n" + + " " + XMLTools.encodeXML(trans.getNumber()) + "\n" + // + " RECHNUNG\n" + // + " 380\n" + + " " + typecode + "\n" + + " " + + DATE.udtFormat(trans.getIssueDate()) + "\n" // date + + notes + + subjectNote + + rebateAgreement + + senderReg + + + " \n" + + " \n"; + int lineID = 0; + for (final IZUGFeRDExportableItem currentItem : trans.getZFItems()) { + lineID++; + if (currentItem.getProduct().getTaxExemptionReason() != null) { + exemptionReason = "" + XMLTools.encodeXML(currentItem.getProduct().getTaxExemptionReason()) + ""; + } + notes = ""; + if (currentItem.getNotes() != null) { + for (final String currentNote : currentItem.getNotes()) { + notes = notes + "" + XMLTools.encodeXML(currentNote) + ""; + + } + } + final LineCalculator lc = new LineCalculator(currentItem); + xml = xml + " \n" + + " \n" + + " " + lineID + "\n" + + notes + + " \n" + + + " \n"; + // + " 4012345001235\n" + if (currentItem.getProduct().getSellerAssignedID() != null) { + xml = xml + " " + + XMLTools.encodeXML(currentItem.getProduct().getSellerAssignedID()) + "\n"; + } + if (currentItem.getProduct().getBuyerAssignedID() != null) { + xml = xml + " " + + XMLTools.encodeXML(currentItem.getProduct().getBuyerAssignedID()) + "\n"; + } + String allowanceChargeStr = ""; + if (currentItem.getItemAllowances() != null && currentItem.getItemAllowances().length > 0) { + for (final IZUGFeRDAllowanceCharge allowance : currentItem.getItemAllowances()) { + allowanceChargeStr += getAllowanceChargeStr(allowance, currentItem); + } + } + if (currentItem.getItemCharges() != null && currentItem.getItemCharges().length > 0) { + for (final IZUGFeRDAllowanceCharge charge : currentItem.getItemCharges()) { + allowanceChargeStr += getAllowanceChargeStr(charge, currentItem); + + } + } + + + xml = xml + " " + XMLTools.encodeXML(currentItem.getProduct().getName()) + "\n" + + " " + XMLTools.encodeXML(currentItem.getProduct().getDescription()) + + "\n" + + " \n" + + + " \n"; + if (currentItem.getReferencedDocuments() != null) { + for (IReferencedDocument currentReferencedDocument : currentItem.getReferencedDocuments()) { + xml = xml + "\n" + + "" + XMLTools.encodeXML(currentReferencedDocument.getIssuerAssignedID()) + "\n" + + "" + XMLTools.encodeXML(currentReferencedDocument.getTypeCode()) + "\n" + + "" + XMLTools.encodeXML(currentReferencedDocument.getReferenceTypeCode()) + "\n" + + "\n"; + + + } + + } + if (currentItem.getBuyerOrderReferencedDocumentLineID() != null) { + xml = xml + " \n" + + " " + XMLTools.encodeXML(currentItem.getBuyerOrderReferencedDocumentLineID()) + "\n" + + " \n"; + + } + xml = xml + " \n" + + " " + priceFormat(lc.getPriceGross()) + + "\n" //currencyID=\"EUR\" + + "" + quantityFormat(currentItem.getBasisQuantity()) + "\n" + + allowanceChargeStr + // + " \n" + // + " false\n" + // + " 0.6667\n" + // + " Rabatt\n" + // + " \n" + + " \n" + + " \n" + + " " + priceFormat(lc.getPrice()) + + "\n" // currencyID=\"EUR\" + + " " + quantityFormat(currentItem.getBasisQuantity()) + "\n" + + " \n" + + " \n" + + + " \n" + + " " + + quantityFormat(currentItem.getQuantity()) + "\n" + + " \n" + + " \n" + + " \n" + + " VAT\n" + + exemptionReason + + " " + currentItem.getProduct().getTaxCategoryCode() + "\n" + + + " " + + vatFormat(currentItem.getProduct().getVATPercent()) + "\n" + + " \n"; + if ((currentItem.getDetailedDeliveryPeriodFrom() != null) || (currentItem.getDetailedDeliveryPeriodTo() != null)) { + xml = xml + ""; + if (currentItem.getDetailedDeliveryPeriodFrom() != null) { + xml = xml + "" + DATE.udtFormat(currentItem.getDetailedDeliveryPeriodFrom()) + ""; + } + if (currentItem.getDetailedDeliveryPeriodTo() != null) { + xml = xml + "" + DATE.udtFormat(currentItem.getDetailedDeliveryPeriodTo()) + ""; + } + xml = xml + ""; + + } + + xml = xml + " \n" + + " " + currencyFormat(lc.getItemTotalNetAmount()) + + "\n" // currencyID=\"EUR\" + + " \n"; + if (currentItem.getAdditionalReferencedDocumentID() != null) { + xml = xml + " " + currentItem.getAdditionalReferencedDocumentID() + "130\n"; + + } + xml = xml + " \n" + + " \n"; + + } + + xml = xml + " \n"; + if (trans.getReferenceNumber() != null) { + xml = xml + " " + XMLTools.encodeXML(trans.getReferenceNumber()) + "\n"; + + } + xml = xml + " \n" + + getTradePartyAsXML(trans.getSender(), true, false) + + " \n" + + " \n"; + // + " GE2020211\n" + // + " 4000001987658\n" + + xml += getTradePartyAsXML(trans.getRecipient(), false, false); + xml += " \n"; + + if (trans.getSellerOrderReferencedDocumentID() != null) { + xml = xml + " \n" + + " " + + XMLTools.encodeXML(trans.getSellerOrderReferencedDocumentID()) + "\n" + + " \n"; + } + if (trans.getBuyerOrderReferencedDocumentID() != null) { + xml = xml + " \n" + + " " + + XMLTools.encodeXML(trans.getBuyerOrderReferencedDocumentID()) + "\n" + + " \n"; + } + if (trans.getContractReferencedDocument() != null) { + xml = xml + " \n" + + " " + + XMLTools.encodeXML(trans.getContractReferencedDocument()) + "\n" + + " \n"; + } + + // Additional Documents of XRechnung (Rechnungsbegruendende Unterlagen - BG-24 XRechnung) + if (trans.getAdditionalReferencedDocuments() != null) { + for (final FileAttachment f : trans.getAdditionalReferencedDocuments()) { + final String documentContent = new String(Base64.getEncoder().encodeToString(f.getData())); + xml = xml + " \n" + + " " + f.getFilename() + "\n" + + " 916\n" + + " " + f.getDescription() + "\n" + + " " + documentContent + "\n" + + " \n"; + } + } + + if (trans.getSpecifiedProcuringProjectID() != null) { + xml = xml + " \n" + + " " + + XMLTools.encodeXML(trans.getSpecifiedProcuringProjectID()) + "\n"; + if(trans.getSpecifiedProcuringProjectName()!= null) { + xml += " " + XMLTools.encodeXML(trans.getSpecifiedProcuringProjectName()) + "\n"; + } + xml += " \n"; + } + xml = xml + " \n" + + " \n"; + if (this.trans.getDeliveryAddress() != null) { + xml += "" + + getTradePartyAsXML(this.trans.getDeliveryAddress(), false, true) + + ""; + } + + xml += " \n" + + " "; + + if (trans.getDeliveryDate() != null) { + xml += DATE.udtFormat(trans.getDeliveryDate()); + } else { + throw new IllegalStateException("No delivery date provided"); + } + xml += "\n"; + xml += " \n" + /* + * + " \n" + + * " 20130603\n" + + * " 2013-51112\n" + + * " \n" + */ + + " \n" + " \n" + + " " + XMLTools.encodeXML(trans.getNumber()) + "\n" + + " " + trans.getCurrency() + "\n"; + + if (trans.getTradeSettlementPayment() != null) { + for (final IZUGFeRDTradeSettlementPayment payment : trans.getTradeSettlementPayment()) { + if (payment != null) { + hasDueDate = true; + xml += payment.getSettlementXML(); + } + } + } + if (trans.getTradeSettlement() != null) { + for (final IZUGFeRDTradeSettlement payment : trans.getTradeSettlement()) { + if (payment != null) { + if (payment instanceof IZUGFeRDTradeSettlementPayment) { + hasDueDate = true; + } + xml += payment.getSettlementXML(); + } + } + } + if ((trans.getDocumentCode() == CORRECTEDINVOICE)/*||(trans.getDocumentCode() == DocumentCodeTypeConstants.CREDITNOTE)*/) { + hasDueDate = false; + } + + final Map VATPercentAmountMap = calc.getVATPercentAmountMap(); + for (final BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { + final VATAmount amount = VATPercentAmountMap.get(currentTaxPercent); + if (amount != null) { + final String amountCategoryCode = amount.getCategoryCode(); + final boolean displayExemptionReason = CATEGORY_CODES_WITH_EXEMPTION_REASON.contains(amountCategoryCode); + xml += " \n" + + " " + currencyFormat(amount.getCalculated()) + + "\n" //currencyID=\"EUR\" + + " VAT\n" + + (displayExemptionReason ? exemptionReason : "") + + " " + currencyFormat(amount.getBasis()) + "\n" // currencyID=\"EUR\" + + " " + amountCategoryCode + "\n" + + " " + + vatFormat(currentTaxPercent) + "\n" + " \n"; + } + } + if ((trans.getDetailedDeliveryPeriodFrom() != null) || (trans.getDetailedDeliveryPeriodTo() != null)) { + xml = xml + ""; + if (trans.getDetailedDeliveryPeriodFrom() != null) { + xml = xml + "" + DATE.udtFormat(trans.getDetailedDeliveryPeriodFrom()) + ""; + } + if (trans.getDetailedDeliveryPeriodTo() != null) { + xml = xml + "" + DATE.udtFormat(trans.getDetailedDeliveryPeriodTo()) + ""; + } + xml = xml + ""; + + + } + + if ((trans.getZFCharges() != null) && (trans.getZFCharges().length > 0)) { + + for (final BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { + if (calc.getChargesForPercent(currentTaxPercent).compareTo(BigDecimal.ZERO) != 0) { + + + xml = xml + " \n" + + " \n" + + " true\n" + + " \n" + + " " + currencyFormat(calc.getChargesForPercent(currentTaxPercent)) + "\n" + + " " + XMLTools.encodeXML(calc.getChargeReasonForPercent(currentTaxPercent)) + "\n" + + " \n" + + " VAT\n" + + " " + VATPercentAmountMap.get(currentTaxPercent).getCategoryCode() + "\n" + + " " + vatFormat(currentTaxPercent) + "\n" + + " \n" + + " \n"; + + } + } + + } + + if ((trans.getZFAllowances() != null) && (trans.getZFAllowances().length > 0)) { + for (final BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { + if (calc.getAllowancesForPercent(currentTaxPercent).compareTo(BigDecimal.ZERO) != 0) { + xml = xml + " \n" + + " \n" + + " false\n" + + " \n" + + " " + currencyFormat(calc.getAllowancesForPercent(currentTaxPercent)) + "\n" + + " " + XMLTools.encodeXML(calc.getAllowanceReasonForPercent(currentTaxPercent)) + "\n" + + " \n" + + " VAT\n" + + " " + VATPercentAmountMap.get(currentTaxPercent).getCategoryCode() + "\n" + + " " + vatFormat(currentTaxPercent) + "\n" + + " \n" + + " \n"; + } + } + } + + + if ((trans.getPaymentTerms() == null) && ((paymentTermsDescription != null) || (trans.getTradeSettlement() != null) || (hasDueDate))) { + xml = xml + "\n"; + + if (paymentTermsDescription != null) { + xml = xml + "" + paymentTermsDescription + "\n"; + } + + if (trans.getTradeSettlement() != null) { + for (final IZUGFeRDTradeSettlement payment : trans.getTradeSettlement()) { + if ((payment != null) && (payment instanceof IZUGFeRDTradeSettlementDebit)) { + xml += payment.getPaymentXML(); + } + } + } + + if (hasDueDate && (trans.getDueDate() != null)) { + xml = xml + " " // $NON-NLS-2$ + + DATE.udtFormat(trans.getDueDate()) + + "\n";// 20130704 + + } + xml = xml + " \n"; + } else { + xml = xml + buildPaymentTermsXml(); + } + + + final String allowanceTotalLine = "" + currencyFormat(calc.getAllowancesForPercent(null)) + ""; + + final String chargesTotalLine = "" + currencyFormat(calc.getChargesForPercent(null)) + ""; + + xml = xml + " \n" + + " " + currencyFormat(calc.getTotal()) + "\n" + + chargesTotalLine + + allowanceTotalLine + + " " + currencyFormat(calc.getTaxBasis()) + "\n" + // // + // currencyID=\"EUR\" + + " " + + currencyFormat(calc.getGrandTotal().subtract(calc.getTaxBasis())) + "\n" + + " " + currencyFormat(calc.getGrandTotal()) + "\n" + // // + // currencyID=\"EUR\" + + " " + currencyFormat(calc.getTotalPrepaid()) + "\n" + + " " + currencyFormat(calc.getGrandTotal().subtract(calc.getTotalPrepaid())) + "\n" + + " \n"; + if (trans.getInvoiceReferencedDocumentID() != null) { + xml = xml + " \n" + + " " + + XMLTools.encodeXML(trans.getInvoiceReferencedDocumentID()) + "\n"; + if(trans.getInvoiceReferencedIssueDate()!= null){ + xml += "" + + DATE.qdtFormat(trans.getInvoiceReferencedIssueDate()) + + "\n"; + } + xml += " \n"; + } + + xml = xml + " \n"; + // + " \n" + // + " \n" + // + " \n" + // + " Wir erlauben uns Ihnen folgende Positionen aus der Lieferung Nr. + // 2013-51112 in Rechnung zu stellen:\n" + // + " \n" + // + " \n" + // + " \n"; + + xml = xml + " \n" + + ""; + + final byte[] zugferdRaw; + try { + zugferdRaw = xml.getBytes("UTF-8"); + + zugferdData = XMLTools.removeBOM(zugferdRaw); + } catch (final UnsupportedEncodingException e) { + Logger.getLogger(OXPullProvider.class.getName()).log(Level.SEVERE, null, e); + } + } + + + @Override + public void setProfile(Profile p) { + profile = p; + } + + private String buildPaymentTermsXml() { + + final IZUGFeRDPaymentTerms paymentTerms = trans.getPaymentTerms(); + if (paymentTerms == null) { + return ""; + } + String paymentTermsXml = ""; + + final IZUGFeRDPaymentDiscountTerms discountTerms = paymentTerms.getDiscountTerms(); + final Date dueDate = paymentTerms.getDueDate(); + if (dueDate != null && discountTerms != null && discountTerms.getBaseDate() != null) { + throw new IllegalStateException( + "if paymentTerms.dueDate is specified, paymentTerms.discountTerms.baseDate has not to be specified"); + } + paymentTermsXml += "" + paymentTerms.getDescription() + ""; + if (dueDate != null) { + paymentTermsXml += ""; + paymentTermsXml += DATE.udtFormat(dueDate) ; + paymentTermsXml += ""; + } + + if (discountTerms != null) { + paymentTermsXml += ""; + final String currency = trans.getCurrency(); + final String basisAmount = currencyFormat(calc.getGrandTotal()); + paymentTermsXml += "" + basisAmount + ""; + paymentTermsXml += "" + discountTerms.getCalculationPercentage().toString() + + ""; + + if (discountTerms.getBaseDate() != null) { + final Date baseDate = discountTerms.getBaseDate(); + paymentTermsXml += ""; + paymentTermsXml += DATE.udtFormat(baseDate); + paymentTermsXml += ""; + + paymentTermsXml += "" + + discountTerms.getBasePeriodMeasure() + ""; + } + + paymentTermsXml += ""; + } + + paymentTermsXml += ""; + return paymentTermsXml; + } + +} diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD1PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD1PullProvider.java index a5bd0457..55b221a4 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD1PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD1PullProvider.java @@ -57,19 +57,19 @@ public class ZUGFeRD1PullProvider extends ZUGFeRD2PullProvider implements IXMLPr public void setTest() { } - private String vatFormat(BigDecimal value) { + protected String vatFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 2); } - private String currencyFormat(BigDecimal value) { + protected String currencyFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 2); } - private String priceFormat(BigDecimal value) { + protected String priceFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 4); } - private String quantityFormat(BigDecimal value) { + protected String quantityFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 4); } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index f4996ff0..6502b65c 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -59,19 +59,19 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { public void setTest() { } - private String vatFormat(BigDecimal value) { + protected String vatFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 2); } - private String currencyFormat(BigDecimal value) { + protected String currencyFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 2); } - private String priceFormat(BigDecimal value) { + protected String priceFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 4); } - private String quantityFormat(BigDecimal value) { + protected String quantityFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 4); } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java index 4113811d..5730d337 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java @@ -69,7 +69,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte private boolean disableAutoClose; private boolean fileAttached = false; private Profile profile = null; - private boolean documentPrepared = false; + protected boolean documentPrepared = false; /** * Data (XML invoice) to be added to the ZUGFeRD PDF. It may be externally set, @@ -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(); @@ -481,7 +481,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte metadata.addSchema(pdfaex); } - private void removeCidSet(PDDocumentCatalog catalog, PDDocument doc) { + protected void removeCidSet(PDDocumentCatalog catalog, PDDocument doc) { // https://github.com/ZUGFeRD/mustangproject/issues/249 diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/MustangReaderWriterEdgeTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/MustangReaderWriterEdgeTest.java index b68e7dfe..4ea4fdaf 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/MustangReaderWriterEdgeTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/MustangReaderWriterEdgeTest.java @@ -135,8 +135,7 @@ public class MustangReaderWriterEdgeTest extends MustangReaderTestCase { @Override public String getPaymentTermDescription() { - SimpleDateFormat germanDateFormat = new SimpleDateFormat("dd.MM.yyyy"); - return "Zahlbar ohne Abzug bis zum " + germanDateFormat.format(getDueDate()); + return "#SKONTO#TAGE=7#PROZENT=3.00#BASISBETRAG=571.04#\n"; } @Override diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/OXTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/OXTest.java new file mode 100644 index 00000000..f0cd421e --- /dev/null +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/OXTest.java @@ -0,0 +1,299 @@ + +/** ********************************************************************** + * + * Copyright 2019 Jochen Staerk + * + * Use is subject to license terms. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + *********************************************************************** */ +package org.mustangproject.ZUGFeRD; + +import junit.framework.Test; +import junit.framework.TestSuite; +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class OXTest extends MustangReaderTestCase implements IExportableTransaction { + final String TARGET_PDF = "./target/testout-OX.pdf"; + + protected class EdgeProduct implements IZUGFeRDExportableProduct { + private String description, name, unit; + + public EdgeProduct(String description, String name, String unit) { + super(); + this.description = description; + this.name = name; + this.unit = unit; + } + + @Override + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + @Override + public BigDecimal getVATPercent() { + return BigDecimal.ZERO; + } + + @Override + public boolean isIntraCommunitySupply() { + return true; // simulate intra community supply + } + } + + protected class DebitPayment implements IZUGFeRDTradeSettlementDebit { + + @Override + public String getIBAN() { + return "DE540815"; + } + + @Override + public String getMandate() { + return "DE99XX12345"; + } + + } + + @Override + public IZUGFeRDTradeSettlement[] getTradeSettlement() { + IZUGFeRDTradeSettlement[] payments = new DebitPayment[1]; + payments[0] = new DebitPayment(); + return payments; + } + + @Override + public Date getDeliveryDate() { + return new GregorianCalendar(2017, Calendar.MAY, 7).getTime(); + } + + @Override + public Date getDueDate() { + return new GregorianCalendar(2017, Calendar.MAY, 30).getTime(); + } + + @Override + public Date getIssueDate() { + return new GregorianCalendar(2017, Calendar.MAY, 9).getTime(); + } + + @Override + public String getNumber() { + return "RE-20170509/505"; + } + + @Override + public String getOwnCountry() { + return "DE"; + } + + @Override + public String getOwnLocation() { + return "Stadthausen"; + } + + @Override + public String getOwnOrganisationName() { + return "Bei Spiel GmbH"; + } + + @Override + public String getOwnStreet() { + return "Ecke 12"; + } + + @Override + public IZUGFeRDExportableTradeParty getSender() { + return new SenderTradeParty(); + } + + @Override + public String getOwnTaxID() { + return "22/815/0815/4"; + } + + @Override + public String getOwnVATID() { + return "DE136695976"; + } + + @Override + public String getOwnZIP() { + return "12345"; + } + + @Override + public IZUGFeRDExportableTradeParty getRecipient() { + return new RecipientTradeParty(); + } + + @Override + public IZUGFeRDExportableTradeParty getDeliveryAddress() { + return new RecipientTradeParty(); + } + + @Override + public String getOwnOrganisationFullPlaintextInfo() { + return null; + } + + @Override + public String getCurrency() { + return "USD"; + } + + @Override + public IZUGFeRDExportableItem[] getZFItems() { + Item[] allItems = new Item[3]; + EdgeProduct designProduct = new EdgeProduct("", "Künstlerische Gestaltung (Stunde): Einer Beispielrechnung", + "HUR"); + EdgeProduct balloonProduct = new EdgeProduct("", "Bestellerweiterung für E&F Umbau", "C62");// test for issue + // 103 + EdgeProduct airProduct = new EdgeProduct("", "Heiße Luft pro Liter", "LTR"); + + Item design=new Item(new BigDecimal("160"), new BigDecimal("1"), designProduct); + design.setAddReference("1825"); + allItems[0] = design; + allItems[1] = new Item(new BigDecimal("0.79"), new BigDecimal("400"), balloonProduct); + allItems[2] = new Item(new BigDecimal("0.10"), new BigDecimal("200"), airProduct); + return allItems; + } + + @Override + public String getPaymentTermDescription() { + SimpleDateFormat germanDateFormat = new SimpleDateFormat("dd.MM.yyyy"); + return "Zahlbar ohne Abzug bis zum " + germanDateFormat.format(getDueDate()); + } + + @Override + public IZUGFeRDAllowanceCharge[] getZFAllowances() { + return null; + } + + @Override + public IZUGFeRDAllowanceCharge[] getZFCharges() { + return null; + } + + @Override + public IZUGFeRDAllowanceCharge[] getZFLogisticsServiceCharges() { + return null; + } + + @Override + public String getReferenceNumber() { + return "AB321"; + } + + /** + * Create the test case + * + * @param testName name of the test case + */ + public OXTest(String testName) { + super(testName); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() { + return new TestSuite(OXTest.class); + } + + // //////// TESTS + // ////////////////////////////////////////////////////////////////////////////////////////// + + /** + * The exporter test bases on @{code + * ./src/test/MustangGnuaccountingBeispielRE-20170509_505PDFA3.pdf}, adds + * metadata, writes to @{code ./target/testout-*} and then imports to check the + * values. + */ + public void testOXExport() { + + // the writing part + + try (InputStream SOURCE_PDF = this.getClass() + .getResourceAsStream("/MustangGnuaccountingBeispielRE-20170509_505blanko.pdf"); + + OXExporterFromA1 oe = new OXExporterFromA1().setProducer("My Application") + .setCreator(System.getProperty("user.name")).setZUGFeRDVersion(1).ignorePDFAErrors() + .load(SOURCE_PDF)) { + oe.setTransaction(this); + String theXML = new String(oe.getProvider().getXML(), StandardCharsets.UTF_8); + assertTrue(theXML.contains("59")); + assertTrue(zi.getUTF8().contains("")); + assertTrue(zi.getUTF8().contains("DE540815")); + assertTrue(zi.getUTF8().contains("DE99XX12345")); + assertFalse(zi.getUTF8().contains("")); + assertFalse(zi.getUTF8().contains("EUR")); + assertTrue(zi.getUTF8().contains("USD"));//currency should be USD, test for #150 + + // Reading ZUGFeRD + assertEquals("496.00", zi.getAmount()); + assertEquals(zi.getHolder(), getOwnOrganisationName()); + assertEquals(zi.getForeignReference(), getNumber()); + try { + assertEquals(zi.getVersion(), 2); + } catch (Exception e) { + e.printStackTrace(); + } + + } + +}