Merge branch 'master' into master
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package org.mustangproject;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/***
|
||||
@@ -28,6 +30,7 @@ public class Allowance extends Charge {
|
||||
* @return false since its not supposed to be calculated negatively
|
||||
*/
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public boolean isCharge() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2023 Jochen Stärk, see LICENSE file
|
||||
package org.mustangproject;
|
||||
|
||||
|
||||
import org.mustangproject.ZUGFeRD.TransactionCalculator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class CalculatedInvoice extends Invoice implements Serializable {
|
||||
|
||||
protected BigDecimal grandTotal=null;
|
||||
|
||||
public void calculate() {
|
||||
TransactionCalculator tc=new TransactionCalculator(this);
|
||||
grandTotal=tc.getGrandTotal();
|
||||
}
|
||||
public BigDecimal getGrandTotal() {
|
||||
if (grandTotal==null) {
|
||||
calculate();
|
||||
}
|
||||
return grandTotal;
|
||||
}
|
||||
public CalculatedInvoice setGrandTotal(BigDecimal grand) {
|
||||
grandTotal=grand;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.mustangproject;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import org.mustangproject.ZUGFeRD.IAbsoluteValueProvider;
|
||||
import org.mustangproject.ZUGFeRD.IZUGFeRDAllowanceCharge;
|
||||
|
||||
@@ -151,6 +152,7 @@ public class Charge implements IZUGFeRDAllowanceCharge {
|
||||
* @return true since it is supposed to be calculated negatively
|
||||
*/
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public boolean isCharge() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import org.mustangproject.ZUGFeRD.*;
|
||||
import org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants;
|
||||
|
||||
@@ -37,6 +38,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
* @see IExportableTransaction if you want to implement an interface instead
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
public class Invoice implements IExportableTransaction {
|
||||
|
||||
protected String documentName = null, documentCode = null, number = null, ownOrganisationFullPlaintextInfo = null, referenceNumber = null, shipToOrganisationID = null, shipToOrganisationName = null, shipToStreet = null, shipToZIP = null, shipToLocation = null, shipToCountry = null, buyerOrderReferencedDocumentID = null, invoiceReferencedDocumentID = null, buyerOrderReferencedDocumentIssueDateTime = null, ownForeignOrganisationID = null, ownOrganisationName = null, currency = null, paymentTermDescription = null;
|
||||
@@ -520,6 +522,20 @@ public class Invoice implements IExportableTransaction {
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* this is wrong and only used from jackson
|
||||
* @param iza
|
||||
* @return
|
||||
*/
|
||||
public Invoice setZFAllowances(Allowance[] iza) {
|
||||
Allowances=new ArrayList<>();
|
||||
|
||||
for (IZUGFeRDAllowanceCharge cz:iza) {
|
||||
Allowances.add(cz);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public IZUGFeRDAllowanceCharge[] getZFCharges() {
|
||||
@@ -530,6 +546,18 @@ public class Invoice implements IExportableTransaction {
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* this is wrong and only used from jackson
|
||||
* @param iza
|
||||
* @return
|
||||
*/
|
||||
public Invoice setZFCharges(Charge[] iza) {
|
||||
Charges=new ArrayList<>();
|
||||
for (IZUGFeRDAllowanceCharge cz:iza) {
|
||||
Charges.add(cz);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IZUGFeRDAllowanceCharge[] getZFLogisticsServiceCharges() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.mustangproject;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import org.mustangproject.ZUGFeRD.IReferencedDocument;
|
||||
import org.mustangproject.ZUGFeRD.IZUGFeRDAllowanceCharge;
|
||||
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem;
|
||||
@@ -18,6 +19,7 @@ import java.util.Date;
|
||||
*/
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
public class Item implements IZUGFeRDExportableItem {
|
||||
protected BigDecimal price = BigDecimal.ZERO;
|
||||
protected BigDecimal quantity;
|
||||
@@ -314,7 +316,7 @@ public class Item implements IZUGFeRDExportableItem {
|
||||
|
||||
|
||||
/***
|
||||
* adds item level references along with their typecodes and issuerassignedIDs (contract ID, cost centre, ...)
|
||||
* adds item level references along with their typecodes and issuerassignedIDs (contract ID, cost centre, ...)
|
||||
* @param doc the ReferencedDocument to add
|
||||
* @return fluent setter
|
||||
*/
|
||||
@@ -333,8 +335,8 @@ public class Item implements IZUGFeRDExportableItem {
|
||||
}
|
||||
return additionalReference.toArray(new IReferencedDocument[0]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/***
|
||||
* specify a item level delivery period
|
||||
* (apart from the document level delivery period, and the document level
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.mustangproject;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import org.mustangproject.ZUGFeRD.IDesignatedProductClassification;
|
||||
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableProduct;
|
||||
import org.mustangproject.util.NodeMap;
|
||||
@@ -17,6 +18,8 @@ import java.util.Map;
|
||||
* describes a product, good or service used in an invoice item line
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
|
||||
public class Product implements IZUGFeRDExportableProduct {
|
||||
protected String unit, name, sellerAssignedID, buyerAssignedID;
|
||||
protected String description="";
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableContact;
|
||||
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableTradeParty;
|
||||
import org.mustangproject.ZUGFeRD.IZUGFeRDLegalOrganisation;
|
||||
@@ -19,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
* A organisation, i.e. usually a company
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
public class TradeParty implements IZUGFeRDExportableTradeParty {
|
||||
|
||||
protected String name, zip, street, location, country;
|
||||
@@ -500,6 +502,7 @@ public class TradeParty implements IZUGFeRDExportableTradeParty {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* (optional)
|
||||
*
|
||||
@@ -511,6 +514,14 @@ public class TradeParty implements IZUGFeRDExportableTradeParty {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* primarily for invoiceimporter and JSON
|
||||
* @return the list of sepa mandates
|
||||
*/
|
||||
public List<DirectDebit> getDebitDetails() {
|
||||
return debitDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IZUGFeRDLegalOrganisation getLegalOrganisation() {
|
||||
return legalOrg;
|
||||
@@ -670,6 +681,7 @@ public class TradeParty implements IZUGFeRDExportableTradeParty {
|
||||
return contact;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public IZUGFeRDTradeSettlement[] getAsTradeSettlement() {
|
||||
if (bankDetails.isEmpty() && debitDetails.isEmpty()) {
|
||||
return null;
|
||||
|
||||
@@ -4,9 +4,13 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.dom4j.io.XMLWriter;
|
||||
import org.mustangproject.ZUGFeRD.ZUGFeRDDateFormat;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
public class XMLTools extends XMLWriter {
|
||||
@Override
|
||||
@@ -39,6 +43,52 @@ public class XMLTools extends XMLWriter {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the value of an node
|
||||
*
|
||||
* @param node the Node to get the value from
|
||||
* @return A String or empty String, if no value was found
|
||||
*/
|
||||
public static String getNodeValue(Node node) {
|
||||
if (node != null && node.getFirstChild() != null) {
|
||||
return node.getFirstChild().getNodeValue();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* tries to convert a String to BigDecimal.
|
||||
*
|
||||
* @param nodeValue The value as String
|
||||
* @return a BigDecimal with the value provides as String or a BigDecimal with value 0.00 if an error occurs
|
||||
*/
|
||||
public static BigDecimal tryBigDecimal(String nodeValue) {
|
||||
try {
|
||||
return new BigDecimal(nodeValue);
|
||||
} catch (final Exception e) {
|
||||
try {
|
||||
return BigDecimal.valueOf(Float.valueOf(nodeValue));
|
||||
} catch (final Exception ex) {
|
||||
return new BigDecimal("0.00");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* tries to convert a Node to a BigDecimal.
|
||||
*
|
||||
* @param node The value as String
|
||||
* @return a BigDecimal with the value provides as String or a BigDecimal with value 0.00 if an error occurs
|
||||
*/
|
||||
public static BigDecimal tryBigDecimal(Node node) {
|
||||
final String nodeValue = XMLTools.getNodeValue(node);
|
||||
if (nodeValue.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return XMLTools.tryBigDecimal(nodeValue);
|
||||
}
|
||||
/***
|
||||
* formats a number so that at least minDecimals are displayed but at the maximum maxDecimals are there, i.e.
|
||||
* cuts potential 0s off the end until minDecimals
|
||||
@@ -60,7 +110,39 @@ public class XMLTools extends XMLWriter {
|
||||
}
|
||||
|
||||
|
||||
public static String encodeXML(CharSequence s) {
|
||||
/***
|
||||
* returns a util.Date from a 102 String yyyymmdd in a node
|
||||
* @param node the node
|
||||
* @return a util.Date, or null, if not parseable
|
||||
*/
|
||||
public static Date tryDate(Node node) {
|
||||
final String nodeValue = XMLTools.getNodeValue(node);
|
||||
if (nodeValue.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return tryDate(nodeValue);
|
||||
}
|
||||
|
||||
/***
|
||||
* returns a util.Date from a 102 String yyyymmdd
|
||||
* @param toParse the string
|
||||
* @return a util.Date, or null, if not parseable
|
||||
*/
|
||||
public static Date tryDate(String toParse) {
|
||||
final SimpleDateFormat formatter = ZUGFeRDDateFormat.DATE.getFormatter();
|
||||
try {
|
||||
return formatter.parse(toParse);
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* relplaces some entities like < , > and & with their escaped pendant like <
|
||||
* @param s the string
|
||||
* @return the "safe" string
|
||||
*/
|
||||
public static String encodeXML(CharSequence s) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -34,9 +34,13 @@ public class LineCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
BigDecimal vatPercent = currentItem.getProduct().getVATPercent();
|
||||
if (vatPercent == null)
|
||||
BigDecimal vatPercent = null;
|
||||
if (currentItem.getProduct()!=null) {
|
||||
vatPercent = currentItem.getProduct().getVATPercent();
|
||||
}
|
||||
if (vatPercent == null) {
|
||||
vatPercent = BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal multiplicator = vatPercent.divide(BigDecimal.valueOf(100));
|
||||
priceGross = currentItem.getPrice(); // see https://github.com/ZUGFeRD/mustangproject/issues/159
|
||||
price = priceGross.subtract(allowance).add(charge);
|
||||
|
||||
@@ -13,6 +13,27 @@ package org.mustangproject.ZUGFeRD;
|
||||
* @version 1.1.0
|
||||
* @author jstaerk
|
||||
*/
|
||||
import java.io.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpression;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.fop.util.XMLUtil;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.io.IOUtils;
|
||||
@@ -43,57 +64,19 @@ import java.nio.file.StandardOpenOption;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class ZUGFeRDImporter {
|
||||
public class ZUGFeRDImporter extends ZUGFeRDInvoiceImporter {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRDImporter.class);
|
||||
|
||||
/**
|
||||
* if metadata has been found
|
||||
*/
|
||||
protected boolean containsMeta = false;
|
||||
/**
|
||||
* map filenames of additional XML files to their contents
|
||||
*/
|
||||
private final HashMap<String, byte[]> additionalXMLs = new HashMap<>();
|
||||
/**
|
||||
* map filenames of all embedded files in the respective PDF
|
||||
*/
|
||||
private final ArrayList<FileAttachment> PDFAttachments = new ArrayList<>();
|
||||
/**
|
||||
* Raw XML form of the extracted data - may be directly obtained.
|
||||
*/
|
||||
private byte[] rawXML = null;
|
||||
/**
|
||||
* XMP metadata
|
||||
*/
|
||||
private String xmpString = null; // XMP metadata
|
||||
/**
|
||||
* parsed Document
|
||||
*/
|
||||
private Document document;
|
||||
private Integer version;
|
||||
|
||||
|
||||
protected ZUGFeRDImporter() {
|
||||
//constructor for extending classes
|
||||
public ZUGFeRDImporter() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ZUGFeRDImporter(String pdfFilename) {
|
||||
try (InputStream bis = Files.newInputStream(Paths.get(pdfFilename), StandardOpenOption.READ)) {
|
||||
extractLowLevel(bis);
|
||||
} catch (final IOException e) {
|
||||
LOGGER.error("Failed to extract ZUGFeRD data", e);
|
||||
throw new ZUGFeRDExportException(e);
|
||||
}
|
||||
public ZUGFeRDImporter(String filename) {
|
||||
super(filename);
|
||||
}
|
||||
|
||||
|
||||
public ZUGFeRDImporter(InputStream pdfStream) {
|
||||
try {
|
||||
extractLowLevel(pdfStream);
|
||||
} catch (final IOException e) {
|
||||
LOGGER.error("Failed to extract ZUGFeRD data", e);
|
||||
throw new ZUGFeRDExportException(e);
|
||||
}
|
||||
public ZUGFeRDImporter(InputStream stream) {
|
||||
super(stream);
|
||||
}
|
||||
|
||||
|
||||
@@ -107,144 +90,6 @@ public class ZUGFeRDImporter {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts a ZUGFeRD invoice from a PDF document represented by an input stream. Errors are reported via exception handling.
|
||||
*
|
||||
* @param inStream a inputstream of a pdf file
|
||||
*/
|
||||
private void extractLowLevel(InputStream inStream) throws IOException {
|
||||
BufferedInputStream pdfStream = new BufferedInputStream(inStream);
|
||||
byte[] pad = new byte[4];
|
||||
pdfStream.mark(0);
|
||||
pdfStream.read(pad);
|
||||
pdfStream.reset();
|
||||
byte[] pdfSignature = {'%', 'P', 'D', 'F'};
|
||||
if (Arrays.equals(pad, pdfSignature)) { // we have a pdf
|
||||
|
||||
|
||||
try (PDDocument doc = Loader.loadPDF(IOUtils.toByteArray(pdfStream))) {
|
||||
// PDDocumentInformation info = doc.getDocumentInformation();
|
||||
final PDDocumentNameDictionary names = new PDDocumentNameDictionary(doc.getDocumentCatalog());
|
||||
//start
|
||||
|
||||
if (doc.getDocumentCatalog() == null || doc.getDocumentCatalog().getMetadata() == null) {
|
||||
LOGGER.info("no-xmlpart");
|
||||
return;
|
||||
}
|
||||
|
||||
final InputStream XMP = doc.getDocumentCatalog().getMetadata().exportXMPMetadata();
|
||||
xmpString = convertStreamToString(XMP);
|
||||
|
||||
final PDEmbeddedFilesNameTreeNode etn = names.getEmbeddedFiles();
|
||||
if (etn == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, PDComplexFileSpecification> efMap = etn.getNames();
|
||||
// String filePath = "/tmp/";
|
||||
|
||||
if (efMap != null) {
|
||||
extractFiles(efMap); // see
|
||||
// https://memorynotfound.com/apache-pdfbox-extract-embedded-file-pdf-document/
|
||||
} else {
|
||||
|
||||
final List<PDNameTreeNode<PDComplexFileSpecification>> kids = etn.getKids();
|
||||
if (kids == null) {
|
||||
return;
|
||||
}
|
||||
for (final PDNameTreeNode<PDComplexFileSpecification> node : kids) {
|
||||
final Map<String, PDComplexFileSpecification> namesL = node.getNames();
|
||||
extractFiles(namesL);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no PDF probably XML
|
||||
containsMeta = true;
|
||||
setRawXML(XMLTools.getBytesFromStream(pdfStream));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void extractFiles(Map<String, PDComplexFileSpecification> names) throws IOException {
|
||||
for (final String alias : names.keySet()) {
|
||||
|
||||
final PDComplexFileSpecification fileSpec = names.get(alias);
|
||||
final String filename = fileSpec.getFilename();
|
||||
/**
|
||||
* filenames for invoice data (ZUGFeRD v1 and v2, Factur-X)
|
||||
*/
|
||||
|
||||
final PDEmbeddedFile embeddedFile = fileSpec.getEmbeddedFile();
|
||||
if ((filename.equals("ZUGFeRD-invoice.xml") || (filename.equals("zugferd-invoice.xml")) || filename.equals("factur-x.xml")) || filename.equals("xrechnung.xml") || filename.equals("order-x.xml") || filename.equals("cida.xml")) {
|
||||
containsMeta = true;
|
||||
|
||||
// String embeddedFilename = filePath + filename;
|
||||
// File file = new File(filePath + filename);
|
||||
// System.out.println("Writing " + embeddedFilename);
|
||||
// ByteArrayOutputStream fileBytes=new
|
||||
// ByteArrayOutputStream();
|
||||
// FileOutputStream fos = new FileOutputStream(file);
|
||||
|
||||
setRawXML(embeddedFile.toByteArray());
|
||||
|
||||
// fos.write(embeddedFile.getByteArray());
|
||||
// fos.close();
|
||||
}
|
||||
if (filename.startsWith("additional_data")) {
|
||||
additionalXMLs.put(filename, embeddedFile.toByteArray());
|
||||
}
|
||||
PDFAttachments.add(new FileAttachment(filename, embeddedFile.getSubtype(), "Data", embeddedFile.toByteArray()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected Document getDocument() {
|
||||
return document;
|
||||
}
|
||||
|
||||
|
||||
private void setDocument() throws ParserConfigurationException, IOException, SAXException {
|
||||
final DocumentBuilderFactory xmlFact = DocumentBuilderFactory.newInstance();
|
||||
xmlFact.setNamespaceAware(true);
|
||||
final DocumentBuilder builder = xmlFact.newDocumentBuilder();
|
||||
final ByteArrayInputStream is = new ByteArrayInputStream(rawXML);
|
||||
/// is.skip(guessBOMSize(is));
|
||||
document = builder.parse(is);
|
||||
}
|
||||
|
||||
|
||||
public void setRawXML(byte[] rawXML) throws IOException {
|
||||
this.containsMeta = true;
|
||||
this.rawXML = rawXML;
|
||||
this.version = null;
|
||||
try {
|
||||
setDocument();
|
||||
} catch (ParserConfigurationException | SAXException e) {
|
||||
LOGGER.error("Failed to parse XML", e);
|
||||
throw new ZUGFeRDExportException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected String extractString(String xpathStr) {
|
||||
if (!containsMeta) {
|
||||
throw new ZUGFeRDExportException("No suitable data/ZUGFeRD file could be found.");
|
||||
}
|
||||
final String result;
|
||||
try {
|
||||
final Document document = getDocument();
|
||||
final XPathFactory xpathFact = XPathFactory.newInstance();
|
||||
final XPath xpath = xpathFact.newXPath();
|
||||
result = xpath.evaluate(xpathStr, document);
|
||||
} catch (final XPathExpressionException e) {
|
||||
LOGGER.error("Failed to evaluate XPath", e);
|
||||
throw new ZUGFeRDExportException(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/***
|
||||
* Wrapper for protected method extractString
|
||||
* @param xpathStr the xpath expression to be evaluated
|
||||
@@ -255,21 +100,21 @@ public class ZUGFeRDImporter {
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @return the reference (purpose) the sender specified for this invoice
|
||||
*/
|
||||
public String getForeignReference() {
|
||||
String result = extractString("//*[local-name() = 'ApplicableHeaderTradeSettlement']/*[local-name() = 'PaymentReference']");
|
||||
if (result == null || result.isEmpty()) {
|
||||
result = extractString("//*[local-name() = 'ApplicableSupplyChainTradeSettlement']/*[local-name() = 'PaymentReference']");
|
||||
}
|
||||
return result;
|
||||
|
||||
return importedInvoice.getNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the ZUGFeRD Profile
|
||||
*/
|
||||
public String getZUGFeRDProfil() {
|
||||
|
||||
String guideline = extractString("//*[local-name() = 'GuidelineSpecifiedDocumentContextParameter']//*[local-name() = 'ID']");
|
||||
if (guideline.contains("xrechnung")) {
|
||||
return "XRECHNUNG";
|
||||
@@ -293,21 +138,6 @@ public class ZUGFeRDImporter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Invoice Currency Code
|
||||
*/
|
||||
public String getInvoiceCurrencyCode() {
|
||||
try {
|
||||
if (getVersion() == 1) {
|
||||
return extractString("//*[local-name() = 'ApplicableSupplyChainTradeSettlement']//*[local-name() = 'InvoiceCurrencyCode']");
|
||||
} else {
|
||||
return extractString("//*[local-name() = 'ApplicableHeaderTradeSettlement']//*[local-name() = 'InvoiceCurrencyCode']");
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
// Exception was already logged
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the IssuerAssigned ID
|
||||
@@ -330,57 +160,6 @@ public class ZUGFeRDImporter {
|
||||
return extractIssuerAssignedID("ContractReferencedDocument");
|
||||
}
|
||||
|
||||
private String extractIssuerAssignedID(String propertyName) {
|
||||
try {
|
||||
if (getVersion() == 1) {
|
||||
return extractString("//*[local-name() = '" + propertyName + "']//*[local-name() = 'ID']");
|
||||
} else {
|
||||
return extractString("//*[local-name() = '" + propertyName + "']//*[local-name() = 'IssuerAssignedID']");
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
// Exception was already logged
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the BuyerTradeParty ID
|
||||
*/
|
||||
public String getBuyerTradePartyID() {
|
||||
return extractString("//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'ID']");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Issue Date()
|
||||
*/
|
||||
public String getIssueDate() {
|
||||
try {
|
||||
if (getVersion() == 1) {
|
||||
return extractString("//*[local-name() = 'HeaderExchangedDocument']//*[local-name() = 'IssueDateTime']//*[local-name() = 'DateTimeString']");
|
||||
} else {
|
||||
return extractString("//*[local-name() = 'ExchangedDocument']//*[local-name() = 'IssueDateTime']//*[local-name() = 'DateTimeString']");
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
// Exception was already logged
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public Date getDetailedDeliveryPeriodFrom() {
|
||||
final String toParse = extractString(
|
||||
"//*[local-name() = 'ApplicableHeaderTradeSettlement']" +
|
||||
"//*[local-name() = 'BillingSpecifiedPeriod']" +
|
||||
"//*[local-name() = 'StartDateTime']//*[local-name() = 'DateTimeString']");
|
||||
return tryDate(toParse);
|
||||
}
|
||||
|
||||
public Date getDetailedDeliveryPeriodTo() {
|
||||
final String toParse = extractString(
|
||||
"//*[local-name() = 'ApplicableHeaderTradeSettlement']" +
|
||||
"//*[local-name() = 'BillingSpecifiedPeriod']" +
|
||||
"//*[local-name() = 'EndDateTime']//*[local-name() = 'DateTimeString']");
|
||||
return tryDate(toParse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the TaxBasisTotalAmount
|
||||
@@ -464,7 +243,16 @@ public class ZUGFeRDImporter {
|
||||
* @return the BuyerTradeParty SpecifiedTaxRegistration ID
|
||||
*/
|
||||
public String getBuyertradePartySpecifiedTaxRegistrationID() {
|
||||
return extractString("//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'SpecifiedTaxRegistration']//*[local-name() = 'ID']");
|
||||
String id = null;
|
||||
if ((importedInvoice.getRecipient()!=null) && (importedInvoice.getRecipient().getLegalOrganisation()!=null)) {
|
||||
// this *should* be the official result
|
||||
id = importedInvoice.getRecipient().getLegalOrganisation().getSchemedID().getID();
|
||||
}
|
||||
// but also provide some fallback
|
||||
if (id == null) {
|
||||
id = getBuyerTradePartyID();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
@@ -488,14 +276,14 @@ public class ZUGFeRDImporter {
|
||||
* @return the BuyerTradeParty Name
|
||||
*/
|
||||
public String getBuyerTradePartyName() {
|
||||
return extractString("//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'Name']");
|
||||
return importedInvoice.getRecipient().getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the BuyerTradeParty Name
|
||||
*/
|
||||
public String getDeliveryTradePartyName() {
|
||||
return extractString("//*[local-name() = 'ShipToTradeParty']//*[local-name() = 'Name']");
|
||||
return importedInvoice.getDeliveryAddress().getName();
|
||||
}
|
||||
|
||||
|
||||
@@ -542,16 +330,7 @@ public class ZUGFeRDImporter {
|
||||
* @return the Invoice ID
|
||||
*/
|
||||
public String getInvoiceID() {
|
||||
try {
|
||||
if (getVersion() == 1) {
|
||||
return extractString("//*[local-name() = 'HeaderExchangedDocument']//*[local-name() = 'ID']");
|
||||
} else {
|
||||
return extractString("//*[local-name() = 'ExchangedDocument']//*[local-name() = 'ID']");
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
// Exception was already logged
|
||||
return "";
|
||||
}
|
||||
return importedInvoice.getNumber();
|
||||
}
|
||||
|
||||
|
||||
@@ -609,11 +388,21 @@ public class ZUGFeRDImporter {
|
||||
* @return the sender's account IBAN code
|
||||
*/
|
||||
public String getIBAN() {
|
||||
return extractString("//*[local-name() = 'PayeePartyCreditorFinancialAccount']/*[local-name() = 'IBANID']");
|
||||
for (IZUGFeRDTradeSettlement settlement : importedInvoice.getTradeSettlement()) {
|
||||
if (settlement instanceof IZUGFeRDTradeSettlementDebit) {
|
||||
return ((IZUGFeRDTradeSettlementDebit) settlement).getIBAN();
|
||||
}
|
||||
if (settlement instanceof IZUGFeRDTradeSettlementPayment) {
|
||||
return ((IZUGFeRDTradeSettlementPayment) settlement).getOwnIBAN();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public String getHolder() {
|
||||
|
||||
|
||||
return extractString("//*[local-name() = 'SellerTradeParty']/*[local-name() = 'Name']");
|
||||
}
|
||||
|
||||
@@ -622,14 +411,8 @@ public class ZUGFeRDImporter {
|
||||
* @return the total payable amount
|
||||
*/
|
||||
public String getAmount() {
|
||||
String result = extractString("//*[local-name() = 'SpecifiedTradeSettlementHeaderMonetarySummation']/*[local-name() = 'DuePayableAmount']");
|
||||
if (result == null || result.isEmpty()) {
|
||||
|
||||
/* fx/zf would be SpecifiedTradeSettlementMonetarySummation
|
||||
* but ox is SpecifiedTradeSettlementHeaderMonetarySummation...*/
|
||||
result = extractString("//*[local-name() = 'GrandTotalAmount']");
|
||||
}
|
||||
return result;
|
||||
return importedInvoice.getGrandTotal().toPlainString();
|
||||
}
|
||||
|
||||
|
||||
@@ -637,7 +420,60 @@ public class ZUGFeRDImporter {
|
||||
* @return when the payment is due
|
||||
*/
|
||||
public String getDueDate() {
|
||||
return extractString("//*[local-name() = 'SpecifiedTradePaymentTerms']/*[local-name() = 'DueDateDateTime']/*[local-name() = 'DateTimeString']");
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
|
||||
return sdf.format(importedInvoice.getDueDate());
|
||||
}
|
||||
|
||||
|
||||
////////////////////
|
||||
|
||||
/**
|
||||
* @return the Invoice Currency Code
|
||||
*/
|
||||
public String getInvoiceCurrencyCode() {
|
||||
return importedInvoice.getCurrency();
|
||||
}
|
||||
|
||||
|
||||
private String extractIssuerAssignedID(String propertyName) {
|
||||
try {
|
||||
if (getVersion() == 1) {
|
||||
return extractString("//*[local-name() = '" + propertyName + "']//*[local-name() = 'ID']");
|
||||
} else {
|
||||
return extractString("//*[local-name() = '" + propertyName + "']//*[local-name() = 'IssuerAssignedID']");
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
// Exception was already logged
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the BuyerTradeParty ID
|
||||
*/
|
||||
public String getBuyerTradePartyID() {
|
||||
String id = importedInvoice.getRecipient().getID();
|
||||
if (id == null) {
|
||||
// provide some fallback
|
||||
id = importedInvoice.getRecipient().getVATID();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Issue Date()
|
||||
*/
|
||||
public String getIssueDate() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
|
||||
return sdf.format(importedInvoice.getIssueDate());
|
||||
}
|
||||
|
||||
public Date getDetailedDeliveryPeriodFrom() {
|
||||
return importedInvoice.getDetailedDeliveryPeriodFrom();
|
||||
}
|
||||
|
||||
public Date getDetailedDeliveryPeriodTo() {
|
||||
return importedInvoice.getDetailedDeliveryPeriodTo();
|
||||
}
|
||||
|
||||
|
||||
@@ -685,28 +521,6 @@ public class ZUGFeRDImporter {
|
||||
}
|
||||
|
||||
|
||||
public EStandard getStandard() throws Exception {
|
||||
if (!containsMeta) {
|
||||
throw new Exception("Not yet parsed");
|
||||
}
|
||||
final String head = getUTF8();
|
||||
String rootNode = extractString("local-name(/*)");
|
||||
if (rootNode.equals("CrossIndustryDocument")) {
|
||||
return EStandard.zugferd;
|
||||
} else if (rootNode.equals("Invoice")) {
|
||||
return EStandard.ubl;
|
||||
} else if (rootNode.equals("CrossIndustryInvoice")) {
|
||||
return EStandard.facturx;
|
||||
} else if (rootNode.equals("SCRDMCCBDACIDAMessageStructure")) {
|
||||
return EStandard.despatchadvice;
|
||||
} else if (head.contains("<rsm:SCRDMCCBDACIOMessageStructure")) {
|
||||
return EStandard.orderx;
|
||||
}
|
||||
|
||||
throw new Exception("ZUGFeRD version could not be determined");
|
||||
|
||||
}
|
||||
|
||||
public int getVersion() throws Exception {
|
||||
if (!containsMeta) {
|
||||
throw new Exception("Not yet parsed");
|
||||
@@ -730,35 +544,6 @@ public class ZUGFeRDImporter {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return return UTF8 XML (without BOM) of the invoice
|
||||
*/
|
||||
public String getUTF8() {
|
||||
if (rawXML == null) {
|
||||
return null;
|
||||
}
|
||||
if (rawXML.length < 3) {
|
||||
return new String(rawXML);
|
||||
}
|
||||
|
||||
|
||||
final byte[] bomlessData;
|
||||
|
||||
if ((rawXML[0] == (byte) 0xEF)
|
||||
&& (rawXML[1] == (byte) 0xBB)
|
||||
&& (rawXML[2] == (byte) 0xBF)) {
|
||||
// I don't like BOMs, lets remove it
|
||||
bomlessData = new byte[rawXML.length - 3];
|
||||
System.arraycopy(rawXML, 3, bomlessData, 0,
|
||||
rawXML.length - 3);
|
||||
} else {
|
||||
bomlessData = rawXML;
|
||||
}
|
||||
|
||||
return new String(bomlessData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the raw XML data as extracted from the ZUGFeRD PDF file.
|
||||
*
|
||||
@@ -784,14 +569,6 @@ public class ZUGFeRDImporter {
|
||||
}
|
||||
|
||||
|
||||
static String convertStreamToString(java.io.InputStream is) {
|
||||
try {
|
||||
return IOUtils.toString(is, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an instance of PostalTradeAddress for SellerTradeParty section
|
||||
*
|
||||
@@ -803,7 +580,7 @@ public class ZUGFeRDImporter {
|
||||
|
||||
try {
|
||||
if (getVersion() == 1) {
|
||||
nl = getNodeListByPath("//*[local-name() = 'CrossIndustryDocument']//*[local-name() = 'SpecifiedSupplyChainTradeTransaction']/*[local-name() = 'ApplicableSupplyChainTradeAgreement']//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'PostalTradeAddress']");
|
||||
nl = getNodeListByPath("//*[localname() = 'CrossIndustryDocument']//*[local-name() = 'SpecifiedSupplyChainTradeTransaction']/*[local-name() = 'ApplicableSupplyChainTradeAgreement']//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'PostalTradeAddress']");
|
||||
} else {
|
||||
nl = getNodeListByPath("//*[local-name() = 'CrossIndustryInvoice']//*[local-name() = 'SupplyChainTradeTransaction']//*[local-name() = 'ApplicableHeaderTradeAgreement']//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'PostalTradeAddress']");
|
||||
}
|
||||
@@ -947,7 +724,7 @@ public class ZUGFeRDImporter {
|
||||
if (node != null) {
|
||||
final NodeList tradeAgreementChildren = node.getChildNodes();
|
||||
node = getNodeByName(tradeAgreementChildren, "ChargeAmount");
|
||||
lineItem.setPrice(tryBigDecimal(getNodeValue(node)));
|
||||
lineItem.setPrice(XMLTools.tryBigDecimal(node));
|
||||
node = getNodeByName(tradeAgreementChildren, "BasisQuantity");
|
||||
if (node != null && node.getAttributes() != null) {
|
||||
final Node unitCodeAttribute = node.getAttributes().getNamedItem("unitCode");
|
||||
@@ -960,14 +737,14 @@ public class ZUGFeRDImporter {
|
||||
node = getNodeByName(nn.getChildNodes(), "GrossPriceProductTradePrice");
|
||||
if (node != null) {
|
||||
node = getNodeByName(node.getChildNodes(), "ChargeAmount");
|
||||
lineItem.setGrossPrice(tryBigDecimal(getNodeValue(node)));
|
||||
lineItem.setGrossPrice(XMLTools.tryBigDecimal(node));
|
||||
}
|
||||
break;
|
||||
|
||||
case "AssociatedDocumentLineDocument":
|
||||
|
||||
node = getNodeByName(nn.getChildNodes(), "LineID");
|
||||
lineItem.setId(getNodeValue(node));
|
||||
lineItem.setId(XMLTools.getNodeValue(node));
|
||||
break;
|
||||
|
||||
case "SpecifiedTradeProduct":
|
||||
@@ -980,35 +757,35 @@ public class ZUGFeRDImporter {
|
||||
lineItem.getProduct().addGlobalID(globalId);
|
||||
}
|
||||
node = getNodeByName(nn.getChildNodes(), "SellerAssignedID");
|
||||
lineItem.getProduct().setSellerAssignedID(getNodeValue(node));
|
||||
lineItem.getProduct().setSellerAssignedID(XMLTools.getNodeValue(node));
|
||||
|
||||
node = getNodeByName(nn.getChildNodes(), "BuyerAssignedID");
|
||||
lineItem.getProduct().setBuyerAssignedID(getNodeValue(node));
|
||||
lineItem.getProduct().setBuyerAssignedID(XMLTools.getNodeValue(node));
|
||||
|
||||
node = getNodeByName(nn.getChildNodes(), "Name");
|
||||
lineItem.getProduct().setName(getNodeValue(node));
|
||||
lineItem.getProduct().setName(XMLTools.getNodeValue(node));
|
||||
|
||||
node = getNodeByName(nn.getChildNodes(), "Description");
|
||||
lineItem.getProduct().setDescription(getNodeValue(node));
|
||||
lineItem.getProduct().setDescription(XMLTools.getNodeValue(node));
|
||||
break;
|
||||
|
||||
case "SpecifiedLineTradeDelivery":
|
||||
case "SpecifiedSupplyChainTradeDelivery":
|
||||
node = getNodeByName(nn.getChildNodes(), "BilledQuantity");
|
||||
lineItem.setQuantity(tryBigDecimal(getNodeValue(node)));
|
||||
lineItem.setQuantity(XMLTools.tryBigDecimal(node));
|
||||
break;
|
||||
|
||||
case "SpecifiedLineTradeSettlement":
|
||||
node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax");
|
||||
if (node != null) {
|
||||
node = getNodeByName(node.getChildNodes(), "RateApplicablePercent");
|
||||
lineItem.getProduct().setVATPercent(tryBigDecimal(getNodeValue(node)));
|
||||
lineItem.getProduct().setVATPercent(XMLTools.tryBigDecimal(node));
|
||||
}
|
||||
|
||||
node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax");
|
||||
if (node != null) {
|
||||
node = getNodeByName(node.getChildNodes(), "CalculatedAmount");
|
||||
lineItem.setTax(tryBigDecimal(getNodeValue(node)));
|
||||
lineItem.setTax(XMLTools.tryBigDecimal(node));
|
||||
}
|
||||
node = getNodeByName(nn.getChildNodes(), "BillingSpecifiedPeriod");
|
||||
if (node != null) {
|
||||
@@ -1022,13 +799,13 @@ public class ZUGFeRDImporter {
|
||||
if (end != null) {
|
||||
dateTimeEnd = getNodeByName(end.getChildNodes(), "DateTimeString");
|
||||
}
|
||||
lineItem.setDetailedDeliveryPeriod(tryDate(dateTimeStart), tryDate(dateTimeEnd));
|
||||
lineItem.setDetailedDeliveryPeriod(XMLTools.tryDate(dateTimeStart), XMLTools.tryDate(dateTimeEnd));
|
||||
}
|
||||
|
||||
node = getNodeByName(nn.getChildNodes(), "SpecifiedTradeSettlementLineMonetarySummation");
|
||||
if (node != null) {
|
||||
node = getNodeByName(node.getChildNodes(), "LineTotalAmount");
|
||||
lineItem.setLineTotalAmount(tryBigDecimal(getNodeValue(node)));
|
||||
lineItem.setLineTotalAmount(XMLTools.tryBigDecimal(node));
|
||||
}
|
||||
break;
|
||||
case "SpecifiedSupplyChainTradeSettlement":
|
||||
@@ -1037,19 +814,19 @@ public class ZUGFeRDImporter {
|
||||
node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax");
|
||||
if (node != null) {
|
||||
node = getNodeByName(node.getChildNodes(), "ApplicablePercent");
|
||||
lineItem.getProduct().setVATPercent(tryBigDecimal(getNodeValue(node)));
|
||||
lineItem.getProduct().setVATPercent(XMLTools.tryBigDecimal(node));
|
||||
}
|
||||
|
||||
node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax");
|
||||
if (node != null) {
|
||||
node = getNodeByName(node.getChildNodes(), "CalculatedAmount");
|
||||
lineItem.setTax(tryBigDecimal(getNodeValue(node)));
|
||||
lineItem.setTax(XMLTools.tryBigDecimal(node));
|
||||
}
|
||||
|
||||
node = getNodeByName(nn.getChildNodes(), "SpecifiedTradeSettlementMonetarySummation");
|
||||
if (node != null) {
|
||||
node = getNodeByName(node.getChildNodes(), "LineTotalAmount");
|
||||
lineItem.setLineTotalAmount(tryBigDecimal(getNodeValue(node)));
|
||||
lineItem.setLineTotalAmount(XMLTools.tryBigDecimal(node));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1124,51 +901,4 @@ public class ZUGFeRDImporter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the value of an node
|
||||
*
|
||||
* @param node the Node to get the value from
|
||||
* @return A String or empty String, if no value was found
|
||||
*/
|
||||
private String getNodeValue(Node node) {
|
||||
if (node != null && node.getFirstChild() != null) {
|
||||
return node.getFirstChild().getNodeValue();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* tries to convert an String to BigDecimal.
|
||||
*
|
||||
* @param nodeValue The value as String
|
||||
* @return a BigDecimal with the value provides as String or a BigDecimal with value 0.00 if an error occurs
|
||||
*/
|
||||
private BigDecimal tryBigDecimal(String nodeValue) {
|
||||
try {
|
||||
return new BigDecimal(nodeValue);
|
||||
} catch (final Exception e) {
|
||||
try {
|
||||
return BigDecimal.valueOf(Float.valueOf(nodeValue));
|
||||
} catch (final Exception ex) {
|
||||
return new BigDecimal("0.00");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Date tryDate(Node node) {
|
||||
final String nodeValue = getNodeValue(node);
|
||||
if (nodeValue.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return tryDate(nodeValue);
|
||||
}
|
||||
|
||||
private static Date tryDate(String toParse) {
|
||||
final SimpleDateFormat formatter = ZUGFeRDDateFormat.DATE.getFormatter();
|
||||
try {
|
||||
return formatter.parse(toParse);
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,292 @@
|
||||
package org.mustangproject.ZUGFeRD;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpression;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
|
||||
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
|
||||
import org.apache.pdfbox.pdmodel.common.PDNameTreeNode;
|
||||
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
|
||||
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
|
||||
import org.mustangproject.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.*;
|
||||
import java.io.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class ZUGFeRDInvoiceImporter {
|
||||
|
||||
public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRDInvoiceImporter.class.getCanonicalName()); // log
|
||||
private boolean recalcPrice = false;
|
||||
private boolean ignoreCalculationErrors = false;
|
||||
private ArrayList<FileAttachment> fileAttachments=new ArrayList<>();
|
||||
/**
|
||||
* map filenames of additional XML files to their contents
|
||||
*/
|
||||
protected final HashMap<String, byte[]> additionalXMLs = new HashMap<>();
|
||||
/**
|
||||
* map filenames of all embedded files in the respective PDF
|
||||
*/
|
||||
protected final ArrayList<FileAttachment> PDFAttachments = new ArrayList<>();
|
||||
/**
|
||||
* if metadata has been found
|
||||
*/
|
||||
protected boolean containsMeta = false;
|
||||
/**
|
||||
* Raw XML form of the extracted data - may be directly obtained.
|
||||
*/
|
||||
protected byte[] rawXML = null;
|
||||
/**
|
||||
* XMP metadata
|
||||
*/
|
||||
protected String xmpString = null; // XMP metadata
|
||||
/**
|
||||
* parsed Document
|
||||
*/
|
||||
protected Document document;
|
||||
/***
|
||||
* automatically parse into importedInvoice
|
||||
*/
|
||||
protected boolean parseAutomatically = true;
|
||||
protected Integer version;
|
||||
protected CalculatedInvoice importedInvoice = null;
|
||||
protected boolean recalcPrice = false;
|
||||
protected boolean ignoreCalculationErrors = false;
|
||||
protected ArrayList<FileAttachment> fileAttachments = new ArrayList<>();
|
||||
|
||||
public ZUGFeRDInvoiceImporter() {
|
||||
super();
|
||||
//constructor for extending classes
|
||||
}
|
||||
|
||||
public ZUGFeRDInvoiceImporter(String filename) {
|
||||
super(filename);
|
||||
public ZUGFeRDInvoiceImporter(String pdfFilename) {
|
||||
setPDFFilename(pdfFilename);
|
||||
}
|
||||
|
||||
public ZUGFeRDInvoiceImporter(InputStream stream) {
|
||||
super(stream);
|
||||
public ZUGFeRDInvoiceImporter(InputStream pdfStream) {
|
||||
setInputStream(pdfStream);
|
||||
}
|
||||
|
||||
public void fromXML(String XML) {
|
||||
private static String trimOrNull(Node node) {
|
||||
if (node != null) {
|
||||
String textContent = node.getTextContent();
|
||||
if (textContent != null) {
|
||||
return textContent.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String convertStreamToString(java.io.InputStream is) {
|
||||
try {
|
||||
containsMeta = true;
|
||||
setRawXML(XML.getBytes(StandardCharsets.UTF_8));
|
||||
return IOUtils.toString(is, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPDFFilename(String pdfFilename) {
|
||||
try (InputStream bis = Files.newInputStream(Paths.get(pdfFilename), StandardOpenOption.READ)) {
|
||||
extractLowLevel(bis);
|
||||
} catch (final IOException e) {
|
||||
LOGGER.error("Failed to extract ZUGFeRD data", e);
|
||||
throw new ZUGFeRDExportException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setInputStream(InputStream pdfStream) {
|
||||
try {
|
||||
extractLowLevel(pdfStream);
|
||||
} catch (final IOException e) {
|
||||
LOGGER.error("Failed to extract ZUGFeRD data", e);
|
||||
throw new ZUGFeRDExportException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* return the file names of all files embedded into the PDF
|
||||
* @see for XML embedded files please use ZUGFeRDInvoiceImporter.getFileAttachmentsXML
|
||||
* @return a ArrayList of FileAttachments, empty if none
|
||||
*/
|
||||
public List<FileAttachment> getFileAttachmentsPDF() {
|
||||
return PDFAttachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a ZUGFeRD invoice from a PDF document represented by an input stream. Errors are reported via exception handling.
|
||||
*
|
||||
* @param inStream a inputstream of a pdf file
|
||||
*/
|
||||
private void extractLowLevel(InputStream inStream) throws IOException {
|
||||
BufferedInputStream pdfStream = new BufferedInputStream(inStream);
|
||||
byte[] pad = new byte[4];
|
||||
pdfStream.mark(0);
|
||||
pdfStream.read(pad);
|
||||
pdfStream.reset();
|
||||
byte[] pdfSignature = {'%', 'P', 'D', 'F'};
|
||||
if (Arrays.equals(pad, pdfSignature)) { // we have a pdf
|
||||
|
||||
|
||||
try {
|
||||
PDDocument doc = Loader.loadPDF(IOUtils.toByteArray(pdfStream));
|
||||
// PDDocumentInformation info = doc.getDocumentInformation();
|
||||
final PDDocumentNameDictionary names = new PDDocumentNameDictionary(doc.getDocumentCatalog());
|
||||
//start
|
||||
|
||||
if (doc.getDocumentCatalog() == null || doc.getDocumentCatalog().getMetadata() == null) {
|
||||
LOGGER.info("no-xmlpart");
|
||||
return;
|
||||
}
|
||||
|
||||
final InputStream XMP = doc.getDocumentCatalog().getMetadata().exportXMPMetadata();
|
||||
xmpString = convertStreamToString(XMP);
|
||||
|
||||
final PDEmbeddedFilesNameTreeNode etn = names.getEmbeddedFiles();
|
||||
if (etn == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, PDComplexFileSpecification> efMap = etn.getNames();
|
||||
// String filePath = "/tmp/";
|
||||
|
||||
if (efMap != null) {
|
||||
extractFiles(efMap); // see
|
||||
// https://memorynotfound.com/apache-pdfbox-extract-embedded-file-pdf-document/
|
||||
} else {
|
||||
|
||||
final List<PDNameTreeNode<PDComplexFileSpecification>> kids = etn.getKids();
|
||||
if (kids == null) {
|
||||
return;
|
||||
}
|
||||
for (final PDNameTreeNode<PDComplexFileSpecification> node : kids) {
|
||||
final Map<String, PDComplexFileSpecification> namesL = node.getNames();
|
||||
extractFiles(namesL);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to parse PDF", e);
|
||||
//ignore otherwise
|
||||
}
|
||||
} else {
|
||||
// no PDF probably XML
|
||||
containsMeta = true;
|
||||
setRawXML(XMLTools.getBytesFromStream(pdfStream));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* have the item prices be determined from the line total.
|
||||
* That's a workaround for some invoices which just put 0 as item price
|
||||
*/
|
||||
public void doRecalculateItemPricesFromLineTotals() {
|
||||
recalcPrice = true;
|
||||
}
|
||||
|
||||
/***
|
||||
* do not raise ParseExceptions even if the reproduced invoice total does not match the given value
|
||||
*/
|
||||
public void doIgnoreCalculationErrors() {
|
||||
ignoreCalculationErrors = true;
|
||||
}
|
||||
|
||||
/***
|
||||
* sets th pdf attachments, and if a file is recognized (e.g. a factur-x.xml) triggers processing
|
||||
* @param names the Hashmap of String, PDComplexFileSpecification
|
||||
* @throws IOException
|
||||
*/
|
||||
private void extractFiles(Map<String, PDComplexFileSpecification> names) throws IOException {
|
||||
for (final String alias : names.keySet()) {
|
||||
|
||||
final PDComplexFileSpecification fileSpec = names.get(alias);
|
||||
final String filename = fileSpec.getFilename();
|
||||
/**
|
||||
* filenames for invoice data (ZUGFeRD v1 and v2, Factur-X)
|
||||
*/
|
||||
|
||||
final PDEmbeddedFile embeddedFile = fileSpec.getEmbeddedFile();
|
||||
if ((filename.equals("ZUGFeRD-invoice.xml") || (filename.equals("zugferd-invoice.xml")) || filename.equals("factur-x.xml")) || filename.equals("xrechnung.xml") || filename.equals("order-x.xml") || filename.equals("cida.xml")) {
|
||||
containsMeta = true;
|
||||
|
||||
// String embeddedFilename = filePath + filename;
|
||||
// File file = new File(filePath + filename);
|
||||
// System.out.println("Writing " + embeddedFilename);
|
||||
// ByteArrayOutputStream fileBytes=new
|
||||
// ByteArrayOutputStream();
|
||||
// FileOutputStream fos = new FileOutputStream(file);
|
||||
|
||||
setRawXML(embeddedFile.toByteArray());
|
||||
|
||||
// fos.write(embeddedFile.getByteArray());
|
||||
// fos.close();
|
||||
}
|
||||
if (filename.startsWith("additional_data")) {
|
||||
additionalXMLs.put(filename, embeddedFile.toByteArray());
|
||||
}
|
||||
PDFAttachments.add(new FileAttachment(filename, embeddedFile.getSubtype(), "Data", embeddedFile.toByteArray()));
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* set the xml of a CII invoice
|
||||
* @param rawXML the xml string
|
||||
* @param doParse automatically parse input for zugferdImporter (not ZUGFeRDInvoiceImporter)
|
||||
* @throws IOException
|
||||
*/
|
||||
public void setRawXML(byte[] rawXML, boolean doParse) throws IOException {
|
||||
this.containsMeta = true;
|
||||
this.rawXML = rawXML;
|
||||
this.version = null;
|
||||
parseAutomatically = doParse;
|
||||
|
||||
try {
|
||||
setDocument();
|
||||
} catch (ParserConfigurationException | SAXException e) {
|
||||
LOGGER.error("Failed to parse XML", e);
|
||||
throw new ZUGFeRDExportException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/***
|
||||
* set the xml of a CII invoice, simple version
|
||||
* @param rawXML the cii(?) as a string
|
||||
* @throws IOException
|
||||
*/
|
||||
public void setRawXML(byte[] rawXML) throws IOException {
|
||||
setRawXML(rawXML, true);
|
||||
}
|
||||
|
||||
private void setDocument() throws ParserConfigurationException, IOException, SAXException {
|
||||
final DocumentBuilderFactory xmlFact = DocumentBuilderFactory.newInstance();
|
||||
xmlFact.setNamespaceAware(true);
|
||||
final DocumentBuilder builder = xmlFact.newDocumentBuilder();
|
||||
final ByteArrayInputStream is = new ByteArrayInputStream(rawXML);
|
||||
/// is.skip(guessBOMSize(is));
|
||||
document = builder.parse(is);
|
||||
if (parseAutomatically) {
|
||||
try {
|
||||
importedInvoice = new CalculatedInvoice();
|
||||
extractInto(importedInvoice);
|
||||
} catch (XPathExpressionException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* This will parse a XML into the given invoice object
|
||||
@@ -63,6 +299,8 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
|
||||
String number = "";
|
||||
String typeCode = null;
|
||||
String deliveryPeriodStart = null;
|
||||
String deliveryPeriodEnd = null;
|
||||
/*
|
||||
* dummywerte sind derzeit noch setDueDate setIssueDate setDeliveryDate
|
||||
* setSender setRecipient setnumber bspw. due date
|
||||
@@ -72,6 +310,12 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
XPath xpath = xpathFact.newXPath();
|
||||
XPathExpression xpr = xpath.compile("//*[local-name()=\"SellerTradeParty\"]|//*[local-name()=\"AccountingSupplierParty\"]/*");
|
||||
NodeList SellerNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
|
||||
XPathExpression shipEx = xpath.compile("//*[local-name()=\"ShipToTradeParty\"]");
|
||||
NodeList deliveryNodes = (NodeList) shipEx.evaluate(getDocument(), XPathConstants.NODESET);
|
||||
if (deliveryNodes != null) {
|
||||
zpp.setDeliveryAddress(new TradeParty(deliveryNodes));
|
||||
}
|
||||
|
||||
|
||||
xpr = xpath.compile("//*[local-name()=\"BuyerTradeParty\"]|//*[local-name()=\"AccountingCustomerParty\"]/*");
|
||||
NodeList BuyerNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
|
||||
@@ -86,13 +330,19 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
BigDecimal expectedGrandTotal = null;
|
||||
NodeList totalNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
|
||||
if (totalNodes.getLength() > 0) {
|
||||
expectedGrandTotal = new BigDecimal(totalNodes.item(0).getTextContent());
|
||||
expectedGrandTotal = new BigDecimal(trimOrNull(totalNodes.item(0)));
|
||||
if (zpp instanceof CalculatedInvoice) {
|
||||
// usually we would re-calculate the invoice to get expectedGrandTotal
|
||||
// however, for "minimal" invoices or other invoices without lines
|
||||
// this will not work
|
||||
((CalculatedInvoice) zpp).setGrandTotal(expectedGrandTotal);
|
||||
}
|
||||
}
|
||||
|
||||
xpr = xpath.compile("//*[local-name()=\"PrepaidAmount\"]");
|
||||
NodeList prepaidNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
|
||||
if (prepaidNodes.getLength() > 0) {
|
||||
zpp.setTotalPrepaidAmount(new BigDecimal(prepaidNodes.item(0).getTextContent()));
|
||||
zpp.setTotalPrepaidAmount(new BigDecimal(trimOrNull(prepaidNodes.item(0))));
|
||||
}
|
||||
|
||||
Date issueDate = null;
|
||||
@@ -100,24 +350,22 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
Date deliveryDate = null;
|
||||
String despatchAdviceReferencedDocument = null;
|
||||
for (int i = 0; i < ExchangedDocumentNodes.getLength(); i++) {
|
||||
|
||||
// nodes.item(i).getTextContent())) {
|
||||
Node exchangedDocumentNode = ExchangedDocumentNodes.item(i);
|
||||
NodeList exchangedDocumentChilds = exchangedDocumentNode.getChildNodes();
|
||||
for (int documentChildIndex = 0; documentChildIndex < exchangedDocumentChilds.getLength(); documentChildIndex++) {
|
||||
Node item = exchangedDocumentChilds.item(documentChildIndex);
|
||||
if ((item.getLocalName() != null) && (item.getLocalName().equals("ID"))) {
|
||||
number = item.getTextContent();
|
||||
number = trimOrNull(item);
|
||||
}
|
||||
if ((item.getLocalName() != null) && (item.getLocalName().equals("TypeCode"))) {
|
||||
typeCode = item.getTextContent();
|
||||
typeCode = trimOrNull(item);
|
||||
}
|
||||
if ((item.getLocalName() != null) && (item.getLocalName().equals("IssueDateTime"))) {
|
||||
NodeList issueDateTimeChilds = item.getChildNodes();
|
||||
for (int issueDateChildIndex = 0; issueDateChildIndex < issueDateTimeChilds.getLength(); issueDateChildIndex++) {
|
||||
if ((issueDateTimeChilds.item(issueDateChildIndex).getLocalName() != null)
|
||||
&& (issueDateTimeChilds.item(issueDateChildIndex).getLocalName().equals("DateTimeString"))) {
|
||||
issueDate = new SimpleDateFormat("yyyyMMdd").parse(issueDateTimeChilds.item(issueDateChildIndex).getTextContent());
|
||||
issueDate = new SimpleDateFormat("yyyyMMdd").parse(trimOrNull(issueDateTimeChilds.item(issueDateChildIndex)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +389,6 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
NodeList headerTradeDeliveryNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
|
||||
|
||||
for (int i = 0; i < headerTradeDeliveryNodes.getLength(); i++) {
|
||||
// nodes.item(i).getTextContent())) {
|
||||
Node headerTradeDeliveryNode = headerTradeDeliveryNodes.item(i);
|
||||
NodeList headerTradeDeliveryChilds = headerTradeDeliveryNode.getChildNodes();
|
||||
for (int deliveryChildIndex = 0; deliveryChildIndex < headerTradeDeliveryChilds.getLength(); deliveryChildIndex++) {
|
||||
@@ -155,7 +402,7 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
for (int occurenceChildIndex = 0; occurenceChildIndex < occurenceChilds.getLength(); occurenceChildIndex++) {
|
||||
if ((occurenceChilds.item(occurenceChildIndex).getLocalName() != null)
|
||||
&& (occurenceChilds.item(occurenceChildIndex).getLocalName().equals("DateTimeString"))) {
|
||||
deliveryDate = new SimpleDateFormat("yyyyMMdd").parse(occurenceChilds.item(occurenceChildIndex).getTextContent());
|
||||
deliveryDate = new SimpleDateFormat("yyyyMMdd").parse(trimOrNull(occurenceChilds.item(occurenceChildIndex)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +414,7 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
for (int despatchAdviceChildIndex = 0; despatchAdviceChildIndex < despatchAdviceChilds.getLength(); despatchAdviceChildIndex++) {
|
||||
if (despatchAdviceChilds.item(despatchAdviceChildIndex).getLocalName() != null
|
||||
&& despatchAdviceChilds.item(despatchAdviceChildIndex).getLocalName().equals("IssuerAssignedID")) {
|
||||
despatchAdviceReferencedDocument = despatchAdviceChilds.item(despatchAdviceChildIndex).getTextContent();
|
||||
despatchAdviceReferencedDocument = trimOrNull(despatchAdviceChilds.item(despatchAdviceChildIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,7 +427,7 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
String buyerOrderIssuerAssignedID = null;
|
||||
String sellerOrderIssuerAssignedID = null;
|
||||
for (int i = 0; i < headerTradeAgreementNodes.getLength(); i++) {
|
||||
// nodes.item(i).getTextContent())) {
|
||||
// trimOrNull(nodes.item(i)))) {
|
||||
Node headerTradeAgreementNode = headerTradeAgreementNodes.item(i);
|
||||
NodeList headerTradeAgreementChilds = headerTradeAgreementNode.getChildNodes();
|
||||
for (int agreementChildIndex = 0; agreementChildIndex < headerTradeAgreementChilds.getLength(); agreementChildIndex++) {
|
||||
@@ -190,7 +437,7 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
for (int buyerOrderChildIndex = 0; buyerOrderChildIndex < buyerOrderChilds.getLength(); buyerOrderChildIndex++) {
|
||||
if ((buyerOrderChilds.item(buyerOrderChildIndex).getLocalName() != null)
|
||||
&& (buyerOrderChilds.item(buyerOrderChildIndex).getLocalName().equals("IssuerAssignedID"))) {
|
||||
buyerOrderIssuerAssignedID = buyerOrderChilds.item(buyerOrderChildIndex).getTextContent();
|
||||
buyerOrderIssuerAssignedID = trimOrNull(buyerOrderChilds.item(buyerOrderChildIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +447,7 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
for (int sellerOrderChildIndex = 0; sellerOrderChildIndex < sellerOrderChilds.getLength(); sellerOrderChildIndex++) {
|
||||
if ((sellerOrderChilds.item(sellerOrderChildIndex).getLocalName() != null)
|
||||
&& (sellerOrderChilds.item(sellerOrderChildIndex).getLocalName().equals("IssuerAssignedID"))) {
|
||||
sellerOrderIssuerAssignedID = sellerOrderChilds.item(sellerOrderChildIndex).getTextContent();
|
||||
sellerOrderIssuerAssignedID = trimOrNull(sellerOrderChilds.item(sellerOrderChildIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,14 +456,19 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
|
||||
}
|
||||
|
||||
String currency = extractString("//*[local-name()=\"ApplicableHeaderTradeSettlement\"]/*[local-name()=\"InvoiceCurrencyCode\"]|*[local-name()=\"DocumentCurrencyCode\"]");
|
||||
zpp.setCurrency(currency);
|
||||
|
||||
xpr = xpath.compile("//*[local-name()=\"ApplicableHeaderTradeSettlement\"]|//*[local-name()=\"ApplicableSupplyChainTradeSettlement\"]");
|
||||
NodeList headerTradeSettlementNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
|
||||
List<BankDetails> bankDetails = new ArrayList<>();
|
||||
String directDebitMandateID = null;
|
||||
String IBAN = null, BIC = null;
|
||||
|
||||
for (int i = 0; i < headerTradeSettlementNodes.getLength(); i++) {
|
||||
// nodes.item(i).getTextContent())) {
|
||||
// trimOrNull(nodes.item(i)))) {
|
||||
Node headerTradeSettlementNode = headerTradeSettlementNodes.item(i);
|
||||
|
||||
NodeList headerTradeSettlementChilds = headerTradeSettlementNode.getChildNodes();
|
||||
for (int settlementChildIndex = 0; settlementChildIndex < headerTradeSettlementChilds.getLength(); settlementChildIndex++) {
|
||||
if ((headerTradeSettlementChilds.item(settlementChildIndex).getLocalName() != null)
|
||||
@@ -227,23 +479,28 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
NodeList dueDateChilds = paymentTermChilds.item(paymentTermChildIndex).getChildNodes();
|
||||
for (int dueDateChildIndex = 0; dueDateChildIndex < dueDateChilds.getLength(); dueDateChildIndex++) {
|
||||
if ((dueDateChilds.item(dueDateChildIndex).getLocalName() != null) && (dueDateChilds.item(dueDateChildIndex).getLocalName().equals("DateTimeString"))) {
|
||||
dueDate = new SimpleDateFormat("yyyyMMdd").parse(dueDateChilds.item(dueDateChildIndex).getTextContent());
|
||||
dueDate = new SimpleDateFormat("yyyyMMdd").parse(trimOrNull(dueDateChilds.item(dueDateChildIndex)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((paymentTermChilds.item(paymentTermChildIndex).getLocalName() != null) && (paymentTermChilds.item(paymentTermChildIndex).getLocalName().equals("DirectDebitMandateID"))) {
|
||||
directDebitMandateID = paymentTermChilds.item(paymentTermChildIndex).getTextContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((headerTradeSettlementChilds.item(settlementChildIndex).getLocalName() != null)
|
||||
&& (headerTradeSettlementChilds.item(settlementChildIndex).getLocalName().equals("SpecifiedTradeSettlementPaymentMeans"))) {
|
||||
NodeList paymentMeansChilds = headerTradeSettlementChilds.item(settlementChildIndex).getChildNodes();
|
||||
String IBAN = null, BIC = null;
|
||||
IBAN = null;
|
||||
BIC = null;
|
||||
for (int paymentMeansChildIndex = 0; paymentMeansChildIndex < paymentMeansChilds.getLength(); paymentMeansChildIndex++) {
|
||||
if ((paymentMeansChilds.item(paymentMeansChildIndex).getLocalName() != null) && (paymentMeansChilds.item(paymentMeansChildIndex).getLocalName().equals("PayeePartyCreditorFinancialAccount"))) {
|
||||
|
||||
if ((paymentMeansChilds.item(paymentMeansChildIndex).getLocalName() != null) && (paymentMeansChilds.item(paymentMeansChildIndex).getLocalName().equals("PayeePartyCreditorFinancialAccount") || paymentMeansChilds.item(paymentMeansChildIndex).getLocalName().equals("PayerPartyDebtorFinancialAccount"))) {
|
||||
NodeList accountChilds = paymentMeansChilds.item(paymentMeansChildIndex).getChildNodes();
|
||||
for (int accountChildIndex = 0; accountChildIndex < accountChilds.getLength(); accountChildIndex++) {
|
||||
if ((accountChilds.item(accountChildIndex).getLocalName() != null) && (accountChilds.item(accountChildIndex).getLocalName().equals("IBANID"))) {//CII
|
||||
IBAN = accountChilds.item(accountChildIndex).getTextContent();
|
||||
IBAN = trimOrNull(accountChilds.item(accountChildIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,7 +508,7 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
NodeList accountChilds = paymentMeansChilds.item(paymentMeansChildIndex).getChildNodes();
|
||||
for (int accountChildIndex = 0; accountChildIndex < accountChilds.getLength(); accountChildIndex++) {
|
||||
if ((accountChilds.item(accountChildIndex).getLocalName() != null) && (accountChilds.item(accountChildIndex).getLocalName().equals("BICID"))) {//CII
|
||||
BIC = accountChilds.item(accountChildIndex).getTextContent();
|
||||
BIC = trimOrNull(accountChilds.item(accountChildIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,15 +522,43 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
bankDetails.add(bd);
|
||||
}
|
||||
}
|
||||
if ((headerTradeSettlementChilds.item(settlementChildIndex).getLocalName() != null)
|
||||
&& (headerTradeSettlementChilds.item(settlementChildIndex).getLocalName().equals("BillingSpecifiedPeriod"))) {
|
||||
NodeList periodChilds = headerTradeSettlementChilds.item(settlementChildIndex).getChildNodes();
|
||||
for (int periodChildIndex = 0; periodChildIndex < periodChilds.getLength(); periodChildIndex++) {
|
||||
if ((periodChilds.item(periodChildIndex).getLocalName() != null) && (periodChilds.item(periodChildIndex).getLocalName().equals("StartDateTime"))) {
|
||||
|
||||
NodeList startPeriodChilds = periodChilds.item(periodChildIndex).getChildNodes();
|
||||
for (int startPeriodIndex = 0; startPeriodIndex < startPeriodChilds.getLength(); startPeriodIndex++) {
|
||||
if ((startPeriodChilds.item(startPeriodIndex).getLocalName() != null) && (startPeriodChilds.item(startPeriodIndex).getLocalName().equals("DateTimeString"))) {//CII
|
||||
deliveryPeriodStart = trimOrNull(startPeriodChilds.item(startPeriodIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((periodChilds.item(periodChildIndex).getLocalName() != null) && (periodChilds.item(periodChildIndex).getLocalName().equals("EndDateTime"))) {
|
||||
NodeList endPeriodChilds = periodChilds.item(periodChildIndex).getChildNodes();
|
||||
for (int endPeriodIndex = 0; endPeriodIndex < endPeriodChilds.getLength(); endPeriodIndex++) {
|
||||
if ((endPeriodChilds.item(endPeriodIndex).getLocalName() != null) && (endPeriodChilds.item(endPeriodIndex).getLocalName().equals("DateTimeString"))) {//CII
|
||||
deliveryPeriodEnd = trimOrNull(endPeriodChilds.item(endPeriodIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((deliveryPeriodStart != null) && (deliveryPeriodEnd != null)) {
|
||||
zpp.setDetailedDeliveryPeriod(XMLTools.tryDate(deliveryPeriodStart), XMLTools.tryDate(deliveryPeriodEnd));
|
||||
} else if (deliveryPeriodStart != null) {
|
||||
zpp.setDeliveryDate(XMLTools.tryDate(deliveryPeriodStart));
|
||||
}
|
||||
|
||||
xpr = xpath.compile("//*[local-name()=\"PaymentMeans\"]"); //UBL only
|
||||
NodeList paymentMeansNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
|
||||
|
||||
for (int i = 0; i < paymentMeansNodes.getLength(); i++) {
|
||||
// nodes.item(i).getTextContent())) {
|
||||
// trimOrNull(nodes.item(i)))) {
|
||||
Node paymentMeansNode = paymentMeansNodes.item(i);
|
||||
NodeList paymentMeansChilds = paymentMeansNode.getChildNodes();
|
||||
for (int meansChildIndex = 0; meansChildIndex < paymentMeansChilds.getLength(); meansChildIndex++) {
|
||||
@@ -282,7 +567,7 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
NodeList paymentTermChilds = paymentMeansChilds.item(meansChildIndex).getChildNodes();
|
||||
for (int paymentTermChildIndex = 0; paymentTermChildIndex < paymentTermChilds.getLength(); paymentTermChildIndex++) {
|
||||
if ((paymentTermChilds.item(paymentTermChildIndex).getLocalName() != null) && (paymentTermChilds.item(paymentTermChildIndex).getLocalName().equals("ID"))) {
|
||||
String IBAN = paymentTermChilds.item(paymentTermChildIndex).getTextContent();
|
||||
IBAN = trimOrNull(paymentTermChilds.item(paymentTermChildIndex));
|
||||
if (IBAN != null) {
|
||||
BankDetails bd = new BankDetails(IBAN);
|
||||
bankDetails.add(bd);
|
||||
@@ -295,6 +580,11 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
|
||||
zpp.setDueDate(dueDate).setDeliveryDate(deliveryDate).setIssueDate(issueDate).setSender(new TradeParty(SellerNodes)).setRecipient(new TradeParty(BuyerNodes)).setNumber(number).setDocumentCode(typeCode);
|
||||
|
||||
if ((directDebitMandateID != null) && (IBAN != null)) {
|
||||
DirectDebit d = new DirectDebit(IBAN, directDebitMandateID);
|
||||
zpp.getSender().addDebitDetails(d);
|
||||
}
|
||||
|
||||
bankDetails.forEach(bankDetail -> zpp.getSender().addBankDetails(bankDetail));
|
||||
|
||||
if (payeeNodes.getLength() > 0) {
|
||||
@@ -317,7 +607,7 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
String buyerReference = null;
|
||||
prepaidNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
|
||||
if (prepaidNodes.getLength() > 0) {
|
||||
buyerReference = prepaidNodes.item(0).getTextContent();
|
||||
buyerReference = trimOrNull(prepaidNodes.item(0));
|
||||
}
|
||||
if (buyerReference != null) {
|
||||
zpp.setReferenceNumber(buyerReference);
|
||||
@@ -339,9 +629,9 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
xpr = xpath.compile("//*[local-name()=\"AttachmentBinaryObject\"]|//*[local-name()=\"EmbeddedDocumentBinaryObject\"]");
|
||||
NodeList attachmentNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
|
||||
for (int i = 0; i < attachmentNodes.getLength(); i++) {
|
||||
FileAttachment fa=new FileAttachment(attachmentNodes.item(i).getAttributes().getNamedItem("filename").getNodeValue(),attachmentNodes.item(i).getAttributes().getNamedItem("mimeCode").getNodeValue(),"Data", Base64.getDecoder().decode(attachmentNodes.item(i).getTextContent()));
|
||||
FileAttachment fa = new FileAttachment(attachmentNodes.item(i).getAttributes().getNamedItem("filename").getNodeValue(), attachmentNodes.item(i).getAttributes().getNamedItem("mimeCode").getNodeValue(), "Data", Base64.getDecoder().decode(trimOrNull(attachmentNodes.item(i))));
|
||||
fileAttachments.add(fa);
|
||||
// filename = "Aufmass.png" mimeCode = "image/png"
|
||||
// filename = "Aufmass.png" mimeCode = "image/png"
|
||||
//EmbeddedDocumentBinaryObject cbc:EmbeddedDocumentBinaryObject mimeCode="image/png" filename="Aufmass.png"
|
||||
}
|
||||
|
||||
@@ -367,21 +657,21 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
for (int indicatorChildIndex = 0; indicatorChildIndex < indicatorChilds.getLength(); indicatorChildIndex++) {
|
||||
if ((indicatorChilds.item(indicatorChildIndex).getLocalName() != null)
|
||||
&& (indicatorChilds.item(indicatorChildIndex).getLocalName().equals("Indicator"))) {
|
||||
isCharge = indicatorChilds.item(indicatorChildIndex).getTextContent().equalsIgnoreCase("true");
|
||||
isCharge = trimOrNull(indicatorChilds.item(indicatorChildIndex)).equalsIgnoreCase("true");
|
||||
}
|
||||
}
|
||||
} else if (chargeChildName.equals("ActualAmount")) {
|
||||
chargeAmount = chargeNodeChilds.item(chargeChildIndex).getTextContent();
|
||||
chargeAmount = trimOrNull(chargeNodeChilds.item(chargeChildIndex));
|
||||
} else if (chargeChildName.equals("Reason")) {
|
||||
reason = chargeNodeChilds.item(chargeChildIndex).getTextContent();
|
||||
reason = trimOrNull(chargeNodeChilds.item(chargeChildIndex));
|
||||
} else if (chargeChildName.equals("ReasonCode")) {
|
||||
reasonCode = chargeNodeChilds.item(chargeChildIndex).getTextContent();
|
||||
reasonCode = trimOrNull(chargeNodeChilds.item(chargeChildIndex));
|
||||
} else if (chargeChildName.equals("CategoryTradeTax")) {
|
||||
NodeList taxChilds = chargeNodeChilds.item(chargeChildIndex).getChildNodes();
|
||||
for (int taxChildIndex = 0; taxChildIndex < taxChilds.getLength(); taxChildIndex++) {
|
||||
String taxItemName = taxChilds.item(taxChildIndex).getLocalName();
|
||||
if ((taxItemName != null) && (taxItemName.equals("RateApplicablePercent") || taxItemName.equals("ApplicablePercent"))) {
|
||||
taxPercent = taxChilds.item(taxChildIndex).getTextContent();
|
||||
taxPercent = trimOrNull(taxChilds.item(taxChildIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,13 +721,86 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
&& ((!expectedStringTotalGross.equals(XMLTools.nDigitFormat(expectedGrandTotal, 2)))
|
||||
&& (!ignoreCalculationErrors))) {
|
||||
throw new ParseException(
|
||||
"Could not reproduce the invoice, this could mean that it could not be read properly", 0);
|
||||
"Could not reproduce the invoice, this could mean that it could not be read properly exp "+expectedStringTotalGross+" is "+XMLTools.nDigitFormat(expectedGrandTotal, 2), 0);
|
||||
}
|
||||
}
|
||||
return zpp;
|
||||
|
||||
}
|
||||
|
||||
protected Document getDocument() {
|
||||
return document;
|
||||
}
|
||||
|
||||
protected String extractString(String xpathStr) {
|
||||
if (!containsMeta) {
|
||||
throw new ZUGFeRDExportException("No suitable data/ZUGFeRD file could be found.");
|
||||
}
|
||||
final String result;
|
||||
try {
|
||||
final Document document = getDocument();
|
||||
final XPathFactory xpathFact = XPathFactory.newInstance();
|
||||
final XPath xpath = xpathFact.newXPath();
|
||||
result = xpath.evaluate(xpathStr, document);
|
||||
} catch (final XPathExpressionException e) {
|
||||
LOGGER.error("Failed to evaluate XPath", e);
|
||||
throw new ZUGFeRDExportException(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public EStandard getStandard() throws Exception {
|
||||
if (!containsMeta) {
|
||||
throw new Exception("Not yet parsed");
|
||||
}
|
||||
final String head = getUTF8();
|
||||
String rootNode = extractString("local-name(/*)");
|
||||
if (rootNode.equals("CrossIndustryDocument")) {
|
||||
return EStandard.zugferd;
|
||||
} else if (rootNode.equals("Invoice")) {
|
||||
return EStandard.ubl;
|
||||
} else if (rootNode.equals("CreditNote")) {
|
||||
return EStandard.ubl;
|
||||
} else if (rootNode.equals("CrossIndustryInvoice")) {
|
||||
return EStandard.facturx;
|
||||
} else if (rootNode.equals("SCRDMCCBDACIDAMessageStructure")) {
|
||||
return EStandard.despatchadvice;
|
||||
} else if (head.contains("<rsm:SCRDMCCBDACIOMessageStructure")) {
|
||||
return EStandard.orderx;
|
||||
}
|
||||
|
||||
throw new Exception("ZUGFeRD version could not be determined");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return return UTF8 XML (without BOM) of the invoice
|
||||
*/
|
||||
public String getUTF8() {
|
||||
if (rawXML == null) {
|
||||
return null;
|
||||
}
|
||||
if (rawXML.length < 3) {
|
||||
return new String(rawXML);
|
||||
}
|
||||
|
||||
|
||||
final byte[] bomlessData;
|
||||
|
||||
if ((rawXML[0] == (byte) 0xEF)
|
||||
&& (rawXML[1] == (byte) 0xBB)
|
||||
&& (rawXML[2] == (byte) 0xBF)) {
|
||||
// I don't like BOMs, lets remove it
|
||||
bomlessData = new byte[rawXML.length - 3];
|
||||
System.arraycopy(rawXML, 3, bomlessData, 0,
|
||||
rawXML.length - 3);
|
||||
} else {
|
||||
bomlessData = rawXML;
|
||||
}
|
||||
|
||||
return new String(bomlessData);
|
||||
}
|
||||
|
||||
/***
|
||||
*
|
||||
* @return the file attachments embedded in XML (using base64) decoded as byte array,
|
||||
@@ -461,18 +824,18 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
|
||||
|
||||
}
|
||||
|
||||
/***
|
||||
* have the item prices be determined from the line total.
|
||||
* That's a workaround for some invoices which just put 0 as item price
|
||||
*/
|
||||
public void doRecalculateItemPricesFromLineTotals() {
|
||||
recalcPrice = true;
|
||||
}
|
||||
|
||||
/***
|
||||
* do not raise ParseExceptions even if the reproduced invoice total does not match the given value
|
||||
* sets the XML for the importer to parse
|
||||
* @param XML the UBL or CII
|
||||
*/
|
||||
public void doIgnoreCalculationErrors() {
|
||||
ignoreCalculationErrors = true;
|
||||
public void fromXML(String XML) {
|
||||
try {
|
||||
containsMeta = true;
|
||||
setRawXML(XML.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user