package com.alexanderlogistics.einvoice; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.logging.Logger; import javax.xml.transform.TransformerException; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; import org.imixs.einvoice.EInvoiceFormatException; import org.imixs.einvoice.EInvoiceModel; import org.imixs.einvoice.EInvoiceModelCII; import org.imixs.einvoice.EInvoiceModelFactory; import org.imixs.einvoice.TradeLineItem; import org.imixs.einvoice.TradeParty; import org.imixs.workflow.FileData; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.SignalAdapter; import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.engine.WorkflowService; import org.imixs.workflow.exceptions.AdapterException; import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.util.XMLParser; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.alexanderlogistics.BusinessPartnerService; import com.alexanderlogistics.InvoiceUtil; import com.alexanderlogistics.ksef.api.KSeFAPIService; import jakarta.inject.Inject; /** * The EInvoiceConverterAdapter converts a Cargsoft outbound invoice into an * e-invoice and embeds the xml file into the pdf file to produce a Zugferd * Invoice. * * The adapter fetches the .pdf file matching the name of * 'cargosoft.import.filename'. If no such an item exits, the adapter takes the * first PDF file from the list of attachments. * * This adapter can only be run on Invoice data provided by Cargosoft. * * The adapter extracts a template XML from the text block, then inserts invoice * information into the XML file. In the end, * it inserts the XML file into the PDF. * * The adapter can be configured by the model: * *
* {@code
textblock-ref
filename
true
}
*
*
*
* Wenn Debug = true gibt der Adapter alle einzelschritte auf der console aus
* und erzeugt zusätzlich die factur-x xml und txt dateien.
*
*
*
* @version 1.0
* @author rsoika
*/
public class EInvoiceAdapter implements SignalAdapter {
public static final String LINE_ITEMS_PROPERTY = "invoice.items";
private static Logger logger = Logger.getLogger(EInvoiceAdapter.class.getName());
public static SimpleDateFormat dateFormatter = new SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN);
public static NumberFormat numberFormat = NumberFormat.getInstance(Locale.GERMANY);
boolean debug = false;
@Inject
WorkflowService workflowService;
@Inject
DocumentService documentService;
@Inject
BusinessPartnerService businessPartnerService;
@Inject
KSeFAPIService kSeFAPIService;
/**
* This method
*
* @throws PluginException
*/
@Override
public ItemCollection execute(ItemCollection workitem, ItemCollection event)
throws AdapterException, PluginException {
logger.info("├── 🔜 Convert Invoice to E-Invoice...");
// read configuration....
ItemCollection eInvoiceConfig = workflowService.evalWorkflowResult(event, "e-invoice",
workitem,
false);
if (eInvoiceConfig == null || !eInvoiceConfig.hasItem("create")) {
throw new PluginException(EInvoiceAdapter.class.getSimpleName(), KSeFAPIService.CONFIG_ERROR,
"missing e-invoice configuration in model event - please check model configuration");
}
ItemCollection createDefinition = XMLParser.parseItemStructure(eInvoiceConfig.getItemValueString("CREATE"));
try {
// Load the e-invoice template....
FileData xmlFileData = kSeFAPIService.loadXMLTemplate(workitem, createDefinition);
FileData pdfFileData = kSeFAPIService.loadPDF(workitem);
logger.info("│ ├── Source File: " + pdfFileData.getName());
updateEInvoice(xmlFileData, workitem);
// append document
logger.info("│ ├── append new e-invoice");
workitem.addFileData(xmlFileData);
// Embedd XML.....
logger.info("│ ├── embed e-invoice into PDF");
FileData eInvoice = embeddXML(pdfFileData, xmlFileData);
workitem.removeFile(pdfFileData.getName());
// set new pdf file...
workitem.addFileData(eInvoice);
} catch (PluginException e) {
throw new AdapterException(e);
}
return workitem;
}
private FileData embeddXML(FileData pdfFileData, FileData xmlFileData) throws PluginException {
try {
PDDocument document = PDDocument.load(pdfFileData.getContent());
byte[] xmlBytes = xmlFileData.getContent();
// Erstelle einen InputStream aus dem XML Content
InputStream inputStream = new ByteArrayInputStream(xmlBytes);
// Erstelle eine Datei-Spezifikation für die XML
PDComplexFileSpecification fileSpec = new PDComplexFileSpecification();
fileSpec.setFile(xmlFileData.getName());
// Erstelle einen PDEmbeddedFile mit dem InputStream
PDEmbeddedFile embeddedFile = new PDEmbeddedFile(document, inputStream);
embeddedFile.setSubtype("application/xml");
embeddedFile.setSize(xmlBytes.length);
// Schließe den InputStream
inputStream.close();
// Setze den PDEmbeddedFile
fileSpec.setEmbeddedFile(embeddedFile);
// Erstelle einen Map-Eintrag für die eingebettete Datei
Map
* An element is considered "empty" if it has no text content, no
* attributes, and no child elements. Because removing a child can make
* its parent empty as well (e.g. an empty {@code DefinedTradeContact}
* once all of its empty grandchildren are gone), the method processes
* children first (bottom-up) and then re-checks the current node.
*
* This is used to clean up template placeholder elements
* (e.g. {@code
* The KSeF template carries comments to help template maintainers
* (field explanations, fixed-value markers, business rules). These
* comments must not appear in the final invoice that is submitted
* to KSeF or reviewed by the tax advisor - they only add noise.
*/
private void stripComments(Document doc) {
if (doc == null) {
return;
}
removeCommentsRecursive(doc);
}
/**
* Recursively walks a node and removes every direct child that is an
* XML comment. Iterates from the last child backwards so that
* removing a node does not affect the index of the still-to-visit
* children.
*/
private void removeCommentsRecursive(Node node) {
Node child = node.getLastChild();
while (child != null) {
Node previous = child.getPreviousSibling();
if (child.getNodeType() == Node.COMMENT_NODE) {
node.removeChild(child);
} else if (child.hasChildNodes()) {
removeCommentsRecursive(child);
}
child = previous;
}
}
}