diff --git a/mustang/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableTransaction.java b/mustang/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableTransaction.java index da74ee77..fb4d5963 100644 --- a/mustang/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableTransaction.java +++ b/mustang/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableTransaction.java @@ -12,7 +12,6 @@ package org.mustangproject.ZUGFeRD; import java.math.BigDecimal; import java.util.Date; -import java.util.HashMap; public interface IZUGFeRDExportableTransaction { @@ -122,13 +121,4 @@ public interface IZUGFeRDExportableTransaction { - /** - * which taxes have been used with which amounts in this transaction, - * empty for no taxes, or e.g. 19=>190 and 7=>14 if 1000 Eur were applicable - * to 19% VAT (=>190 EUR VAT) and 200 EUR were applicable to 7% (=>14 EUR VAT) - * 190 Eur - * @return - * - HashMap getVATPercentAmountMap(); - */ } diff --git a/mustang/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java b/mustang/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java new file mode 100644 index 00000000..f1982b40 --- /dev/null +++ b/mustang/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java @@ -0,0 +1,35 @@ +package org.mustangproject.ZUGFeRD; + +import java.math.BigDecimal; + +public class VATAmount { + + public VATAmount(BigDecimal basis, BigDecimal calculated) { + super(); + this.basis = basis; + this.calculated = calculated; + } + + BigDecimal basis, calculated; + + public BigDecimal getBasis() { + return basis; + } + + public void setBasis(BigDecimal basis) { + this.basis = basis; + } + + public BigDecimal getCalculated() { + return calculated; + } + + public void setCalculated(BigDecimal calculated) { + this.calculated = calculated; + } + + public VATAmount add(VATAmount v) { + return new VATAmount(basis.add(v.getBasis()), calculated.add(v.getCalculated())); + } + +} diff --git a/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.java b/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.java index 4e5bd111..f5c04857 100644 --- a/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.java +++ b/mustang/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporter.java @@ -12,8 +12,11 @@ package org.mustangproject.ZUGFeRD; import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigDecimal; +import java.math.MathContext; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.text.SimpleDateFormat; -import java.util.Collections; +import java.util.Arrays; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Map; @@ -40,6 +43,7 @@ import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecifica import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; + public class ZUGFeRDExporter { /*** * You will need Apache PDFBox. To use the ZUGFeRD exporter, @@ -78,8 +82,10 @@ public class ZUGFeRDExporter { * pass a IZUGFeRDExportableTransaction for the XML to be populated. */ byte[] zugferdData = null; - - private String currencyFormat(BigDecimal value, char decimalDelimiter) { + private boolean isTest; + + + private String nDigitFormat(BigDecimal value, int scale) { /* * I needed 123,45, locale independent.I tried * NumberFormat.getCurrencyInstance().format( 12345.6789 ); but that is @@ -106,18 +112,30 @@ public class ZUGFeRDExporter { * * 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(); + value=value.setScale( scale, BigDecimal.ROUND_HALF_UP ); // first, round so that e.g. 1.189999999999999946709294817992486059665679931640625 becomes 1.19 + char[] repeat = new char[scale]; + Arrays.fill(repeat, '0'); + + DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(); + otherSymbols.setDecimalSeparator('.'); + DecimalFormat dec = new DecimalFormat("0."+new String(repeat), otherSymbols); + return dec.format(value); + + } + + private String vatFormat(BigDecimal value) { + return nDigitFormat(value, 2); + } + + private String currencyFormat(BigDecimal value) { + return nDigitFormat(value, 2); + } + + private String priceFormat(BigDecimal value) { + return nDigitFormat(value, 4); + } + private String quantityFormat(BigDecimal value) { + return nDigitFormat(value, 4); } /** @@ -138,6 +156,12 @@ public class ZUGFeRDExporter { conformanceLevel=newLevel; } + /** + * enables the flag to indicate a test invoice in the XML structure + * */ + public void setTest() { + isTest=true; + } /** * Makes A PDF/A3a-compliant document from a PDF-A1 compliant document (on @@ -208,17 +232,23 @@ public class ZUGFeRDExporter { } private String getZugferdXMLForTransaction(IZUGFeRDExportableTransaction trans) { + SimpleDateFormat germanDateFormat = new SimpleDateFormat("dd.MM.yyyy"); //$NON-NLS-1$ SimpleDateFormat zugferdDateFormat = new SimpleDateFormat("yyyyMMdd"); //$NON-NLS-1$ + String testBooleanStr="false"; + if (isTest) { + testBooleanStr="true"; + + } String xml= "\n" //$NON-NLS-1$ + "\n" //$NON-NLS-1$ + " \n" //$NON-NLS-1$ - + " false\n" //$NON-NLS-1$ + + " "+testBooleanStr+"\n" //$NON-NLS-1$ + " \n" //$NON-NLS-1$ - + " urn:ferd:invoice:rc:comfort\n" //$NON-NLS-1$ + + " urn:ferd:CrossIndustryDocument:invoice:1p0:comfort\n" //$NON-NLS-1$ + " \n" //$NON-NLS-1$ + " \n" //$NON-NLS-1$ + " \n" //$NON-NLS-1$ @@ -306,23 +336,23 @@ public class ZUGFeRDExporter { + " \n"; //$NON-NLS-1$ - /* - HashMap VATPercentAmountMap=trans.getVATPercentAmountMap(); + + HashMap VATPercentAmountMap=getVATPercentAmountMap(trans); for (BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { - BigDecimal amount = VATPercentAmountMap.get(currentTaxPercent); + VATAmount 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" //$NON-NLS-1$ + + " "+currencyFormat(amount.getCalculated())+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " VAT\n" //$NON-NLS-1$ + + " "+currencyFormat(amount.getBasis())+"\n" + + " S\n" //$NON-NLS-1$ + + " "+vatFormat(currentTaxPercent)+"\n" //$NON-NLS-1$ + + " \n"; //$NON-NLS-1$ } - }*/ + } /* xml+= " + " \n" + " false\n" @@ -357,18 +387,20 @@ public class ZUGFeRDExporter { + " \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" + + " Zahlbar ohne Abzug bis "+germanDateFormat.format(trans.getDueDate())+"\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$ + + " "+currencyFormat(trans.getTotal())+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " 0.00\n" //$NON-NLS-1$ + + " 0.00\n" //$NON-NLS-1$ // + " 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$ + + " "+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$ + + " "+currencyFormat(trans.getTotalGross())+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + " \n" //$NON-NLS-1$ + " \n"; //$NON-NLS-1$ // + " \n" @@ -390,8 +422,8 @@ public class ZUGFeRDExporter { + " \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$ + + " "+priceFormat(currentItem.getPriceGross())+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " 1.0000\n" //$NON-NLS-1$ //$NON-NLS-2$ // + " \n" // + " false\n" // + " 0.6667\n" @@ -399,22 +431,22 @@ public class ZUGFeRDExporter { // + " \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$ + + " "+priceFormat(currentItem.getPrice())+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + + " 1.0000\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$ + + " "+quantityFormat(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$ + + " "+vatFormat(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$ + + " "+currencyFormat(currentItem.getTotalGross())+"\n" //$NON-NLS-1$ //$NON-NLS-2$ + " \n" //$NON-NLS-1$ + " \n" //$NON-NLS-1$ + " \n" //$NON-NLS-1$ @@ -436,6 +468,34 @@ public class ZUGFeRDExporter { return xml; } + + /** + * which taxes have been used with which amounts in this transaction, + * empty for no taxes, or e.g. 19=>190 and 7=>14 if 1000 Eur were applicable + * to 19% VAT (=>190 EUR VAT) and 200 EUR were applicable to 7% (=>14 EUR VAT) + * 190 Eur + * @return + * + */ + private HashMap getVATPercentAmountMap(IZUGFeRDExportableTransaction trans) { + HashMap hm=new HashMap (); + + for (IZUGFeRDExportableItem currentItem : trans.getZFItems()) { + BigDecimal percent=currentItem.getProduct().getVATPercent(); + BigDecimal currentBasis=currentItem.getQuantity().multiply(currentItem.getPrice()).setScale(2,BigDecimal.ROUND_HALF_UP );// item total net amount + VATAmount itemVATAmount=new VATAmount(currentBasis, currentItem.getTotalGross().subtract(currentBasis)) ; + VATAmount current=hm.get(percent); + if (current==null) { + hm.put(percent, itemVATAmount); + } else { + hm.put(percent, current.add(itemVATAmount)); + + } + } + + return hm; + } + /** * Embeds the Zugferd XML structure in a file named ZUGFeRD-invoice.xml. * @@ -445,20 +505,6 @@ public class ZUGFeRDExporter { */ 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 @@ -475,34 +521,8 @@ public class ZUGFeRDExporter { } } - 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); - - // In addition make sure the embedded file is set under /UF - dict = fs.getCOSDictionary(); - 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. - 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$ + PDFAttachGenericFile(doc, "ZUGFeRD-invoice.xml", "Alternative", "Invoice metadata conforming to ZUGFeRD standard (http://www.ferd-net.de/front_content.php?idcat=231&lang=4)", "text/xml", zugferdData); } @@ -511,18 +531,21 @@ public class ZUGFeRDExporter { * * @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 */ - public void PDFAttachGenericFile(PDDocument doc, String filename, String subType, byte[] data) + public void PDFAttachGenericFile(PDDocument doc, String filename, String relationship, String description, String subType, byte[] data) throws IOException { PDComplexFileSpecification fs = new PDComplexFileSpecification(); fs.setFile(filename); COSDictionary dict = fs.getCOSDictionary(); - dict.setName("AFRelationship", "Alternative"); + dict.setName("AFRelationship", relationship); dict.setString("UF", filename); + dict.setString("Desc", description); ByteArrayInputStream fakeFile = new ByteArrayInputStream(data); PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile);