Merge branch 'issues/435-importer-ubl'

# Conflicts:
#	History.md
This commit is contained in:
jstaerk
2024-10-15 13:23:09 +02:00
10 changed files with 660 additions and 468 deletions

View File

@@ -1,3 +1,10 @@
2.15.0
=======
2024-
- 435 use invoiceimporter as common technical basis also for zugferdimporter
- also import delivery address
2.14.2 2.14.2
======= =======
2024-10-14 2024-10-14
@@ -7,7 +14,6 @@
- #505 crash after invoking ZUGFeRD2PullProvider - #505 crash after invoking ZUGFeRD2PullProvider
- #506 Fix POM missing dependencies - #506 Fix POM missing dependencies
2.14.1 2.14.1
======= =======
2024-10-06 2024-10-06

View File

@@ -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;
}
}

View File

@@ -4,9 +4,13 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.dom4j.io.XMLWriter; import org.dom4j.io.XMLWriter;
import org.mustangproject.ZUGFeRD.ZUGFeRDDateFormat;
import org.w3c.dom.Node;
public class XMLTools extends XMLWriter { public class XMLTools extends XMLWriter {
@Override @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. * 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 * 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 &lt;
* @param s the string
* @return the "safe" string
*/
public static String encodeXML(CharSequence s) {
if (s == null) { if (s == null) {
return ""; return "";
} }

View File

@@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
@@ -33,6 +34,7 @@ import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory; import javax.xml.xpath.XPathFactory;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.fop.util.XMLUtil;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
@@ -48,57 +50,19 @@ import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
public class ZUGFeRDImporter { public class ZUGFeRDImporter extends ZUGFeRDInvoiceImporter {
private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRDImporter.class); private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRDImporter.class);
/** public ZUGFeRDImporter() {
* if metadata has been found super();
*/
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(String pdfFilename) { public ZUGFeRDImporter(String filename) {
try (InputStream bis = Files.newInputStream(Paths.get(pdfFilename), StandardOpenOption.READ)) { super(filename);
extractLowLevel(bis);
} catch (final IOException e) {
LOGGER.error("Failed to extract ZUGFeRD data", e);
throw new ZUGFeRDExportException(e);
}
} }
public ZUGFeRDImporter(InputStream stream) {
public ZUGFeRDImporter(InputStream pdfStream) { super(stream);
try {
extractLowLevel(pdfStream);
} catch (final IOException e) {
LOGGER.error("Failed to extract ZUGFeRD data", e);
throw new ZUGFeRDExportException(e);
}
} }
@@ -112,145 +76,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 * Wrapper for protected method extractString
* @param xpathStr the xpath expression to be evaluated * @param xpathStr the xpath expression to be evaluated
@@ -261,21 +86,21 @@ public class ZUGFeRDImporter {
} }
////////////////////////////////////
/** /**
* @return the reference (purpose) the sender specified for this invoice * @return the reference (purpose) the sender specified for this invoice
*/ */
public String getForeignReference() { public String getForeignReference() {
String result = extractString("//*[local-name() = 'ApplicableHeaderTradeSettlement']/*[local-name() = 'PaymentReference']");
if (result == null || result.isEmpty()) { return importedInvoice.getNumber();
result = extractString("//*[local-name() = 'ApplicableSupplyChainTradeSettlement']/*[local-name() = 'PaymentReference']");
}
return result;
} }
/** /**
* @return the ZUGFeRD Profile * @return the ZUGFeRD Profile
*/ */
public String getZUGFeRDProfil() { public String getZUGFeRDProfil() {
String guideline = extractString("//*[local-name() = 'GuidelineSpecifiedDocumentContextParameter']//*[local-name() = 'ID']"); String guideline = extractString("//*[local-name() = 'GuidelineSpecifiedDocumentContextParameter']//*[local-name() = 'ID']");
if (guideline.contains("xrechnung")) { if (guideline.contains("xrechnung")) {
return "XRECHNUNG"; return "XRECHNUNG";
@@ -299,21 +124,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 * @return the IssuerAssigned ID
@@ -336,57 +146,6 @@ public class ZUGFeRDImporter {
return extractIssuerAssignedID("ContractReferencedDocument"); 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 * @return the TaxBasisTotalAmount
@@ -470,7 +229,16 @@ public class ZUGFeRDImporter {
* @return the BuyerTradeParty SpecifiedTaxRegistration ID * @return the BuyerTradeParty SpecifiedTaxRegistration ID
*/ */
public String getBuyertradePartySpecifiedTaxRegistrationID() { 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;
} }
@@ -494,14 +262,14 @@ public class ZUGFeRDImporter {
* @return the BuyerTradeParty Name * @return the BuyerTradeParty Name
*/ */
public String getBuyerTradePartyName() { public String getBuyerTradePartyName() {
return extractString("//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'Name']"); return importedInvoice.getRecipient().getName();
} }
/** /**
* @return the BuyerTradeParty Name * @return the BuyerTradeParty Name
*/ */
public String getDeliveryTradePartyName() { public String getDeliveryTradePartyName() {
return extractString("//*[local-name() = 'ShipToTradeParty']//*[local-name() = 'Name']"); return importedInvoice.getDeliveryAddress().getName();
} }
@@ -548,16 +316,7 @@ public class ZUGFeRDImporter {
* @return the Invoice ID * @return the Invoice ID
*/ */
public String getInvoiceID() { public String getInvoiceID() {
try { return importedInvoice.getNumber();
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 "";
}
} }
@@ -615,11 +374,21 @@ public class ZUGFeRDImporter {
* @return the sender's account IBAN code * @return the sender's account IBAN code
*/ */
public String getIBAN() { 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() { public String getHolder() {
return extractString("//*[local-name() = 'SellerTradeParty']/*[local-name() = 'Name']"); return extractString("//*[local-name() = 'SellerTradeParty']/*[local-name() = 'Name']");
} }
@@ -628,14 +397,8 @@ public class ZUGFeRDImporter {
* @return the total payable amount * @return the total payable amount
*/ */
public String getAmount() { public String getAmount() {
String result = extractString("//*[local-name() = 'SpecifiedTradeSettlementHeaderMonetarySummation']/*[local-name() = 'DuePayableAmount']");
if (result == null || result.isEmpty()) {
/* fx/zf would be SpecifiedTradeSettlementMonetarySummation return importedInvoice.getGrandTotal().toPlainString();
* but ox is SpecifiedTradeSettlementHeaderMonetarySummation...*/
result = extractString("//*[local-name() = 'GrandTotalAmount']");
}
return result;
} }
@@ -643,7 +406,60 @@ public class ZUGFeRDImporter {
* @return when the payment is due * @return when the payment is due
*/ */
public String getDueDate() { 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();
} }
@@ -691,28 +507,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 { public int getVersion() throws Exception {
if (!containsMeta) { if (!containsMeta) {
throw new Exception("Not yet parsed"); throw new Exception("Not yet parsed");
@@ -736,35 +530,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. * Returns the raw XML data as extracted from the ZUGFeRD PDF file.
* *
@@ -790,14 +555,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 * returns an instance of PostalTradeAddress for SellerTradeParty section
* *
@@ -809,7 +566,7 @@ public class ZUGFeRDImporter {
try { try {
if (getVersion() == 1) { 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 { } else {
nl = getNodeListByPath("//*[local-name() = 'CrossIndustryInvoice']//*[local-name() = 'SupplyChainTradeTransaction']//*[local-name() = 'ApplicableHeaderTradeAgreement']//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'PostalTradeAddress']"); nl = getNodeListByPath("//*[local-name() = 'CrossIndustryInvoice']//*[local-name() = 'SupplyChainTradeTransaction']//*[local-name() = 'ApplicableHeaderTradeAgreement']//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'PostalTradeAddress']");
} }
@@ -953,7 +710,7 @@ public class ZUGFeRDImporter {
if (node != null) { if (node != null) {
final NodeList tradeAgreementChildren = node.getChildNodes(); final NodeList tradeAgreementChildren = node.getChildNodes();
node = getNodeByName(tradeAgreementChildren, "ChargeAmount"); node = getNodeByName(tradeAgreementChildren, "ChargeAmount");
lineItem.setPrice(tryBigDecimal(getNodeValue(node))); lineItem.setPrice(XMLTools.tryBigDecimal(node));
node = getNodeByName(tradeAgreementChildren, "BasisQuantity"); node = getNodeByName(tradeAgreementChildren, "BasisQuantity");
if (node != null && node.getAttributes() != null) { if (node != null && node.getAttributes() != null) {
final Node unitCodeAttribute = node.getAttributes().getNamedItem("unitCode"); final Node unitCodeAttribute = node.getAttributes().getNamedItem("unitCode");
@@ -966,48 +723,48 @@ public class ZUGFeRDImporter {
node = getNodeByName(nn.getChildNodes(), "GrossPriceProductTradePrice"); node = getNodeByName(nn.getChildNodes(), "GrossPriceProductTradePrice");
if (node != null) { if (node != null) {
node = getNodeByName(node.getChildNodes(), "ChargeAmount"); node = getNodeByName(node.getChildNodes(), "ChargeAmount");
lineItem.setGrossPrice(tryBigDecimal(getNodeValue(node))); lineItem.setGrossPrice(XMLTools.tryBigDecimal(node));
} }
break; break;
case "AssociatedDocumentLineDocument": case "AssociatedDocumentLineDocument":
node = getNodeByName(nn.getChildNodes(), "LineID"); node = getNodeByName(nn.getChildNodes(), "LineID");
lineItem.setId(getNodeValue(node)); lineItem.setId(XMLTools.getNodeValue(node));
break; break;
case "SpecifiedTradeProduct": case "SpecifiedTradeProduct":
node = getNodeByName(nn.getChildNodes(), "SellerAssignedID"); node = getNodeByName(nn.getChildNodes(), "SellerAssignedID");
lineItem.getProduct().setSellerAssignedID(getNodeValue(node)); lineItem.getProduct().setSellerAssignedID(XMLTools.getNodeValue(node));
node = getNodeByName(nn.getChildNodes(), "BuyerAssignedID"); node = getNodeByName(nn.getChildNodes(), "BuyerAssignedID");
lineItem.getProduct().setBuyerAssignedID(getNodeValue(node)); lineItem.getProduct().setBuyerAssignedID(XMLTools.getNodeValue(node));
node = getNodeByName(nn.getChildNodes(), "Name"); node = getNodeByName(nn.getChildNodes(), "Name");
lineItem.getProduct().setName(getNodeValue(node)); lineItem.getProduct().setName(XMLTools.getNodeValue(node));
node = getNodeByName(nn.getChildNodes(), "Description"); node = getNodeByName(nn.getChildNodes(), "Description");
lineItem.getProduct().setDescription(getNodeValue(node)); lineItem.getProduct().setDescription(XMLTools.getNodeValue(node));
break; break;
case "SpecifiedLineTradeDelivery": case "SpecifiedLineTradeDelivery":
case "SpecifiedSupplyChainTradeDelivery": case "SpecifiedSupplyChainTradeDelivery":
node = getNodeByName(nn.getChildNodes(), "BilledQuantity"); node = getNodeByName(nn.getChildNodes(), "BilledQuantity");
lineItem.setQuantity(tryBigDecimal(getNodeValue(node))); lineItem.setQuantity(XMLTools.tryBigDecimal(node));
break; break;
case "SpecifiedLineTradeSettlement": case "SpecifiedLineTradeSettlement":
node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax"); node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax");
if (node != null) { if (node != null) {
node = getNodeByName(node.getChildNodes(), "RateApplicablePercent"); node = getNodeByName(node.getChildNodes(), "RateApplicablePercent");
lineItem.getProduct().setVATPercent(tryBigDecimal(getNodeValue(node))); lineItem.getProduct().setVATPercent(XMLTools.tryBigDecimal(node));
} }
node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax"); node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax");
if (node != null) { if (node != null) {
node = getNodeByName(node.getChildNodes(), "CalculatedAmount"); node = getNodeByName(node.getChildNodes(), "CalculatedAmount");
lineItem.setTax(tryBigDecimal(getNodeValue(node))); lineItem.setTax(XMLTools.tryBigDecimal(node));
} }
node = getNodeByName(nn.getChildNodes(), "BillingSpecifiedPeriod"); node = getNodeByName(nn.getChildNodes(), "BillingSpecifiedPeriod");
if (node != null) { if (node != null) {
@@ -1021,13 +778,13 @@ public class ZUGFeRDImporter {
if (end != null) { if (end != null) {
dateTimeEnd = getNodeByName(end.getChildNodes(), "DateTimeString"); dateTimeEnd = getNodeByName(end.getChildNodes(), "DateTimeString");
} }
lineItem.setDetailedDeliveryPeriod(tryDate(dateTimeStart), tryDate(dateTimeEnd)); lineItem.setDetailedDeliveryPeriod(XMLTools.tryDate(dateTimeStart), XMLTools.tryDate(dateTimeEnd));
} }
node = getNodeByName(nn.getChildNodes(), "SpecifiedTradeSettlementLineMonetarySummation"); node = getNodeByName(nn.getChildNodes(), "SpecifiedTradeSettlementLineMonetarySummation");
if (node != null) { if (node != null) {
node = getNodeByName(node.getChildNodes(), "LineTotalAmount"); node = getNodeByName(node.getChildNodes(), "LineTotalAmount");
lineItem.setLineTotalAmount(tryBigDecimal(getNodeValue(node))); lineItem.setLineTotalAmount(XMLTools.tryBigDecimal(node));
} }
break; break;
case "SpecifiedSupplyChainTradeSettlement": case "SpecifiedSupplyChainTradeSettlement":
@@ -1036,19 +793,19 @@ public class ZUGFeRDImporter {
node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax"); node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax");
if (node != null) { if (node != null) {
node = getNodeByName(node.getChildNodes(), "ApplicablePercent"); node = getNodeByName(node.getChildNodes(), "ApplicablePercent");
lineItem.getProduct().setVATPercent(tryBigDecimal(getNodeValue(node))); lineItem.getProduct().setVATPercent(XMLTools.tryBigDecimal(node));
} }
node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax"); node = getNodeByName(nn.getChildNodes(), "ApplicableTradeTax");
if (node != null) { if (node != null) {
node = getNodeByName(node.getChildNodes(), "CalculatedAmount"); node = getNodeByName(node.getChildNodes(), "CalculatedAmount");
lineItem.setTax(tryBigDecimal(getNodeValue(node))); lineItem.setTax(XMLTools.tryBigDecimal(node));
} }
node = getNodeByName(nn.getChildNodes(), "SpecifiedTradeSettlementMonetarySummation"); node = getNodeByName(nn.getChildNodes(), "SpecifiedTradeSettlementMonetarySummation");
if (node != null) { if (node != null) {
node = getNodeByName(node.getChildNodes(), "LineTotalAmount"); node = getNodeByName(node.getChildNodes(), "LineTotalAmount");
lineItem.setLineTotalAmount(tryBigDecimal(getNodeValue(node))); lineItem.setLineTotalAmount(XMLTools.tryBigDecimal(node));
} }
break; break;
} }
@@ -1123,51 +880,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;
}
}
} }

View File

@@ -1,9 +1,11 @@
package org.mustangproject.ZUGFeRD; package org.mustangproject.ZUGFeRD;
import java.io.IOException; import java.io.*;
import java.io.InputStream;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; 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.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@@ -11,43 +13,272 @@ import java.util.Base64;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
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.XPath;
import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory; 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.mustangproject.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node; import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter { public class ZUGFeRDInvoiceImporter {
private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRDInvoiceImporter.class.getCanonicalName()); // log 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<>();
public ZUGFeRDInvoiceImporter() { /**
super(); * if metadata has been found
*/
protected boolean containsMeta = false;
/**
* 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<>();
/**
* 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<>();
protected ZUGFeRDInvoiceImporter() {
//constructor for extending classes
} }
public ZUGFeRDInvoiceImporter(String filename) { public ZUGFeRDInvoiceImporter(String pdfFilename) {
super(filename); setPDFFilename(pdfFilename);
} }
public ZUGFeRDInvoiceImporter(InputStream stream) { public ZUGFeRDInvoiceImporter(InputStream pdfStream) {
super(stream); setInputStream(pdfStream);
} }
public void fromXML(String XML) { 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 { 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);
}
}
}
} else {
// no PDF probably XML
containsMeta = true; containsMeta = true;
setRawXML(XML.getBytes(StandardCharsets.UTF_8)); setRawXML(XMLTools.getBytesFromStream(pdfStream));
} catch (IOException e) {
LOGGER.error(e.getMessage(), e); }
}
/***
* 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);
}
} }
} }
@@ -63,6 +294,8 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
String number = ""; String number = "";
String typeCode = null; String typeCode = null;
String deliveryPeriodStart = null;
String deliveryPeriodEnd = null;
/* /*
* dummywerte sind derzeit noch setDueDate setIssueDate setDeliveryDate * dummywerte sind derzeit noch setDueDate setIssueDate setDeliveryDate
* setSender setRecipient setnumber bspw. due date * setSender setRecipient setnumber bspw. due date
@@ -72,6 +305,12 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
XPath xpath = xpathFact.newXPath(); XPath xpath = xpathFact.newXPath();
XPathExpression xpr = xpath.compile("//*[local-name()=\"SellerTradeParty\"]|//*[local-name()=\"AccountingSupplierParty\"]/*"); XPathExpression xpr = xpath.compile("//*[local-name()=\"SellerTradeParty\"]|//*[local-name()=\"AccountingSupplierParty\"]/*");
NodeList SellerNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); 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\"]/*"); xpr = xpath.compile("//*[local-name()=\"BuyerTradeParty\"]|//*[local-name()=\"AccountingCustomerParty\"]/*");
NodeList BuyerNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); NodeList BuyerNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
@@ -87,6 +326,12 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
NodeList totalNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); NodeList totalNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
if (totalNodes.getLength() > 0) { if (totalNodes.getLength() > 0) {
expectedGrandTotal = new BigDecimal(totalNodes.item(0).getTextContent()); expectedGrandTotal = new BigDecimal(totalNodes.item(0).getTextContent());
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\"]"); xpr = xpath.compile("//*[local-name()=\"PrepaidAmount\"]");
@@ -209,6 +454,8 @@ 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\"]"); xpr = xpath.compile("//*[local-name()=\"ApplicableHeaderTradeSettlement\"]|//*[local-name()=\"ApplicableSupplyChainTradeSettlement\"]");
NodeList headerTradeSettlementNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); NodeList headerTradeSettlementNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
@@ -265,9 +512,37 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
bankDetails.add(bd); 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 = startPeriodChilds.item(startPeriodIndex).getTextContent();
}
}
}
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 = endPeriodChilds.item(endPeriodIndex).getTextContent();
}
}
}
}
}
} }
} }
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 xpr = xpath.compile("//*[local-name()=\"PaymentMeans\"]"); //UBL only
NodeList paymentMeansNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); NodeList paymentMeansNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
@@ -339,9 +614,9 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
xpr = xpath.compile("//*[local-name()=\"AttachmentBinaryObject\"]|//*[local-name()=\"EmbeddedDocumentBinaryObject\"]"); xpr = xpath.compile("//*[local-name()=\"AttachmentBinaryObject\"]|//*[local-name()=\"EmbeddedDocumentBinaryObject\"]");
NodeList attachmentNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); NodeList attachmentNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
for (int i = 0; i < attachmentNodes.getLength(); i++) { 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(attachmentNodes.item(i).getTextContent()));
fileAttachments.add(fa); fileAttachments.add(fa);
// filename = "Aufmass.png" mimeCode = "image/png" // filename = "Aufmass.png" mimeCode = "image/png"
//EmbeddedDocumentBinaryObject cbc:EmbeddedDocumentBinaryObject mimeCode="image/png" filename="Aufmass.png" //EmbeddedDocumentBinaryObject cbc:EmbeddedDocumentBinaryObject mimeCode="image/png" filename="Aufmass.png"
} }
@@ -438,6 +713,90 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter {
} }
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);
}
static String convertStreamToString(java.io.InputStream is) {
try {
return IOUtils.toString(is, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/*** /***
* *
* @return the file attachments embedded in XML (using base64) decoded as byte array, * @return the file attachments embedded in XML (using base64) decoded as byte array,
@@ -461,18 +820,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() { public void fromXML(String XML) {
ignoreCalculationErrors = true; try {
containsMeta = true;
setRawXML(XML.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
LOGGER.error(e.getMessage(), e);
}
} }
} }

View File

@@ -214,8 +214,10 @@ public class MustangReaderWriterTest extends MustangReaderTestCase {
public void testForeignImport() { public void testForeignImport() {
InputStream inputStream = this.getClass().getResourceAsStream("/zugferd_invoice.pdf"); InputStream inputStream = this.getClass().getResourceAsStream("/zugferd_invoice.pdf");
ZUGFeRDImporter zi = new ZUGFeRDImporter(inputStream); ZUGFeRDImporter zi = new ZUGFeRDImporter();
zi.doRecalculateItemPricesFromLineTotals();
zi.doIgnoreCalculationErrors();
zi.setInputStream(inputStream);
// Reading ZUGFeRD // Reading ZUGFeRD
String amount = zi.getAmount(); String amount = zi.getAmount();

View File

@@ -137,7 +137,7 @@ public class ProfilesMinimumBasicWLTest extends TestCase {
ZUGFeRDImporter zi = new ZUGFeRDImporter(TARGET_PDF_FX_MINIMUM_INV); ZUGFeRDImporter zi = new ZUGFeRDImporter(TARGET_PDF_FX_MINIMUM_INV);
// Reading ZUGFeRD // Reading ZUGFeRD
assertEquals("145.37",zi.getAmount()); assertEquals("146.37",zi.getAmount());
// assertEquals(zi.getBIC(), ownBIC); // assertEquals(zi.getBIC(), ownBIC);
// assertEquals(zi.getIBAN(), ownIBAN); // assertEquals(zi.getIBAN(), ownIBAN);
assertEquals(ownOrgName, zi.getHolder()); assertEquals(ownOrgName, zi.getHolder());

View File

@@ -129,13 +129,12 @@ public class XRTest extends TestCase {
Invoice readInvoice = new Invoice(); Invoice readInvoice = new Invoice();
ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter(); ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter();
try { try {
zii.setRawXML(zf2p.getXML(), false);
zii.setRawXML(zf2p.getXML());
zii.extractInto(readInvoice); zii.extractInto(readInvoice);
} catch (ParseException | XPathExpressionException xp) { } catch (ParseException | XPathExpressionException xp) {
fail("Exception not expected"); fail("ParseException not expected");
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); fail("IOException not expected");
} }
List<FileAttachment> attachedFiles=zii.getFileAttachmentsXML(); List<FileAttachment> attachedFiles=zii.getFileAttachmentsXML();
assertNotNull(attachedFiles); assertNotNull(attachedFiles);

View File

@@ -1,22 +1,24 @@
/** ********************************************************************** /**
* * *********************************************************************
* <p>
* Copyright 2019 Jochen Staerk * Copyright 2019 Jochen Staerk
* * <p>
* Use is subject to license terms. * Use is subject to license terms.
* * <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy * use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0. * of the License at http://www.apache.org/licenses/LICENSE-2.0.
* * <p>
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * <p>
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
* * <p>
*********************************************************************** */ * **********************************************************************
*/
package org.mustangproject.ZUGFeRD; package org.mustangproject.ZUGFeRD;
import java.io.IOException; import java.io.IOException;
@@ -99,9 +101,9 @@ public class ZF2Test extends MustangReaderTestCase {
public IZUGFeRDExportableItem[] getZFItems() { public IZUGFeRDExportableItem[] getZFItems() {
final Item[] allItems = new Item[3]; final Item[] allItems = new Item[3];
final Product designProduct = new Product("", "Künstlerische Gestaltung (Stunde): Einer Beispielrechnung", "HUR", final Product designProduct = new Product("", "Künstlerische Gestaltung (Stunde): Einer Beispielrechnung", "HUR",
new BigDecimal("7.000000")); new BigDecimal("7.000000"));
final Product balloonProduct = new Product("", "Bestellerweiterung für E&F Umbau", "C62", final Product balloonProduct = new Product("", "Bestellerweiterung für E&F Umbau", "C62",
new BigDecimal("19.000000"));// test for issue 103 new BigDecimal("19.000000"));// test for issue 103
final Product airProduct = new Product("", "Heiße Luft pro Liter", "LTR", new BigDecimal("19.000000")); final Product airProduct = new Product("", "Heiße Luft pro Liter", "LTR", new BigDecimal("19.000000"));
allItems[0] = new Item(new BigDecimal("160"), new BigDecimal("1"), designProduct); allItems[0] = new Item(new BigDecimal("160"), new BigDecimal("1"), designProduct);
@@ -166,11 +168,11 @@ public class ZF2Test extends MustangReaderTestCase {
// the writing part // the writing part
try (InputStream SOURCE_PDF = this.getClass() try (InputStream SOURCE_PDF = this.getClass()
.getResourceAsStream("/MustangGnuaccountingBeispielRE-20170509_505PDFA3.pdf"); .getResourceAsStream("/MustangGnuaccountingBeispielRE-20170509_505PDFA3.pdf");
ZUGFeRDExporterFromA3 ze = new ZUGFeRDExporterFromA3().setProducer("My Application") ZUGFeRDExporterFromA3 ze = new ZUGFeRDExporterFromA3().setProducer("My Application")
.setCreator(System.getProperty("user.name")).setZUGFeRDVersion(2).setProfile("EN16931") .setCreator(System.getProperty("user.name")).setZUGFeRDVersion(2).setProfile("EN16931")
.load(SOURCE_PDF)) { .load(SOURCE_PDF)) {
ze.setTransaction(this); ze.setTransaction(this);
final String theXML = new String(ze.getProvider().getXML()); final String theXML = new String(ze.getProvider().getXML());
@@ -190,13 +192,13 @@ public class ZF2Test extends MustangReaderTestCase {
assertEquals(zi.getInvoiceID(), "RE-20170509/505"); assertEquals(zi.getInvoiceID(), "RE-20170509/505");
assertEquals(zi.getZUGFeRDProfil(), "COMFORT"); assertEquals(zi.getZUGFeRDProfil(), "COMFORT");
assertEquals(zi.getInvoiceCurrencyCode(), "EUR"); assertEquals(zi.getInvoiceCurrencyCode(), "EUR");
assertEquals(zi.getIssuerAssignedID(),""); assertEquals(zi.getIssuerAssignedID(), "");
assertEquals(zi.getIssueDate(), "20170509"); assertEquals(zi.getIssueDate(), "20170509");
assertEquals(zi.getTaxPointDate(), "20170507"); assertEquals(zi.getTaxPointDate(), "20170507");
assertEquals(zi.getPaymentTerms(), "Zahlbar ohne Abzug bis zum 30.05.2017"); assertEquals(zi.getPaymentTerms(), "Zahlbar ohne Abzug bis zum 30.05.2017");
assertEquals(zi.getLineTotalAmount(), "496.00"); assertEquals(zi.getLineTotalAmount(), "496.00");
assertEquals(zi.getTaxBasisTotalAmount(), "496.00"); assertEquals(zi.getTaxBasisTotalAmount(), "496.00");
assertEquals(zi.getTaxTotalAmount(),"75.04"); assertEquals(zi.getTaxTotalAmount(), "75.04");
assertEquals(zi.getRoundingAmount(), ""); assertEquals(zi.getRoundingAmount(), "");
assertEquals(zi.getPaidAmount(), "0.00"); assertEquals(zi.getPaidAmount(), "0.00");
assertEquals(zi.getBuyerTradePartyName(), "Theodor Est"); assertEquals(zi.getBuyerTradePartyName(), "Theodor Est");
@@ -206,8 +208,8 @@ public class ZF2Test extends MustangReaderTestCase {
assertEquals(zi.getBuyertradePartySpecifiedTaxRegistrationID(), "DE999999999"); assertEquals(zi.getBuyertradePartySpecifiedTaxRegistrationID(), "DE999999999");
assertEquals(zi.getIncludedNote(), ""); assertEquals(zi.getIncludedNote(), "");
assertEquals(zi.getHolder(), getOwnOrganisationName()); assertEquals(zi.getHolder(), getOwnOrganisationName());
assertEquals(zi.getDocumentCode(),"380"); assertEquals(zi.getDocumentCode(), "380");
assertEquals(zi.getReference(),"AB321"); assertEquals(zi.getReference(), "AB321");
assertEquals(zi.getAmount(), "571.04"); assertEquals(zi.getAmount(), "571.04");
assertEquals(zi.getBIC(), "COBADEFFXXX"); assertEquals(zi.getBIC(), "COBADEFFXXX");
assertEquals(zi.getIBAN(), "DE88 2008 0000 0970 3757 00"); assertEquals(zi.getIBAN(), "DE88 2008 0000 0970 3757 00");
@@ -244,7 +246,9 @@ public class ZF2Test extends MustangReaderTestCase {
// TODO Auto-generated catch block // TODO Auto-generated catch block
e.printStackTrace(); e.printStackTrace();
} }
} /** }
/**
* The exporter test bases on @{code * The exporter test bases on @{code
* ./src/test/MustangBeispiel20221026.pdf}, adds * ./src/test/MustangBeispiel20221026.pdf}, adds
* metadata, writes to @{code ./target/testout-*} and then imports to check the * metadata, writes to @{code ./target/testout-*} and then imports to check the
@@ -264,7 +268,7 @@ public class ZF2Test extends MustangReaderTestCase {
assertEquals("Innerhalb von 30 Tagen 2% Skonto, 60 Tage ohne Abzug", zi.getPaymentTerms()); assertEquals("Innerhalb von 30 Tagen 2% Skonto, 60 Tage ohne Abzug", zi.getPaymentTerms());
assertEquals("804.35", zi.getLineTotalAmount()); assertEquals("804.35", zi.getLineTotalAmount());
assertEquals("809.34", zi.getTaxBasisTotalAmount()); assertEquals("809.34", zi.getTaxBasisTotalAmount());
assertEquals("153.77",zi.getTaxTotalAmount()); assertEquals("153.77", zi.getTaxTotalAmount());
assertEquals("", zi.getRoundingAmount()); assertEquals("", zi.getRoundingAmount());
assertEquals("0.00", zi.getPaidAmount()); assertEquals("0.00", zi.getPaidAmount());
assertEquals("Beispiel AG", zi.getBuyerTradePartyName()); assertEquals("Beispiel AG", zi.getBuyerTradePartyName());
@@ -273,8 +277,8 @@ public class ZF2Test extends MustangReaderTestCase {
assertEquals("10000", zi.getBuyerTradePartyID()); assertEquals("10000", zi.getBuyerTradePartyID());
assertEquals("\n weclapp.com\nSomestreet 42\n08155 Some city\nDE\n ", zi.getIncludedNote()); assertEquals("\n weclapp.com\nSomestreet 42\n08155 Some city\nDE\n ", zi.getIncludedNote());
assertEquals("weclapp.com", zi.getHolder()); assertEquals("weclapp.com", zi.getHolder());
assertEquals("380",zi.getDocumentCode()); assertEquals("380", zi.getDocumentCode());
assertEquals("01-95",zi.getReference()); assertEquals("01-95", zi.getReference());
assertEquals("RE1001", zi.getForeignReference()); assertEquals("RE1001", zi.getForeignReference());
assertEquals("54321", zi.getBuyerTradePartyAddress().getPostcodeCode()); assertEquals("54321", zi.getBuyerTradePartyAddress().getPostcodeCode());
assertEquals("Feldstraße 34", zi.getBuyerTradePartyAddress().getLineOne()); assertEquals("Feldstraße 34", zi.getBuyerTradePartyAddress().getLineOne());
@@ -284,13 +288,13 @@ public class ZF2Test extends MustangReaderTestCase {
assertEquals("DE", zi.getBuyerTradePartyAddress().getCountryID()); assertEquals("DE", zi.getBuyerTradePartyAddress().getCountryID());
assertEquals("Hithausen", zi.getBuyerTradePartyAddress().getCityName()); assertEquals("Hithausen", zi.getBuyerTradePartyAddress().getCityName());
assertEquals("Beispiel Lager AG", zi.getDeliveryTradePartyName()); assertEquals("Beispiel Lager AG", zi.getDeliveryTradePartyName());
assertEquals("54321", zi.getDeliveryTradePartyAddress().getPostcodeCode()); assertEquals("54321", zi.getDeliveryTradePartyAddress().getPostcodeCode());
assertEquals("Feldstraße 39", zi.getDeliveryTradePartyAddress().getLineOne()); assertEquals("Feldstraße 39", zi.getDeliveryTradePartyAddress().getLineOne());
assertEquals(null, zi.getDeliveryTradePartyAddress().getLineTwo()); assertEquals(null, zi.getDeliveryTradePartyAddress().getLineTwo());
assertEquals(null, zi.getDeliveryTradePartyAddress().getLineThree()); assertEquals(null, zi.getDeliveryTradePartyAddress().getLineThree());
assertEquals(null, zi.getDeliveryTradePartyAddress().getCountrySubDivisionName()); assertEquals(null, zi.getDeliveryTradePartyAddress().getCountrySubDivisionName());
assertEquals("DE", zi.getDeliveryTradePartyAddress().getCountryID()); assertEquals("DE", zi.getDeliveryTradePartyAddress().getCountryID());
assertEquals("Hithausen", zi.getDeliveryTradePartyAddress().getCityName()); assertEquals("Hithausen", zi.getDeliveryTradePartyAddress().getCityName());
assertEquals("08155", zi.getSellerTradePartyAddress().getPostcodeCode()); assertEquals("08155", zi.getSellerTradePartyAddress().getPostcodeCode());
assertEquals("Somestreet 42", zi.getSellerTradePartyAddress().getLineOne()); assertEquals("Somestreet 42", zi.getSellerTradePartyAddress().getLineOne());
assertEquals(null, zi.getSellerTradePartyAddress().getLineTwo()); assertEquals(null, zi.getSellerTradePartyAddress().getLineTwo());