diff --git a/mustang/pom.xml b/mustang/pom.xml index f88270c9..c44e810c 100644 --- a/mustang/pom.xml +++ b/mustang/pom.xml @@ -4,7 +4,7 @@ org.mustangproject.ZUGFeRD mustang - 1.1.0 + 1.1.1alpha diff --git a/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.java b/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.java index 588797d6..c8fa3d20 100644 --- a/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.java +++ b/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.java @@ -1,501 +1,522 @@ -package org.mustangproject.ZUGFeRD; -/** - * Mustangproject's ZUGFeRD implementation - * ZUGFeRD exporter - * Licensed under the APLv2 - * @date 2014-07-12 - * @version 1.1 - * @author jstaerk - * */ - - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.math.BigDecimal; -import java.text.SimpleDateFormat; -import java.util.Collections; -import java.util.GregorianCalendar; -import java.util.HashMap; - -import javax.xml.transform.TransformerException; - -import org.apache.jempbox.xmp.XMPMetadata; -import org.apache.jempbox.xmp.XMPSchemaBasic; -import org.apache.jempbox.xmp.XMPSchemaDublinCore; -import org.apache.jempbox.xmp.XMPSchemaPDF; -import org.apache.jempbox.xmp.pdfa.XMPSchemaPDFAId; -import org.apache.pdfbox.cos.COSArray; -import org.apache.pdfbox.cos.COSDictionary; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentCatalog; -import org.apache.pdfbox.pdmodel.PDDocumentInformation; -import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; -import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; -import org.apache.pdfbox.pdmodel.common.PDMetadata; -import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; -import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; - - -public class ZUGFeRDExporter { - /*** - * You will need Apache PDFBox. To use the ZUGFeRD exporter, - implement IZUGFeRDExportableTransaction in yourTransaction - (which will require you to implement Product, Item and Contact) - then call - doc = PDDocument.load(PDFfilename); - // automatically add Zugferd to all outgoing invoices - ZUGFeRDExporter ze = new ZUGFeRDExporter(); - ze.PDFmakeA3compliant(doc, "Your application name", - System.getProperty("user.name"), true); - ze.PDFattachZugferdFile(doc, yourTransaction); - - doc.save(PDFfilename); - - * @author jstaerk - * - */ - - - - - - //// MAIN CLASS - - private String conformanceLevel="U"; - private String versionStr="1.0"; - - private String currencyFormat(BigDecimal value, char decimalDelimiter) { - /* - * I needed 123,45, locale independent.I tried - * NumberFormat.getCurrencyInstance().format( 12345.6789 ); but that is - * locale specific.I also tried DecimalFormat df = new DecimalFormat( - * "0,00" ); df.setDecimalSeparatorAlwaysShown(true); - * df.setGroupingUsed(false); DecimalFormatSymbols symbols = new - * DecimalFormatSymbols(); symbols.setDecimalSeparator(','); - * symbols.setGroupingSeparator(' '); - * df.setDecimalFormatSymbols(symbols); - * - * but that would not switch off grouping. Although I liked very much - * the (incomplete) "BNF diagram" in - * http://docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html - * in the end I decided to calculate myself and take eur+sparator+cents - * - * This function will cut off, i.e. floor() subcent values Tests: - * System.err.println(utils.currencyFormat(new BigDecimal(0), - * ".")+"\n"+utils.currencyFormat(new BigDecimal("-1.10"), - * ",")+"\n"+utils.currencyFormat(new BigDecimal("-1.1"), - * ",")+"\n"+utils.currencyFormat(new BigDecimal("-1.01"), - * ",")+"\n"+utils.currencyFormat(new BigDecimal("20000123.3489"), - * ",")+"\n"+utils.currencyFormat(new BigDecimal("20000123.3419"), - * ",")+"\n"+utils.currencyFormat(new BigDecimal("12"), ",")); - * - * results 0.00 -1,10 -1,10 -1,01 20000123,34 20000123,34 12,00 - */ - value=value.setScale( 2, BigDecimal.ROUND_HALF_UP ); // first, round so that e.g. 1.189999999999999946709294817992486059665679931640625 becomes 1.19 - long totalCent = value.multiply(new BigDecimal(100)).intValue(); //now get the cents - long eurOnly = value.longValue(); - long centOnly = Math.abs(totalCent % 100); - StringBuffer res = new StringBuffer(); - res.append(eurOnly); - res.append(decimalDelimiter); - if (centOnly < 10) { - res.append('0'); - } - res.append(centOnly); - return res.toString(); - } - - /** - 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 void setConformanceLevel(String newLevel) { - conformanceLevel=newLevel; - } - - - /** - * 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) - * */ - public PDDocumentCatalog PDFmakeA3compliant(PDDocument doc, String producer, String creator, - boolean attachZugferdHeaders) throws IOException, - TransformerException { - String fullProducer=producer+"(via mustangproject.org "+versionStr+")"; - PDDocumentCatalog cat = doc.getDocumentCatalog(); - PDMetadata metadata = new PDMetadata(doc); - cat.setMetadata(metadata); - // we're using the jempbox org.apache.jempbox.xmp.XMPMetadata version, - // not the xmpbox one - XMPMetadata xmp = new XMPMetadata(); - - XMPSchemaPDFAId pdfaid = new XMPSchemaPDFAId(xmp); - pdfaid.setAbout(""); //$NON-NLS-1$ - xmp.addSchema(pdfaid); - - XMPSchemaDublinCore dc = xmp.addDublinCoreSchema(); - dc.addCreator(creator); - dc.setAbout(""); //$NON-NLS-1$ - - XMPSchemaBasic xsb = xmp.addBasicSchema(); - xsb.setAbout(""); //$NON-NLS-1$ - - xsb.setCreatorTool(creator); - xsb.setCreateDate(GregorianCalendar.getInstance()); - // PDDocumentInformation pdi=doc.getDocumentInformation(); - PDDocumentInformation pdi = new PDDocumentInformation(); - pdi.setProducer(fullProducer); - pdi.setAuthor(creator); - doc.setDocumentInformation(pdi); - - XMPSchemaPDF pdf = xmp.addPDFSchema(); - pdf.setProducer(fullProducer); - pdf.setAbout(""); //$NON-NLS-1$ - - /* - // Mandatory: PDF/A3-a is tagged PDF which has to be expressed using a - // MarkInfo dictionary (PDF A/3 Standard sec. 6.7.2.2) - PDMarkInfo markinfo = new PDMarkInfo(); - markinfo.setMarked(true); - doc.getDocumentCatalog().setMarkInfo(markinfo); -*/ -/* - * - To be on the safe side, we use level B without Markinfo because we can not - guarantee that the user correctly tagged the templates for the PDF. - - * */ - pdfaid.setConformance(conformanceLevel);//$NON-NLS-1$ //$NON-NLS-1$ - - pdfaid.setPart(3); - - if (attachZugferdHeaders) { - addZugferdXMP(xmp); /* - * this is the only line where we do something - * Zugferd-specific, i.e. add PDF metadata - * specifically for Zugferd, not generically for - * a embedded file - */ - } - - metadata.importXMPMetadata(xmp); - return cat; - } - - private String getZugferdXMLForTransaction(IZUGFeRDExportableTransaction trans) { - SimpleDateFormat zugferdDateFormat = new SimpleDateFormat("yyyyMMdd"); //$NON-NLS-1$ - String xml= "\n" //$NON-NLS-1$ - - + "\n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " false\n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " urn:ferd:invoice:rc:comfort\n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " "+trans.getNumber()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " RECHNUNG\n" //$NON-NLS-1$ - + " 380\n" //$NON-NLS-1$ - + " "+zugferdDateFormat.format(trans.getIssueDate())+"\n" //date format was 20130605 //$NON-NLS-1$ //$NON-NLS-2$ -// + " \n" -// + " \n" -// + "Rechnung gemäß Bestellung Nr. 2013-471331 vom 01.03.2013.\n" -// + "\n" -// + " \n" -// + " \n" -// + " \n" -// + " \n" -// + "Es bestehen Rabatt- und Bonusvereinbarungen.\n" -// + " \n" -// + " AAK\n" -// + " \n" - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ -// + " AB-312\n" - + " \n" //$NON-NLS-1$ -// + " 4000001123452\n" - + " "+trans.getOwnOrganisationName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" - + " "+trans.getOwnZIP()+"\n" - + " "+trans.getOwnStreet()+"\n" - + " "+trans.getOwnLocation()+"\n" - + " "+trans.getOwnCountry()+"\n" - + " \n" - + " \n" //$NON-NLS-1$ - + " "+trans.getOwnTaxID()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " "+trans.getOwnVATID()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ -// + " GE2020211\n" -// + " 4000001987658\n" - + " "+trans.getRecipient().getName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ -// + " \n" -// + " xxx\n" -// + " \n" - + " \n" //$NON-NLS-1$ - + " "+trans.getRecipient().getZIP()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " "+trans.getRecipient().getStreet()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " "+trans.getRecipient().getLocation()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " "+trans.getRecipient().getCountry()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " "+trans.getRecipient().getVATID()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ -// + " \n" -// + " 20130301\n" -// + " 2013-471331\n" -// + " \n" - + " \n" //$NON-NLS-1$ - + " \n" - + " \n" - + " "+zugferdDateFormat.format(trans.getDeliveryDate())+"\n" - + " \n" - /* - + " \n" - + " 20130603\n" - + " 2013-51112\n" - + " \n" */ - + " \n" - + " \n" //$NON-NLS-1$ - + " "+trans.getNumber()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " EUR\n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " 42\n" //$NON-NLS-1$ - + " Überweisung\n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " "+trans.getOwnIBAN()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " "+trans.getOwnBIC()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " "+trans.getOwnBankName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n"; //$NON-NLS-1$ - - - /* - HashMap VATPercentAmountMap=trans.getVATPercentAmountMap(); - for (BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { - BigDecimal amount = VATPercentAmountMap.get(currentTaxPercent); - if (amount != null) { - xml += " \n" //$NON-NLS-1$ - + " "+currencyFormat(amount, '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " VAT\n" //$NON-NLS-1$ -// + " 129.37\n" - + " S\n" //$NON-NLS-1$ - + " "+currentTaxPercent+"\n" //$NON-NLS-1$ - + " \n"; //$NON-NLS-1$ - - - - } - }*/ -/* xml+= " - + " \n" - + " false\n" - + " 10\n" - + " 1.00\n" - + " Sondernachlass\n" - + " \n" - + " VAT\n" - + " S\n" - + " 19\n" - + " \n" - + " \n" - + " \n" - + " false\n" - + " 137.30\n" - + " 13.73\n" - + " Sondernachlass\n" - + " \n" - + " VAT\n" - + " S\n" - + " 7\n" - + " \n" - + " \n" - + " \n" - + " Versandkosten\n" - + " 5.80\n" - + " \n" - + " VAT\n" - + " S\n" - + " 7\n" - + " \n" - + " \n"*/ - - xml=xml+ " \n" //$NON-NLS-1$ -// + " Zahlbar innerhalb 30 Tagen netto bis 04.07.2013, 3% Skonto innerhalb 10 Tagen bis 15.06.2013\n" - + " "+zugferdDateFormat.format(trans.getDueDate())+"\n"//20130704 //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " "+currencyFormat(trans.getTotal(), '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ -// + " 5.80\n" -// + " 14.73\n" - + " "+currencyFormat(trans.getTotal(), '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " "+currencyFormat(trans.getTotalGross().subtract(trans.getTotal()), '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " "+currencyFormat(trans.getTotalGross(), '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ -// + " 0.00\n" - + " "+currencyFormat(trans.getTotalGross(), '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n"; //$NON-NLS-1$ -// + " \n" -// + " \n" -// + " \n" -// + " Wir erlauben uns Ihnen folgende Positionen aus der Lieferung Nr. 2013-51112 in Rechnung zu stellen:\n" -// + " \n" -// + " \n" -// + " \n"; - - - int lineID=0; - for (IZUGFeRDExportableItem currentItem : trans.getZFItems()) { - lineID++; - xml=xml+ " \n"+ //$NON-NLS-1$ - " \n" //$NON-NLS-1$ - + " "+lineID+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " "+currencyFormat(currentItem.getPriceGross(),'.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " 1\n" //$NON-NLS-1$ //$NON-NLS-2$ -// + " \n" -// + " false\n" -// + " 0.6667\n" -// + " Rabatt\n" -// + " \n" - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " "+currencyFormat(currentItem.getPrice(),'.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " 1\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - - + " \n" //$NON-NLS-1$ - + " "+currentItem.getQuantity()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " VAT\n" //$NON-NLS-1$ - + " S\n" //$NON-NLS-1$ - + " "+currentItem.getProduct().getVATPercent()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " "+currencyFormat(currentItem.getTotalGross(),'.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ - + " \n" //$NON-NLS-1$ -// + " 4012345001235\n" -// + " KR3M\n" -// + " 55T01\n" - + " "+currentItem.getProduct().getName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " "+currentItem.getProduct().getDescription()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ - + " \n" //$NON-NLS-1$ - + " \n"; //$NON-NLS-1$ - - - - } - - - xml=xml + " \n" //$NON-NLS-1$ - + ""; //$NON-NLS-1$ - return xml; - } - - /** - * embed the Zugferd XML structure in a file named ZUGFeRD-invoice.xml - * */ - public void PDFattachZugferdFile(PDDocument doc, IZUGFeRDExportableTransaction trans) throws IOException { - - // embedded files are stored in a named tree - PDEmbeddedFilesNameTreeNode efTree = new PDEmbeddedFilesNameTreeNode(); - - String filename="ZUGFeRD-invoice.xml"; //$NON-NLS-1$ - // first create the file specification, which holds the embedded file - PDComplexFileSpecification fs = new PDComplexFileSpecification(); - fs.setFile(filename); - - COSDictionary dict = fs.getCOSDictionary(); - // Relation "Source" for linking with eg. catalog - dict.setName("AFRelationship", "Alternative"); // as defined in Zugferd standard //$NON-NLS-1$ //$NON-NLS-2$ - - dict.setString("UF", filename); //$NON-NLS-1$ - - // create a dummy file stream, this would probably normally be a - // FileInputStream - - byte[] zugferdRaw = getZugferdXMLForTransaction(trans).getBytes("UTF-8"); //$NON-NLS-1$ - - byte[] zugferdData; - - - - if ((zugferdRaw[0]==(byte)0xEF)&&(zugferdRaw[1]==(byte)0xBB)&&(zugferdRaw[2]==(byte)0xBF)) { - // I don't like BOMs, lets remove it - zugferdData=new byte[zugferdRaw.length-3]; - System.arraycopy(zugferdRaw,3,zugferdData,0,zugferdRaw.length-3); - } else { - zugferdData=zugferdRaw; - } - - ByteArrayInputStream fakeFile = new ByteArrayInputStream(zugferdData); - PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile); - // now lets some of the optional parameters - ef.setSubtype("text/xml");// as defined in Zugferd standard //$NON-NLS-1$ - ef.setSize(zugferdData.length); - ef.setCreationDate(new GregorianCalendar()); - - ef.setModDate(GregorianCalendar.getInstance()); - - fs.setEmbeddedFile(ef); - - // now add the entry to the embedded file tree and set in the document. - efTree.setNames(Collections.singletonMap(filename, fs)); - PDDocumentNameDictionary names = new PDDocumentNameDictionary( - doc.getDocumentCatalog()); - names.setEmbeddedFiles(efTree); - doc.getDocumentCatalog().setNames(names); - // AF entry (Array) in catalog with the FileSpec - COSArray cosArray = new COSArray(); - cosArray.add(fs); - doc.getDocumentCatalog().getCOSDictionary().setItem("AF", cosArray); //$NON-NLS-1$ - - } - -/*** - * 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 - */ - private void addZugferdXMP(XMPMetadata metadata) { - - XMPSchemaZugferd zf = new XMPSchemaZugferd(metadata); - zf.setAbout(""); //$NON-NLS-1$ - metadata.addSchema(zf); - - XMPSchemaPDFAExtensions pdfaex = new XMPSchemaPDFAExtensions(metadata); - pdfaex.setAbout(""); //$NON-NLS-1$ - metadata.addSchema(pdfaex); - - } - -} +package org.mustangproject.ZUGFeRD; +/** + * Mustangproject's ZUGFeRD implementation + * ZUGFeRD exporter + * Licensed under the APLv2 + * @date 2014-07-12 + * @version 1.1 + * @author jstaerk + * */ + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.HashMap; + +import javax.xml.transform.TransformerException; + +import org.apache.jempbox.xmp.XMPMetadata; +import org.apache.jempbox.xmp.XMPSchemaBasic; +import org.apache.jempbox.xmp.XMPSchemaDublinCore; +import org.apache.jempbox.xmp.XMPSchemaPDF; +import org.apache.jempbox.xmp.pdfa.XMPSchemaPDFAId; +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.PDDocumentInformation; +import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; +import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; + + +public class ZUGFeRDExporter { + /*** + * You will need Apache PDFBox. To use the ZUGFeRD exporter, + implement IZUGFeRDExportableTransaction in yourTransaction + (which will require you to implement Product, Item and Contact) + then call + doc = PDDocument.load(PDFfilename); + // automatically add Zugferd to all outgoing invoices + ZUGFeRDExporter ze = new ZUGFeRDExporter(); + ze.PDFmakeA3compliant(doc, "Your application name", + System.getProperty("user.name"), true); + ze.PDFattachZugferdFile(doc, yourTransaction); + + doc.save(PDFfilename); + + * @author jstaerk + * + */ + + + + + + //// MAIN CLASS + + private String conformanceLevel="U"; + private String versionStr="1.0"; + + /** + * 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. + */ + byte[] zugferdData = null; + + private String currencyFormat(BigDecimal value, char decimalDelimiter) { + /* + * I needed 123,45, locale independent.I tried + * NumberFormat.getCurrencyInstance().format( 12345.6789 ); but that is + * locale specific.I also tried DecimalFormat df = new DecimalFormat( + * "0,00" ); df.setDecimalSeparatorAlwaysShown(true); + * df.setGroupingUsed(false); DecimalFormatSymbols symbols = new + * DecimalFormatSymbols(); symbols.setDecimalSeparator(','); + * symbols.setGroupingSeparator(' '); + * df.setDecimalFormatSymbols(symbols); + * + * but that would not switch off grouping. Although I liked very much + * the (incomplete) "BNF diagram" in + * http://docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html + * in the end I decided to calculate myself and take eur+sparator+cents + * + * This function will cut off, i.e. floor() subcent values Tests: + * System.err.println(utils.currencyFormat(new BigDecimal(0), + * ".")+"\n"+utils.currencyFormat(new BigDecimal("-1.10"), + * ",")+"\n"+utils.currencyFormat(new BigDecimal("-1.1"), + * ",")+"\n"+utils.currencyFormat(new BigDecimal("-1.01"), + * ",")+"\n"+utils.currencyFormat(new BigDecimal("20000123.3489"), + * ",")+"\n"+utils.currencyFormat(new BigDecimal("20000123.3419"), + * ",")+"\n"+utils.currencyFormat(new BigDecimal("12"), ",")); + * + * results 0.00 -1,10 -1,10 -1,01 20000123,34 20000123,34 12,00 + */ + value=value.setScale( 2, BigDecimal.ROUND_HALF_UP ); // first, round so that e.g. 1.189999999999999946709294817992486059665679931640625 becomes 1.19 + long totalCent = value.multiply(new BigDecimal(100)).intValue(); //now get the cents + long eurOnly = value.longValue(); + long centOnly = Math.abs(totalCent % 100); + StringBuffer res = new StringBuffer(); + res.append(eurOnly); + res.append(decimalDelimiter); + if (centOnly < 10) { + res.append('0'); + } + res.append(centOnly); + return res.toString(); + } + + /** + 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 void setConformanceLevel(String newLevel) { + conformanceLevel=newLevel; + } + + + /** + * 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) + * */ + public PDDocumentCatalog PDFmakeA3compliant(PDDocument doc, String producer, String creator, + boolean attachZugferdHeaders) throws IOException, + TransformerException { + String fullProducer=producer + " (via mustangproject.org " + versionStr + ")"; + PDDocumentCatalog cat = doc.getDocumentCatalog(); + PDMetadata metadata = new PDMetadata(doc); + cat.setMetadata(metadata); + // we're using the jempbox org.apache.jempbox.xmp.XMPMetadata version, + // not the xmpbox one + XMPMetadata xmp = new XMPMetadata(); + + XMPSchemaPDFAId pdfaid = new XMPSchemaPDFAId(xmp); + pdfaid.setAbout(""); //$NON-NLS-1$ + xmp.addSchema(pdfaid); + + XMPSchemaDublinCore dc = xmp.addDublinCoreSchema(); + dc.addCreator(creator); + dc.setAbout(""); //$NON-NLS-1$ + + XMPSchemaBasic xsb = xmp.addBasicSchema(); + xsb.setAbout(""); //$NON-NLS-1$ + + xsb.setCreatorTool(creator); + xsb.setCreateDate(GregorianCalendar.getInstance()); + // PDDocumentInformation pdi=doc.getDocumentInformation(); + PDDocumentInformation pdi = new PDDocumentInformation(); + pdi.setProducer(fullProducer); + pdi.setAuthor(creator); + doc.setDocumentInformation(pdi); + + XMPSchemaPDF pdf = xmp.addPDFSchema(); + pdf.setProducer(fullProducer); + pdf.setAbout(""); //$NON-NLS-1$ + + /* + // Mandatory: PDF/A3-a is tagged PDF which has to be expressed using a + // MarkInfo dictionary (PDF A/3 Standard sec. 6.7.2.2) + PDMarkInfo markinfo = new PDMarkInfo(); + markinfo.setMarked(true); + doc.getDocumentCatalog().setMarkInfo(markinfo); +*/ +/* + * + To be on the safe side, we use level B without Markinfo because we can not + guarantee that the user correctly tagged the templates for the PDF. + + * */ + pdfaid.setConformance(conformanceLevel);//$NON-NLS-1$ //$NON-NLS-1$ + + pdfaid.setPart(3); + + if (attachZugferdHeaders) { + addZugferdXMP(xmp); /* + * this is the only line where we do something + * Zugferd-specific, i.e. add PDF metadata + * specifically for Zugferd, not generically for + * a embedded file + */ + } + + metadata.importXMPMetadata(xmp); + return cat; + } + + private String getZugferdXMLForTransaction(IZUGFeRDExportableTransaction trans) { + SimpleDateFormat zugferdDateFormat = new SimpleDateFormat("yyyyMMdd"); //$NON-NLS-1$ + String xml= "\n" //$NON-NLS-1$ + + + "\n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " false\n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " urn:ferd:invoice:rc:comfort\n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " "+trans.getNumber()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " RECHNUNG\n" //$NON-NLS-1$ + + " 380\n" //$NON-NLS-1$ + + " "+zugferdDateFormat.format(trans.getIssueDate())+"\n" //date format was 20130605 //$NON-NLS-1$ //$NON-NLS-2$ +// + " \n" +// + " \n" +// + "Rechnung gemäß Bestellung Nr. 2013-471331 vom 01.03.2013.\n" +// + "\n" +// + " \n" +// + " \n" +// + " \n" +// + " \n" +// + "Es bestehen Rabatt- und Bonusvereinbarungen.\n" +// + " \n" +// + " AAK\n" +// + " \n" + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ +// + " AB-312\n" + + " \n" //$NON-NLS-1$ +// + " 4000001123452\n" + + " "+trans.getOwnOrganisationName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" + + " "+trans.getOwnZIP()+"\n" + + " "+trans.getOwnStreet()+"\n" + + " "+trans.getOwnLocation()+"\n" + + " "+trans.getOwnCountry()+"\n" + + " \n" + + " \n" //$NON-NLS-1$ + + " "+trans.getOwnTaxID()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " "+trans.getOwnVATID()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ +// + " GE2020211\n" +// + " 4000001987658\n" + + " "+trans.getRecipient().getName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ +// + " \n" +// + " xxx\n" +// + " \n" + + " \n" //$NON-NLS-1$ + + " "+trans.getRecipient().getZIP()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " "+trans.getRecipient().getStreet()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " "+trans.getRecipient().getLocation()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " "+trans.getRecipient().getCountry()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " "+trans.getRecipient().getVATID()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ +// + " \n" +// + " 20130301\n" +// + " 2013-471331\n" +// + " \n" + + " \n" //$NON-NLS-1$ + + " \n" + + " \n" + + " "+zugferdDateFormat.format(trans.getDeliveryDate())+"\n" + + " \n" + /* + + " \n" + + " 20130603\n" + + " 2013-51112\n" + + " \n" */ + + " \n" + + " \n" //$NON-NLS-1$ + + " "+trans.getNumber()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " EUR\n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " 42\n" //$NON-NLS-1$ + + " Überweisung\n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " "+trans.getOwnIBAN()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " "+trans.getOwnBIC()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " "+trans.getOwnBankName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n"; //$NON-NLS-1$ + + + /* + HashMap VATPercentAmountMap=trans.getVATPercentAmountMap(); + for (BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { + BigDecimal amount = VATPercentAmountMap.get(currentTaxPercent); + if (amount != null) { + xml += " \n" //$NON-NLS-1$ + + " "+currencyFormat(amount, '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " VAT\n" //$NON-NLS-1$ +// + " 129.37\n" + + " S\n" //$NON-NLS-1$ + + " "+currentTaxPercent+"\n" //$NON-NLS-1$ + + " \n"; //$NON-NLS-1$ + + + + } + }*/ +/* xml+= " + + " \n" + + " false\n" + + " 10\n" + + " 1.00\n" + + " Sondernachlass\n" + + " \n" + + " VAT\n" + + " S\n" + + " 19\n" + + " \n" + + " \n" + + " \n" + + " false\n" + + " 137.30\n" + + " 13.73\n" + + " Sondernachlass\n" + + " \n" + + " VAT\n" + + " S\n" + + " 7\n" + + " \n" + + " \n" + + " \n" + + " Versandkosten\n" + + " 5.80\n" + + " \n" + + " VAT\n" + + " S\n" + + " 7\n" + + " \n" + + " \n"*/ + + xml=xml+ " \n" //$NON-NLS-1$ +// + " Zahlbar innerhalb 30 Tagen netto bis 04.07.2013, 3% Skonto innerhalb 10 Tagen bis 15.06.2013\n" + + " "+zugferdDateFormat.format(trans.getDueDate())+"\n"//20130704 //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " "+currencyFormat(trans.getTotal(), '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ +// + " 5.80\n" +// + " 14.73\n" + + " "+currencyFormat(trans.getTotal(), '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " "+currencyFormat(trans.getTotalGross().subtract(trans.getTotal()), '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " "+currencyFormat(trans.getTotalGross(), '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ +// + " 0.00\n" + + " "+currencyFormat(trans.getTotalGross(), '.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n"; //$NON-NLS-1$ +// + " \n" +// + " \n" +// + " \n" +// + " Wir erlauben uns Ihnen folgende Positionen aus der Lieferung Nr. 2013-51112 in Rechnung zu stellen:\n" +// + " \n" +// + " \n" +// + " \n"; + + + int lineID=0; + for (IZUGFeRDExportableItem currentItem : trans.getZFItems()) { + lineID++; + xml=xml+ " \n"+ //$NON-NLS-1$ + " \n" //$NON-NLS-1$ + + " "+lineID+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " "+currencyFormat(currentItem.getPriceGross(),'.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " 1\n" //$NON-NLS-1$ //$NON-NLS-2$ +// + " \n" +// + " false\n" +// + " 0.6667\n" +// + " Rabatt\n" +// + " \n" + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " "+currencyFormat(currentItem.getPrice(),'.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " 1\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + + " \n" //$NON-NLS-1$ + + " "+currentItem.getQuantity()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " VAT\n" //$NON-NLS-1$ + + " S\n" //$NON-NLS-1$ + + " "+currentItem.getProduct().getVATPercent()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " "+currencyFormat(currentItem.getTotalGross(),'.')+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ + + " \n" //$NON-NLS-1$ +// + " 4012345001235\n" +// + " KR3M\n" +// + " 55T01\n" + + " "+currentItem.getProduct().getName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " "+currentItem.getProduct().getDescription()+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " \n" //$NON-NLS-1$ + + " \n"; //$NON-NLS-1$ + + + + } + + + xml=xml + " \n" //$NON-NLS-1$ + + ""; //$NON-NLS-1$ + return xml; + } + + /** + * Embeds the Zugferd XML structure in a file named ZUGFeRD-invoice.xml. + * + * @param doc PDDocument to attach an XML invoice to + * @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) + */ + public void PDFattachZugferdFile(PDDocument doc, IZUGFeRDExportableTransaction trans) throws IOException { + + // embedded files are stored in a named tree + PDEmbeddedFilesNameTreeNode efTree = new PDEmbeddedFilesNameTreeNode(); + + String filename="ZUGFeRD-invoice.xml"; //$NON-NLS-1$ + // first create the file specification, which holds the embedded file + PDComplexFileSpecification fs = new PDComplexFileSpecification(); + fs.setFile(filename); + + COSDictionary dict = fs.getCOSDictionary(); + // Relation "Source" for linking with eg. catalog + dict.setName("AFRelationship", "Alternative"); // as defined in Zugferd standard //$NON-NLS-1$ //$NON-NLS-2$ + + dict.setString("UF", filename); //$NON-NLS-1$ + + if (zugferdData == null) // XML ZUGFeRD data not set externally, needs to be built + { + // create a dummy file stream, this would probably normally be a + // FileInputStream + + byte[] zugferdRaw = getZugferdXMLForTransaction(trans).getBytes("UTF-8"); //$NON-NLS-1$ + + if ((zugferdRaw[0]==(byte)0xEF)&&(zugferdRaw[1]==(byte)0xBB)&&(zugferdRaw[2]==(byte)0xBF)) { + // I don't like BOMs, lets remove it + zugferdData=new byte[zugferdRaw.length-3]; + System.arraycopy(zugferdRaw,3,zugferdData,0,zugferdRaw.length-3); + } else { + zugferdData=zugferdRaw; + } + } + + ByteArrayInputStream fakeFile = new ByteArrayInputStream(zugferdData); + PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile); + // now lets some of the optional parameters + ef.setSubtype("text/xml");// as defined in Zugferd standard //$NON-NLS-1$ + ef.setSize(zugferdData.length); + ef.setCreationDate(new GregorianCalendar()); + + ef.setModDate(GregorianCalendar.getInstance()); + + fs.setEmbeddedFile(ef); + + // now add the entry to the embedded file tree and set in the document. + efTree.setNames(Collections.singletonMap(filename, fs)); + PDDocumentNameDictionary names = new PDDocumentNameDictionary( + doc.getDocumentCatalog()); + names.setEmbeddedFiles(efTree); + doc.getDocumentCatalog().setNames(names); + // AF entry (Array) in catalog with the FileSpec + COSArray cosArray = new COSArray(); + cosArray.add(fs); + doc.getDocumentCatalog().getCOSDictionary().setItem("AF", cosArray); //$NON-NLS-1$ + + } + + /** + * 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). + */ + public void setZUGFeRDXMLData(byte[] zugferdData) + { + this.zugferdData = zugferdData; + } + +/*** + * 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 + */ + private void addZugferdXMP(XMPMetadata metadata) { + + XMPSchemaZugferd zf = new XMPSchemaZugferd(metadata); + zf.setAbout(""); //$NON-NLS-1$ + metadata.addSchema(zf); + + XMPSchemaPDFAExtensions pdfaex = new XMPSchemaPDFAExtensions(metadata); + pdfaex.setAbout(""); //$NON-NLS-1$ + metadata.addSchema(pdfaex); + + } + +} diff --git a/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java b/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java index 1c8f7dc6..10c32cc1 100644 --- a/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java +++ b/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java @@ -1,358 +1,390 @@ -package org.mustangproject.ZUGFeRD; -/** - * Mustangproject's ZUGFeRD implementation - * ZUGFeRD importer - * Licensed under the APLv2 - * @date 2014-07-07 - * @version 1.1.0 - * @author jstaerk - * */ - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; -import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; -import org.apache.pdfbox.pdmodel.common.COSObjectable; -import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; -import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -//root.setNamespace(Namespace.getNamespace("http://www.energystar.gov/manageBldgs/req")); - -public class ZUGFeRDImporter { - /* - call extract(importFilename). - containsMeta() will return if ZUGFeRD data has been found, - afterwards you can call getBIC(), getIBAN() etc. - -*/ - - /**@var if metadata has been found */ - private boolean containsMeta=false; - /**@var the reference (i.e. invoice number) of the sender */ - private String foreignReference; - private String BIC; - private String IBAN; - private String holder; - private String amount; - private String meta; - private String bankName; - private boolean amountFound; - - - - - public void extract(String pdfFilename) { - PDDocument doc = null; - try { - doc = PDDocument.load(pdfFilename); -// PDDocumentInformation info = doc.getDocumentInformation(); - PDDocumentNameDictionary names = new PDDocumentNameDictionary( - doc.getDocumentCatalog()); - PDEmbeddedFilesNameTreeNode etn; - etn = names.getEmbeddedFiles(); - if (etn==null) { - doc.close(); - return; - } - Map efMap = etn.getNames(); - // String filePath = "/tmp/"; - for (String filename : efMap.keySet()) { - /** - * currently (in the release candidate of version 1) only one - * attached file with the name ZUGFeRD-invoice.xml is allowed - * */ - if (filename.equals("ZUGFeRD-invoice.xml")) { //$NON-NLS-1$ - containsMeta = true; - - PDComplexFileSpecification fileSpec = (PDComplexFileSpecification) efMap - .get(filename); - PDEmbeddedFile embeddedFile = fileSpec.getEmbeddedFile(); - // String embeddedFilename = filePath + filename; - // File file = new File(filePath + filename); - // System.out.println("Writing " + embeddedFilename); - // ByteArrayOutputStream fileBytes=new - // ByteArrayOutputStream(); - // FileOutputStream fos = new FileOutputStream(file); - - setMeta(new String(embeddedFile.getByteArray())); - // fos.write(embeddedFile.getByteArray()); - // fos.close(); - } - } - - } catch (IOException e1) { - // TODO Auto-generated catch block - e1.printStackTrace(); - } - finally { - try { - if(doc!=null) { - doc.close(); - } - } catch (IOException e) {} - } - - } - - public void parse() { - DocumentBuilderFactory factory = null; - DocumentBuilder builder = null; - Document document = null; - - factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); //otherwise we can not act namespace independend, i.e. use document.getElementsByTagNameNS("*",... - try { - builder = factory.newDocumentBuilder(); - } catch (ParserConfigurationException ex3) { - // TODO Auto-generated catch block - ex3.printStackTrace(); - } - - try { - InputStream bais = new ByteArrayInputStream(meta.getBytes()); - document = builder.parse(bais); - } catch (SAXException ex1) { - ex1.printStackTrace(); - } catch (IOException ex2) { - ex2.printStackTrace(); - } - NodeList ndList ; - - // rootNode = document.getDocumentElement(); - // ApplicableSupplyChainTradeSettlement - ndList = document.getDocumentElement() - .getElementsByTagNameNS("*","PaymentReference"); //$NON-NLS-1$ - - for (int bookingIndex = 0; bookingIndex < ndList - .getLength(); bookingIndex++) { - Node booking = ndList.item(bookingIndex); - // if there is a attribute in the tag number:value - - setForeignReference(booking.getTextContent()); - - } -/* - ndList = document - .getElementsByTagName("GermanBankleitzahlID"); //$NON-NLS-1$ - - for (int bookingIndex = 0; bookingIndex < ndList - .getLength(); bookingIndex++) { - Node booking = ndList.item(bookingIndex); - // if there is a attribute in the tag number:value - setBIC(booking.getTextContent()); - - } - - ndList = document.getElementsByTagName("ProprietaryID"); //$NON-NLS-1$ - - for (int bookingIndex = 0; bookingIndex < ndList - .getLength(); bookingIndex++) { - Node booking = ndList.item(bookingIndex); - // if there is a attribute in the tag number:value - setIBAN(booking.getTextContent()); - - } - - DE1234 - - - DE5656565 - Commerzbank - - -*/ - ndList = document.getElementsByTagNameNS("*","PayeePartyCreditorFinancialAccount"); //$NON-NLS-1$ - - for (int bookingIndex = 0; bookingIndex < ndList - .getLength(); bookingIndex++) { - - Node booking = ndList.item(bookingIndex); - // there are many "name" elements, so get the one below - // SellerTradeParty - NodeList bookingDetails = booking.getChildNodes(); - - - for (int detailIndex = 0; detailIndex < bookingDetails - .getLength(); detailIndex++) { - Node detail = bookingDetails.item(detailIndex); - if ((detail.getLocalName()!=null)&&(detail.getLocalName().equals("IBANID"))) { //$NON-NLS-1$ - setIBAN(detail.getTextContent()); - - } - } - - } - - ndList = document.getElementsByTagNameNS("*","PayeeSpecifiedCreditorFinancialInstitution"); //$NON-NLS-1$ - - for (int bookingIndex = 0; bookingIndex < ndList - .getLength(); bookingIndex++) { - Node booking = ndList.item(bookingIndex); - // there are many "name" elements, so get the one below - // SellerTradeParty - NodeList bookingDetails = booking.getChildNodes(); - for (int detailIndex = 0; detailIndex < bookingDetails - .getLength(); detailIndex++) { - Node detail = bookingDetails.item(detailIndex); - if ((detail.getLocalName()!=null)&&(detail.getLocalName().equals("BICID"))) { //$NON-NLS-1$ - setBIC(detail.getTextContent()); - } - if ((detail.getLocalName()!=null)&&(detail.getLocalName().equals("Name"))) { //$NON-NLS-1$ - setBankName(detail.getTextContent()); - } - } - - } - - // - ndList = document.getElementsByTagNameNS("*","SellerTradeParty"); //$NON-NLS-1$ - - for (int bookingIndex = 0; bookingIndex < ndList - .getLength(); bookingIndex++) { - Node booking = ndList.item(bookingIndex); - // there are many "name" elements, so get the one below - // SellerTradeParty - NodeList bookingDetails = booking.getChildNodes(); - for (int detailIndex = 0; detailIndex < bookingDetails - .getLength(); detailIndex++) { - Node detail = bookingDetails.item(detailIndex); - if ((detail.getLocalName()!=null)&&(detail.getLocalName().equals("Name"))) { //$NON-NLS-1$ - setHolder(detail.getTextContent()); - } - } - - } - - ndList = document.getElementsByTagNameNS("*","DuePayableAmount"); //$NON-NLS-1$ - - for (int bookingIndex = 0; bookingIndex < ndList - .getLength(); bookingIndex++) { - Node booking = ndList.item(bookingIndex); - // if there is a attribute in the tag number:value - amountFound=true; - setAmount(booking.getTextContent()); - - } - - - if (!amountFound) { - /* there is apparently no requirement to mention DuePayableAmount,, - * if it's not there, check for GrandTotalAmount - */ - ndList = document.getElementsByTagNameNS("*","GrandTotalAmount"); //$NON-NLS-1$ - for (int bookingIndex = 0; bookingIndex < ndList - .getLength(); bookingIndex++) { - Node booking = ndList.item(bookingIndex); - // if there is a attribute in the tag number:value - amountFound=true; - setAmount(booking.getTextContent()); - - } - - } - - - } - - - public boolean containsMeta() { - return containsMeta; - } - - public String getForeignReference() { - return foreignReference; - } - - - - public void setForeignReference(String foreignReference) { - this.foreignReference = foreignReference; - } - - - - public String getBIC() { - return BIC; - } - - - - private void setBIC(String bic) { - this.BIC = bic; - } - - - private void setBankName(String bankname) { - this.bankName = bankname; - } - - - - public String getIBAN() { - return IBAN; - } - - - public String getBankName() { - return bankName; - } - - - - public void setIBAN(String IBAN) { - this.IBAN = IBAN; - } - - - - public String getHolder() { - return holder; - } - - - - private void setHolder(String holder) { - this.holder = holder; - } - - - - public String getAmount() { - return amount; - } - - - private void setAmount(String amount) { - this.amount = amount; - } - - public void setMeta(String meta) { - this.meta=meta; - } - - public String getMeta() { - return meta; - } - - /** - * will return true if the metadata (just extract-ed or set with setMeta) contains ZUGFeRD XML - * */ - public boolean canParse() { - - - //SpecifiedExchangedDocumentContext is in the schema, so a relatively good indication if zugferd is present - better than just invoice - return (meta!=null)&&( meta.length()>0)&&( meta.contains("SpecifiedExchangedDocumentContext")); //$NON-NLS-1$ - } -} +package org.mustangproject.ZUGFeRD; +/** + * Mustangproject's ZUGFeRD implementation + * ZUGFeRD importer + * Licensed under the APLv2 + * @date 2014-07-07 + * @version 1.1.0 + * @author jstaerk + * */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; +import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; +import org.apache.pdfbox.pdmodel.common.COSObjectable; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +//root.setNamespace(Namespace.getNamespace("http://www.energystar.gov/manageBldgs/req")); + +public class ZUGFeRDImporter { + /* + call extract(importFilename). + containsMeta() will return if ZUGFeRD data has been found, + afterwards you can call getBIC(), getIBAN() etc. + +*/ + + /**@var if metadata has been found */ + private boolean containsMeta=false; + /**@var the reference (i.e. invoice number) of the sender */ + private String foreignReference; + private String BIC; + private String IBAN; + private String holder; + private String amount; + /** Raw XML form of the extracted data - may be directly obtained. */ + private byte[] rawXML=null; + private String bankName; + private boolean amountFound; + + /** + * Extracts a ZUGFeRD invoice from a PDF document represented by a file name. + * Errors are just logged to STDOUT. + */ + public void extract(String pdfFilename) + { + try + { + extractLowLevel(new BufferedInputStream(new FileInputStream(pdfFilename))); + } catch (IOException ioe) + { + ioe.printStackTrace(); + } + } + + /** + * Extracts a ZUGFeRD invoice from a PDF document represented by an input stream. + * Errors are reported via exception handling. + */ + public void extractLowLevel(InputStream pdfStream) throws IOException { + PDDocument doc = null; + try { + doc = PDDocument.load(pdfStream); +// PDDocumentInformation info = doc.getDocumentInformation(); + PDDocumentNameDictionary names = new PDDocumentNameDictionary( + doc.getDocumentCatalog()); + PDEmbeddedFilesNameTreeNode etn; + etn = names.getEmbeddedFiles(); + if (etn==null) { + doc.close(); + return; + } + Map efMap = etn.getNames(); + // String filePath = "/tmp/"; + for (String filename : efMap.keySet()) { + /** + * currently (in the release candidate of version 1) only one + * attached file with the name ZUGFeRD-invoice.xml is allowed + * */ + if (filename.equals("ZUGFeRD-invoice.xml")) { //$NON-NLS-1$ + containsMeta = true; + + PDComplexFileSpecification fileSpec = (PDComplexFileSpecification) efMap + .get(filename); + PDEmbeddedFile embeddedFile = fileSpec.getEmbeddedFile(); + // String embeddedFilename = filePath + filename; + // File file = new File(filePath + filename); + // System.out.println("Writing " + embeddedFilename); + // ByteArrayOutputStream fileBytes=new + // ByteArrayOutputStream(); + // FileOutputStream fos = new FileOutputStream(file); + rawXML = embeddedFile.getByteArray(); + setMeta(new String(rawXML)); + // fos.write(embeddedFile.getByteArray()); + // fos.close(); + } + } + + } catch (IOException e1) { + throw e1; + } + finally { + try { + if(doc!=null) { + doc.close(); + } + } catch (IOException e) {} + } + + } + + public void parse() { + DocumentBuilderFactory factory = null; + DocumentBuilder builder = null; + Document document = null; + + factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); //otherwise we can not act namespace independend, i.e. use document.getElementsByTagNameNS("*",... + try { + builder = factory.newDocumentBuilder(); + } catch (ParserConfigurationException ex3) { + // TODO Auto-generated catch block + ex3.printStackTrace(); + } + + try { + InputStream bais = new ByteArrayInputStream(rawXML); + document = builder.parse(bais); + } catch (SAXException ex1) { + ex1.printStackTrace(); + } catch (IOException ex2) { + ex2.printStackTrace(); + } + NodeList ndList ; + + // rootNode = document.getDocumentElement(); + // ApplicableSupplyChainTradeSettlement + ndList = document.getDocumentElement() + .getElementsByTagNameNS("*","PaymentReference"); //$NON-NLS-1$ + + for (int bookingIndex = 0; bookingIndex < ndList + .getLength(); bookingIndex++) { + Node booking = ndList.item(bookingIndex); + // if there is a attribute in the tag number:value + + setForeignReference(booking.getTextContent()); + + } +/* + ndList = document + .getElementsByTagName("GermanBankleitzahlID"); //$NON-NLS-1$ + + for (int bookingIndex = 0; bookingIndex < ndList + .getLength(); bookingIndex++) { + Node booking = ndList.item(bookingIndex); + // if there is a attribute in the tag number:value + setBIC(booking.getTextContent()); + + } + + ndList = document.getElementsByTagName("ProprietaryID"); //$NON-NLS-1$ + + for (int bookingIndex = 0; bookingIndex < ndList + .getLength(); bookingIndex++) { + Node booking = ndList.item(bookingIndex); + // if there is a attribute in the tag number:value + setIBAN(booking.getTextContent()); + + } + + DE1234 + + + DE5656565 + Commerzbank + + +*/ + ndList = document.getElementsByTagNameNS("*","PayeePartyCreditorFinancialAccount"); //$NON-NLS-1$ + + for (int bookingIndex = 0; bookingIndex < ndList + .getLength(); bookingIndex++) { + + Node booking = ndList.item(bookingIndex); + // there are many "name" elements, so get the one below + // SellerTradeParty + NodeList bookingDetails = booking.getChildNodes(); + + + for (int detailIndex = 0; detailIndex < bookingDetails + .getLength(); detailIndex++) { + Node detail = bookingDetails.item(detailIndex); + if ((detail.getLocalName()!=null)&&(detail.getLocalName().equals("IBANID"))) { //$NON-NLS-1$ + setIBAN(detail.getTextContent()); + + } + } + + } + + ndList = document.getElementsByTagNameNS("*","PayeeSpecifiedCreditorFinancialInstitution"); //$NON-NLS-1$ + + for (int bookingIndex = 0; bookingIndex < ndList + .getLength(); bookingIndex++) { + Node booking = ndList.item(bookingIndex); + // there are many "name" elements, so get the one below + // SellerTradeParty + NodeList bookingDetails = booking.getChildNodes(); + for (int detailIndex = 0; detailIndex < bookingDetails + .getLength(); detailIndex++) { + Node detail = bookingDetails.item(detailIndex); + if ((detail.getLocalName()!=null)&&(detail.getLocalName().equals("BICID"))) { //$NON-NLS-1$ + setBIC(detail.getTextContent()); + } + if ((detail.getLocalName()!=null)&&(detail.getLocalName().equals("Name"))) { //$NON-NLS-1$ + setBankName(detail.getTextContent()); + } + } + + } + + // + ndList = document.getElementsByTagNameNS("*","SellerTradeParty"); //$NON-NLS-1$ + + for (int bookingIndex = 0; bookingIndex < ndList + .getLength(); bookingIndex++) { + Node booking = ndList.item(bookingIndex); + // there are many "name" elements, so get the one below + // SellerTradeParty + NodeList bookingDetails = booking.getChildNodes(); + for (int detailIndex = 0; detailIndex < bookingDetails + .getLength(); detailIndex++) { + Node detail = bookingDetails.item(detailIndex); + if ((detail.getLocalName()!=null)&&(detail.getLocalName().equals("Name"))) { //$NON-NLS-1$ + setHolder(detail.getTextContent()); + } + } + + } + + ndList = document.getElementsByTagNameNS("*","DuePayableAmount"); //$NON-NLS-1$ + + for (int bookingIndex = 0; bookingIndex < ndList + .getLength(); bookingIndex++) { + Node booking = ndList.item(bookingIndex); + // if there is a attribute in the tag number:value + amountFound=true; + setAmount(booking.getTextContent()); + + } + + + if (!amountFound) { + /* there is apparently no requirement to mention DuePayableAmount,, + * if it's not there, check for GrandTotalAmount + */ + ndList = document.getElementsByTagNameNS("*","GrandTotalAmount"); //$NON-NLS-1$ + for (int bookingIndex = 0; bookingIndex < ndList + .getLength(); bookingIndex++) { + Node booking = ndList.item(bookingIndex); + // if there is a attribute in the tag number:value + amountFound=true; + setAmount(booking.getTextContent()); + + } + + } + + + } + + + public boolean containsMeta() { + return containsMeta; + } + + public String getForeignReference() { + return foreignReference; + } + + + + public void setForeignReference(String foreignReference) { + this.foreignReference = foreignReference; + } + + + + public String getBIC() { + return BIC; + } + + + + private void setBIC(String bic) { + this.BIC = bic; + } + + + private void setBankName(String bankname) { + this.bankName = bankname; + } + + + + public String getIBAN() { + return IBAN; + } + + + public String getBankName() { + return bankName; + } + + + + public void setIBAN(String IBAN) { + this.IBAN = IBAN; + } + + + + public String getHolder() { + return holder; + } + + + + private void setHolder(String holder) { + this.holder = holder; + } + + + + public String getAmount() { + return amount; + } + + + private void setAmount(String amount) { + this.amount = amount; + } + + public void setMeta(String meta) { + this.rawXML=meta.getBytes(); + } + + public String getMeta() { + if (rawXML==null){ + return null; + } else { + return new String(rawXML); + } + } + + + /** + * Returns the raw XML data as extracted from the ZUGFeRD PDF file. + */ + public byte[] getRawXML() + { + return rawXML; + } + + /** + * will return true if the metadata (just extract-ed or set with setMeta) contains ZUGFeRD XML + * */ + public boolean canParse() { + + + //SpecifiedExchangedDocumentContext is in the schema, so a relatively good indication if zugferd is present - better than just invoice + String meta=getMeta(); + return (meta!=null)&&( meta.length()>0)&&( meta.contains("SpecifiedExchangedDocumentContext")); //$NON-NLS-1$ + } +} diff --git a/mustang/target/classes/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.class b/mustang/target/classes/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.class index b5c652b2..54f41f99 100644 Binary files a/mustang/target/classes/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.class and b/mustang/target/classes/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.class differ diff --git a/mustang/target/classes/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.class b/mustang/target/classes/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.class index f2e67642..e4f918d3 100644 Binary files a/mustang/target/classes/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.class and b/mustang/target/classes/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.class differ diff --git a/mustang/target/maven-archiver/pom.properties b/mustang/target/maven-archiver/pom.properties index f76d5b7c..d353e8da 100644 --- a/mustang/target/maven-archiver/pom.properties +++ b/mustang/target/maven-archiver/pom.properties @@ -1,5 +1,5 @@ #Generated by Maven -#Sat Jun 28 19:58:49 CEST 2014 -version=1.1.0.alpha +#Fri Aug 08 19:33:01 CEST 2014 +version=1.1.1alpha groupId=org.mustangproject.ZUGFeRD artifactId=mustang diff --git a/mustang/target/mustang-1.0.1.jar b/mustang/target/mustang-1.0.1.jar deleted file mode 100644 index a699b18d..00000000 Binary files a/mustang/target/mustang-1.0.1.jar and /dev/null differ diff --git a/mustang/target/mustang-1.0.2.jar b/mustang/target/mustang-1.0.2.jar deleted file mode 100644 index 85a027b7..00000000 Binary files a/mustang/target/mustang-1.0.2.jar and /dev/null differ diff --git a/mustang/target/mustang-1.0.jar b/mustang/target/mustang-1.0.jar deleted file mode 100644 index 4c1c0e68..00000000 Binary files a/mustang/target/mustang-1.0.jar and /dev/null differ diff --git a/mustang/target/mustang-1.1.0.alpha.jar b/mustang/target/mustang-1.1.0.alpha.jar deleted file mode 100644 index 410917b3..00000000 Binary files a/mustang/target/mustang-1.1.0.alpha.jar and /dev/null differ diff --git a/mustang/target/mustang-1.1.0.jar b/mustang/target/mustang-1.1.0.jar deleted file mode 100644 index 88bf3ae3..00000000 Binary files a/mustang/target/mustang-1.1.0.jar and /dev/null differ diff --git a/mustang/target/surefire-reports/TEST-org.mustangproject.ZUGFeRD.AppTest.xml b/mustang/target/surefire-reports/TEST-org.mustangproject.ZUGFeRD.AppTest.xml deleted file mode 100644 index 2aaddb88..00000000 --- a/mustang/target/surefire-reports/TEST-org.mustangproject.ZUGFeRD.AppTest.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mustang/target/surefire-reports/org.mustangproject.ZUGFeRD.AppTest.txt b/mustang/target/surefire-reports/org.mustangproject.ZUGFeRD.AppTest.txt deleted file mode 100644 index e5dc4367..00000000 --- a/mustang/target/surefire-reports/org.mustangproject.ZUGFeRD.AppTest.txt +++ /dev/null @@ -1,4 +0,0 @@ -------------------------------------------------------------------------------- -Test set: org.mustangproject.ZUGFeRD.AppTest -------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.223 sec