Started e-invoice interface

This commit is contained in:
Ralph Soika 2025-11-17 12:35:59 +01:00
parent 1f2d3b4cd5
commit 1897377eb3
5 changed files with 3852 additions and 1 deletions

View file

@ -1,6 +1,12 @@
# Versionen
## 1.3.4 (Development)
## 1.3.5 (Development)
- Verbesserter Cargosoft Export (Eingangsrechnungen)
- Polen KSeF Schnittstelle
- E-Rechnungs Adapter
## 1.3.4
- Finalisierung Business Partner Interface
|- Neue Plugin Logik (Aktualisierung der BP Nummer und Aktivierung von BP Objekten falls diese archiviert waren)

View file

@ -0,0 +1,556 @@
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.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;
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.archive.core.SnapshotService;
import org.imixs.einvoice.EInvoiceFormatException;
import org.imixs.einvoice.EInvoiceModel;
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.exceptions.QueryException;
import org.imixs.workflow.util.XMLParser;
import com.alexanderlogistics.InvoiceUtil;
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:
*
* <pre>
* {@code
<e-invoice name="create">
<textblock>textblock-ref</textblock>
<template>filename</template>
<debug>true</debug>
</e-invoice>
}
* </pre>
*
* <p>
* 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 EInvoiceConverterAdapter implements SignalAdapter {
final String TYPE_TEXTBLOCK = "textblock";
public static final String DOCUMENT_ERROR = "DOCUMENT_ERROR";
public static final String CONFIG_ERROR = "CONFIG_ERROR";
public static final String LINE_ITEMS_PROPERTY = "invoice.items";
private static Logger logger = Logger.getLogger(EInvoiceConverterAdapter.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
SnapshotService snapshotService;
/**
* 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(EInvoiceConverterAdapter.class.getSimpleName(), 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 = loadXMLTemplate(workitem, createDefinition);
FileData pdfFileData = loadPDF(workitem);
logger.info("│ ├── Source File: " + pdfFileData.getName());
} catch (PluginException e) {
throw new AdapterException(e);
// throw new AdapterException(
// EInvoiceConverterAdapter.class.getSimpleName(), e.getErrorCode(),
// e.getMessage());
}
return workitem;
}
/**
* This helper method tries to find the pdf file matching the item
* 'cargosoft.import.filename'. If not found the method returns the first .pdf
* file form the list of attachments. The method returns null if no pdf file
* exits
*
* @param workitem
* @return the pdf FileData object
* @throws PluginException if not pdf file was found
*/
private FileData loadPDF(ItemCollection workitem) throws PluginException {
FileData pdfFileData = null;
String cargosoftFileName = workitem.getItemValueString("cargosoft.import.filename");
if (!cargosoftFileName.isEmpty()) {
cargosoftFileName = cargosoftFileName.replace(".xml", ".pdf");
// try to load filedata....
pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), cargosoftFileName);
}
if (pdfFileData == null) {
// take first pdf file available
List<String> fileNames = workitem.getFileNames();
for (String filename : fileNames) {
if (filename.toLowerCase().endsWith(".pdf")) {
pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), filename);
break;
}
}
}
if (pdfFileData == null) {
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(),
DOCUMENT_ERROR,
"DOCUMENT_ERROR: no pdf file found!");
}
return pdfFileData;
}
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<String, PDComplexFileSpecification> filesMap = new HashMap<>();
filesMap.put(xmlFileData.getName(), fileSpec);
// Erstelle einen Eingebettete-Dateien-Namensbaum und füge die Datei hinzu
PDEmbeddedFilesNameTreeNode efTree = new PDEmbeddedFilesNameTreeNode();
efTree.setNames(filesMap);
// Erstelle ein Namens-Dictionary und füge den Baum hinzu
PDDocumentNameDictionary names = new PDDocumentNameDictionary(document.getDocumentCatalog());
names.setEmbeddedFiles(efTree);
document.getDocumentCatalog().setNames(names);
// Speichere das PDF-Dokument
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
byte[] eInvoiceData = baos.toByteArray();
document.close();
baos.close();
FileData eInvoiceFileData = new FileData(pdfFileData.getName(), eInvoiceData, pdfFileData.getContentType(),
pdfFileData.getAttributes());
return eInvoiceFileData;
} catch (IOException e) {
// TODO Auto-generated catch block
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(), CONFIG_ERROR,
"Failed to generate e-invoice: " + e.getMessage());
}
}
/**
* THis method updates an e-invoice template with the data stored in the
* workitem.
*
* First the method loads an EInvoiceModel based on the provided XML Template
* and than updates the e-invoice data based on the items stored in the given
* workitem.
*
* @param workitem
* @throws PluginException
*/
@SuppressWarnings("unchecked")
private void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException {
try {
EInvoiceModel model = EInvoiceModelFactory.read(new ByteArrayInputStream(fileDataXMLTemplate.getContent()));
model.setId(workitem.getItemValueString("invoice.number"));
// Update Rechnungssummen...
if (workitem.getItemValueDouble("invoice.vat.rate") > 0) {
// wir haben eine Steuer!
model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.amount.total")
- workitem.getItemValueDouble("invoice.amount.net")));
model.setTaxRate(workitem.getItemValueDouble("invoice.vat.rate"));
}
// // Update Invoice Items
// List<ItemCollection> invoiceItems = explodeChildList(workitem);
// double lineTotalAmount = 0.00;
// for (ItemCollection invoiceItem : invoiceItems) {
// TradeLineItem tradeLineItem = buildTradeLineItem(invoiceItem);
// model.setTradeLineItem(tradeLineItem);
// lineTotalAmount = lineTotalAmount + tradeLineItem.getTotal();
// }
// // Summenbildung
// model.setNetTotalAmount(workitem.getItemValueDouble("invoice.amount.net"));
// model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.amount.total"));
// model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date"));
// // Update Addresses
// TradeParty billingAddress =
// parseAddress(workitem.getItemValue("dbtr.address"), "buyer");
// model.setTradeParty(billingAddress);
// TradeParty shippingAddress =
// parseAddress(workitem.getItemValue("dbtr.address.shipping"), "ship_to");
// model.setTradeParty(shippingAddress);
// model.setOrderReferenceId(workitem.getItemValueString("order.number"));
// // finally update the template file
// fileDataXMLTemplate.setContent(model.getContent());
} catch (FileNotFoundException | EInvoiceFormatException e) {
throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, e.getMessage(), e);
}
}
/**
* Parses a list of address lines into a TradeParty object using best-guess
* approach
*/
/**
* Erstellt ein TradeParty Objekt aus einer Liste von Adresszeilen.
*
* @param addressLines Liste der Adresszeilen
* @param type Der Typ der TradeParty
* @return TradeParty
*/
public static TradeParty parseAddress(List<String> addressLines, String type) {
if (addressLines == null || addressLines.isEmpty()) {
throw new IllegalArgumentException("Address lines cannot be null or empty");
}
TradeParty tradeParty = new TradeParty(type);
// Name ist immer die erste Zeile
tradeParty.setName(addressLines.get(0).trim());
// Letzte Zeile analysieren
String lastLine = addressLines.get(addressLines.size() - 1).trim();
// Prüfen ob die letzte Zeile ein PLZ + Stadt Format hat
if (hasPostcodeAndCity(lastLine)) {
tradeParty.setCountryId("DE");
parsePostcodeAndCity(lastLine, tradeParty);
// Straßenadresse aus den Zeilen zwischen Name und PLZ/Stadt
buildStreetAddress(addressLines.subList(1, addressLines.size() - 1), tradeParty);
} else {
// Wenn die letzte Zeile kein PLZ + Stadt Format hat, dann ist es ein Land
String countryCode = resolveISOCountryCodes(lastLine.toUpperCase());
tradeParty.setCountryId(countryCode);
// Stadt und PLZ sind in der vorletzten Zeile
if (addressLines.size() > 2) {
parsePostcodeAndCity(addressLines.get(addressLines.size() - 2), tradeParty);
// Straßenadresse aus den Zeilen zwischen Name und PLZ/Stadt/Land
buildStreetAddress(addressLines.subList(1, addressLines.size() - 2), tradeParty);
}
}
return tradeParty;
}
/**
* Hilfemethode zum übersetzen von ISO Country codes
*
* @param country
* @return
*/
private static String resolveISOCountryCodes(String country) {
String countryCode = country.toUpperCase().substring(0, 2);
// Bulgarien -> BG
countryCode = countryCode.replace("BU", "BG");
return countryCode;
}
/**
* Baut die Straßenadresse aus den gegebenen Zeilen
*/
private static void buildStreetAddress(List<String> streetLines, TradeParty tradeParty) {
if (streetLines.isEmpty())
return;
StringBuilder streetAddress = new StringBuilder();
for (int i = 0; i < streetLines.size(); i++) {
if (i > 0) {
streetAddress.append(", ");
}
streetAddress.append(streetLines.get(i).trim());
}
tradeParty.setStreetAddress(streetAddress.toString());
}
/**
* Prüft ob eine Zeile PLZ und Stadt enthält.
* Eine PLZ wird als 4-5 stellige Zahl am Anfang der Zeile erkannt.
*/
private static boolean hasPostcodeAndCity(String line) {
String[] parts = line.trim().split("\\s+", 2);
if (parts.length < 2)
return false;
String potentialPostcode = parts[0];
return potentialPostcode.matches("\\d{4,5}");
}
/**
* Analysiert die PLZ und Stadt aus einer Zeile.
*/
private static void parsePostcodeAndCity(String line, TradeParty tradeParty) {
String[] parts = line.trim().split("\\s+", 2);
if (parts.length >= 2) {
tradeParty.setPostcodeCode(parts[0].trim());
tradeParty.setCityName(parts[1].trim());
} else {
tradeParty.setCityName(line.trim());
}
}
/**
* Parses the data list of an invoice line and returns a TradeLineItem object.
* The order of the list items must be exactly!
*/
private TradeLineItem buildTradeLineItem(ItemCollection orderItem) {
if (orderItem == null) {
return null;
}
TradeLineItem tradeLineItem = new TradeLineItem(orderItem.getItemValueString("id"));
tradeLineItem.setName(orderItem.getItemValueString("name"));
tradeLineItem.setQuantity(orderItem.getItemValueDouble("quantity"));
tradeLineItem.setNetPrice(orderItem.getItemValueDouble("price"));
tradeLineItem.setTotal(orderItem.getItemValueDouble("total"));
tradeLineItem.setTaxRate(orderItem.getItemValueDouble("vat"));
// if (orderItem.hasItem("orderid")) {
// tradeLineItem.setOrderReferenceId(orderItem.getItemValueString("orderid"));
// }
return tradeLineItem;
}
/**
* Generic method to set a item value by a regex
*
* @param text
* @param regex
* @return
*/
@SuppressWarnings("rawtypes")
private void setItemValueByRegex(ItemCollection workitem, String itemName, String content, String regex,
Class itemType) {
String value = getValueByRegex(content, regex);
logger.info(itemName + "=" + value);
if (value.isEmpty()) {
return; // no value found
}
// Parse Double?
if (itemType == Double.class) {
if (value.contains(",")) {
value = value.replace(".", "");
}
value = value.replace(",", ".");
workitem.setItemValue(itemName, Double.parseDouble(value));
} else
// Parse Integer
if (itemType == Integer.class) {
workitem.setItemValue(itemName, Integer.parseInt(value));
} else
// Parse Date
if (itemType == Date.class) {
try {
workitem.setItemValue(itemName, dateFormatter.parse(value));
} catch (ParseException e) {
logger.warning("Date value " + value + " not parsable: " + e.getMessage());
workitem.setItemValue(itemName, value);
}
} else {
workitem.setItemValue(itemName, value);
}
}
/**
* Generic method to extract a text fragment by regex
*
* @param text
* @param regex
* @return
*/
private String getValueByRegex(String text, String regex) {
String result = "";
Pattern datumPattern = Pattern.compile(regex);
Matcher datumMatcher = datumPattern.matcher(text);
if (datumMatcher.find()) {
result = datumMatcher.group(1);
}
return result;
}
/**
* This method loads a text-block for a specified ref and appends the named
* fileData object of this document.
*
* @param document
* @throws PluginException
*/
private FileData loadXMLTemplate(ItemCollection workitem, ItemCollection config)
throws PluginException {
String textblock = config.getItemValueString("textblock");
String template = config.getItemValueString("template");
String sourceName = config.getItemValueString("source");
try {
debug = Boolean.parseBoolean(config.getItemValueString("debug"));
} catch (Exception e) {
}
String targetName = "factur-x.xml";
// adapt text....
sourceName = workflowService.adaptText(sourceName, workitem);
if ((template == null || template.isEmpty()) || (textblock == null || textblock.isEmpty())) {
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(),
CONFIG_ERROR,
"invalid e-invoice configuration in model event - textblock/template reference not defined!");
}
// load the text block
FileData fileData = loadTextBlockFileData(textblock, template);
// do we found the document?
if (fileData == null) {
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(),
CONFIG_ERROR,
"invalid e-invoice configuration in model event - textblock/template: " + textblock + "/" + template
+ " not found!");
}
fileData.setName(targetName);
return fileData;
}
/**
* This method returns a text-block ItemCollection for a specified name.
*
* @param name in attribute txtname
*
*
*/
public FileData loadTextBlockFileData(String name, String fileName) {
ItemCollection textBlockItemCollection = null;
// load text-block by name....
String sQuery = "(type:\"" + TYPE_TEXTBLOCK + "\" AND txtname:\"" + name + "\")";
Collection<ItemCollection> col;
try {
// find the textblock...
col = documentService.find(sQuery, 1, 0);
if (col.size() > 0) {
textBlockItemCollection = col.iterator().next();
// fetch the fileData...
return snapshotService.getWorkItemFile(textBlockItemCollection.getUniqueID(), fileName);
} else {
logger.warning("Missing text-block : '" + name + "'");
}
} catch (QueryException e) {
logger.warning("getTextBlock - invalid query: " + e.getMessage());
}
return null;
}
}

View file

@ -0,0 +1,548 @@
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.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;
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.archive.core.SnapshotService;
import org.imixs.einvoice.EInvoiceFormatException;
import org.imixs.einvoice.EInvoiceModel;
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.exceptions.QueryException;
import org.imixs.workflow.util.XMLParser;
import com.alexanderlogistics.InvoiceUtil;
import jakarta.inject.Inject;
/**
* The KSeFAdapter converts a Cargsoft outbound invoice into an KSeF XML
* e-invoice and sends the xml file to the polish KSeF API
*
* The adapter can be configured by the model:
*
* <pre>
* {@code
<e-invoice name="ksef">
<textblock>textblock-ref</textblock>
<template>filename</template>
<debug>true</debug>
</e-invoice>
}
* </pre>
*
* <p>
* 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 KSeFAdapter implements SignalAdapter {
final String TYPE_TEXTBLOCK = "textblock";
public static final String DOCUMENT_ERROR = "DOCUMENT_ERROR";
public static final String CONFIG_ERROR = "CONFIG_ERROR";
public static final String LINE_ITEMS_PROPERTY = "invoice.items";
private static Logger logger = Logger.getLogger(EInvoiceConverterAdapter.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
SnapshotService snapshotService;
/**
* This method
*
* @throws PluginException
*/
@Override
public ItemCollection execute(ItemCollection workitem, ItemCollection event)
throws AdapterException, PluginException {
logger.info("├── 🔜 Convert Invoice to KSeF...");
// read configuration....
ItemCollection eInvoiceConfig = workflowService.evalWorkflowResult(event, "e-invoice",
workitem,
false);
if (eInvoiceConfig == null || !eInvoiceConfig.hasItem("ksef")) {
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(), CONFIG_ERROR,
"missing e-invoice/ksef configuration in model event - please check model configuration");
}
ItemCollection ksefDefinition = XMLParser.parseItemStructure(eInvoiceConfig.getItemValueString("ksef"));
try {
// Load the e-invoice template....
FileData xmlFileData = loadXMLTemplate(workitem, ksefDefinition);
FileData pdfFileData = loadPDF(workitem);
logger.info("│ ├── Source File: " + pdfFileData.getName());
if (true)
throw new AdapterException(KSeFAdapter.class.getSimpleName(), CONFIG_ERROR, "NOT YET IMPLEMENTED");
} catch (PluginException e) {
throw new AdapterException(e);
// throw new AdapterException(
// EInvoiceConverterAdapter.class.getSimpleName(), e.getErrorCode(),
// e.getMessage());
}
return workitem;
}
/**
* This helper method tries to find the pdf file matching the item
* 'cargosoft.import.filename'. If not found the method returns the first .pdf
* file form the list of attachments. The method returns null if no pdf file
* exits
*
* @param workitem
* @return the pdf FileData object
* @throws PluginException if not pdf file was found
*/
private FileData loadPDF(ItemCollection workitem) throws PluginException {
FileData pdfFileData = null;
String cargosoftFileName = workitem.getItemValueString("cargosoft.import.filename");
if (!cargosoftFileName.isEmpty()) {
cargosoftFileName = cargosoftFileName.replace(".xml", ".pdf");
// try to load filedata....
pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), cargosoftFileName);
}
if (pdfFileData == null) {
// take first pdf file available
List<String> fileNames = workitem.getFileNames();
for (String filename : fileNames) {
if (filename.toLowerCase().endsWith(".pdf")) {
pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), filename);
break;
}
}
}
if (pdfFileData == null) {
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(),
DOCUMENT_ERROR,
"DOCUMENT_ERROR: no pdf file found!");
}
return pdfFileData;
}
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<String, PDComplexFileSpecification> filesMap = new HashMap<>();
filesMap.put(xmlFileData.getName(), fileSpec);
// Erstelle einen Eingebettete-Dateien-Namensbaum und füge die Datei hinzu
PDEmbeddedFilesNameTreeNode efTree = new PDEmbeddedFilesNameTreeNode();
efTree.setNames(filesMap);
// Erstelle ein Namens-Dictionary und füge den Baum hinzu
PDDocumentNameDictionary names = new PDDocumentNameDictionary(document.getDocumentCatalog());
names.setEmbeddedFiles(efTree);
document.getDocumentCatalog().setNames(names);
// Speichere das PDF-Dokument
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
byte[] eInvoiceData = baos.toByteArray();
document.close();
baos.close();
FileData eInvoiceFileData = new FileData(pdfFileData.getName(), eInvoiceData, pdfFileData.getContentType(),
pdfFileData.getAttributes());
return eInvoiceFileData;
} catch (IOException e) {
// TODO Auto-generated catch block
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(), CONFIG_ERROR,
"Failed to generate e-invoice: " + e.getMessage());
}
}
/**
* THis method updates an e-invoice template with the data stored in the
* workitem.
*
* First the method loads an EInvoiceModel based on the provided XML Template
* and than updates the e-invoice data based on the items stored in the given
* workitem.
*
* @param workitem
* @throws PluginException
*/
@SuppressWarnings("unchecked")
private void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException {
try {
EInvoiceModel model = EInvoiceModelFactory.read(new ByteArrayInputStream(fileDataXMLTemplate.getContent()));
model.setId(workitem.getItemValueString("invoice.number"));
// Update Rechnungssummen...
if (workitem.getItemValueDouble("invoice.vat.rate") > 0) {
// wir haben eine Steuer!
model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.amount.total")
- workitem.getItemValueDouble("invoice.amount.net")));
model.setTaxRate(workitem.getItemValueDouble("invoice.vat.rate"));
}
// // Update Invoice Items
// List<ItemCollection> invoiceItems = explodeChildList(workitem);
// double lineTotalAmount = 0.00;
// for (ItemCollection invoiceItem : invoiceItems) {
// TradeLineItem tradeLineItem = buildTradeLineItem(invoiceItem);
// model.setTradeLineItem(tradeLineItem);
// lineTotalAmount = lineTotalAmount + tradeLineItem.getTotal();
// }
// // Summenbildung
// model.setNetTotalAmount(workitem.getItemValueDouble("invoice.amount.net"));
// model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.amount.total"));
// model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date"));
// // Update Addresses
// TradeParty billingAddress =
// parseAddress(workitem.getItemValue("dbtr.address"), "buyer");
// model.setTradeParty(billingAddress);
// TradeParty shippingAddress =
// parseAddress(workitem.getItemValue("dbtr.address.shipping"), "ship_to");
// model.setTradeParty(shippingAddress);
// model.setOrderReferenceId(workitem.getItemValueString("order.number"));
// // finally update the template file
// fileDataXMLTemplate.setContent(model.getContent());
} catch (FileNotFoundException | EInvoiceFormatException e) {
throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, e.getMessage(), e);
}
}
/**
* Parses a list of address lines into a TradeParty object using best-guess
* approach
*/
/**
* Erstellt ein TradeParty Objekt aus einer Liste von Adresszeilen.
*
* @param addressLines Liste der Adresszeilen
* @param type Der Typ der TradeParty
* @return TradeParty
*/
public static TradeParty parseAddress(List<String> addressLines, String type) {
if (addressLines == null || addressLines.isEmpty()) {
throw new IllegalArgumentException("Address lines cannot be null or empty");
}
TradeParty tradeParty = new TradeParty(type);
// Name ist immer die erste Zeile
tradeParty.setName(addressLines.get(0).trim());
// Letzte Zeile analysieren
String lastLine = addressLines.get(addressLines.size() - 1).trim();
// Prüfen ob die letzte Zeile ein PLZ + Stadt Format hat
if (hasPostcodeAndCity(lastLine)) {
tradeParty.setCountryId("DE");
parsePostcodeAndCity(lastLine, tradeParty);
// Straßenadresse aus den Zeilen zwischen Name und PLZ/Stadt
buildStreetAddress(addressLines.subList(1, addressLines.size() - 1), tradeParty);
} else {
// Wenn die letzte Zeile kein PLZ + Stadt Format hat, dann ist es ein Land
String countryCode = resolveISOCountryCodes(lastLine.toUpperCase());
tradeParty.setCountryId(countryCode);
// Stadt und PLZ sind in der vorletzten Zeile
if (addressLines.size() > 2) {
parsePostcodeAndCity(addressLines.get(addressLines.size() - 2), tradeParty);
// Straßenadresse aus den Zeilen zwischen Name und PLZ/Stadt/Land
buildStreetAddress(addressLines.subList(1, addressLines.size() - 2), tradeParty);
}
}
return tradeParty;
}
/**
* Hilfemethode zum übersetzen von ISO Country codes
*
* @param country
* @return
*/
private static String resolveISOCountryCodes(String country) {
String countryCode = country.toUpperCase().substring(0, 2);
// Bulgarien -> BG
countryCode = countryCode.replace("BU", "BG");
return countryCode;
}
/**
* Baut die Straßenadresse aus den gegebenen Zeilen
*/
private static void buildStreetAddress(List<String> streetLines, TradeParty tradeParty) {
if (streetLines.isEmpty())
return;
StringBuilder streetAddress = new StringBuilder();
for (int i = 0; i < streetLines.size(); i++) {
if (i > 0) {
streetAddress.append(", ");
}
streetAddress.append(streetLines.get(i).trim());
}
tradeParty.setStreetAddress(streetAddress.toString());
}
/**
* Prüft ob eine Zeile PLZ und Stadt enthält.
* Eine PLZ wird als 4-5 stellige Zahl am Anfang der Zeile erkannt.
*/
private static boolean hasPostcodeAndCity(String line) {
String[] parts = line.trim().split("\\s+", 2);
if (parts.length < 2)
return false;
String potentialPostcode = parts[0];
return potentialPostcode.matches("\\d{4,5}");
}
/**
* Analysiert die PLZ und Stadt aus einer Zeile.
*/
private static void parsePostcodeAndCity(String line, TradeParty tradeParty) {
String[] parts = line.trim().split("\\s+", 2);
if (parts.length >= 2) {
tradeParty.setPostcodeCode(parts[0].trim());
tradeParty.setCityName(parts[1].trim());
} else {
tradeParty.setCityName(line.trim());
}
}
/**
* Parses the data list of an invoice line and returns a TradeLineItem object.
* The order of the list items must be exactly!
*/
private TradeLineItem buildTradeLineItem(ItemCollection orderItem) {
if (orderItem == null) {
return null;
}
TradeLineItem tradeLineItem = new TradeLineItem(orderItem.getItemValueString("id"));
tradeLineItem.setName(orderItem.getItemValueString("name"));
tradeLineItem.setQuantity(orderItem.getItemValueDouble("quantity"));
tradeLineItem.setNetPrice(orderItem.getItemValueDouble("price"));
tradeLineItem.setTotal(orderItem.getItemValueDouble("total"));
tradeLineItem.setTaxRate(orderItem.getItemValueDouble("vat"));
// if (orderItem.hasItem("orderid")) {
// tradeLineItem.setOrderReferenceId(orderItem.getItemValueString("orderid"));
// }
return tradeLineItem;
}
/**
* Generic method to set a item value by a regex
*
* @param text
* @param regex
* @return
*/
@SuppressWarnings("rawtypes")
private void setItemValueByRegex(ItemCollection workitem, String itemName, String content, String regex,
Class itemType) {
String value = getValueByRegex(content, regex);
logger.info(itemName + "=" + value);
if (value.isEmpty()) {
return; // no value found
}
// Parse Double?
if (itemType == Double.class) {
if (value.contains(",")) {
value = value.replace(".", "");
}
value = value.replace(",", ".");
workitem.setItemValue(itemName, Double.parseDouble(value));
} else
// Parse Integer
if (itemType == Integer.class) {
workitem.setItemValue(itemName, Integer.parseInt(value));
} else
// Parse Date
if (itemType == Date.class) {
try {
workitem.setItemValue(itemName, dateFormatter.parse(value));
} catch (ParseException e) {
logger.warning("Date value " + value + " not parsable: " + e.getMessage());
workitem.setItemValue(itemName, value);
}
} else {
workitem.setItemValue(itemName, value);
}
}
/**
* Generic method to extract a text fragment by regex
*
* @param text
* @param regex
* @return
*/
private String getValueByRegex(String text, String regex) {
String result = "";
Pattern datumPattern = Pattern.compile(regex);
Matcher datumMatcher = datumPattern.matcher(text);
if (datumMatcher.find()) {
result = datumMatcher.group(1);
}
return result;
}
/**
* This method loads a text-block for a specified ref and appends the named
* fileData object of this document.
*
* @param document
* @throws PluginException
*/
private FileData loadXMLTemplate(ItemCollection workitem, ItemCollection config)
throws PluginException {
String textblock = config.getItemValueString("textblock");
String template = config.getItemValueString("template");
String sourceName = config.getItemValueString("source");
try {
debug = Boolean.parseBoolean(config.getItemValueString("debug"));
} catch (Exception e) {
}
String targetName = "factur-x.xml";
// adapt text....
sourceName = workflowService.adaptText(sourceName, workitem);
if ((template == null || template.isEmpty()) || (textblock == null || textblock.isEmpty())) {
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(),
CONFIG_ERROR,
"invalid e-invoice configuration in model event - textblock/template reference not defined!");
}
// load the text block
FileData fileData = loadTextBlockFileData(textblock, template);
// do we found the document?
if (fileData == null) {
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(),
CONFIG_ERROR,
"invalid e-invoice configuration in model event - textblock/template: " + textblock + "/" + template
+ " not found!");
}
fileData.setName(targetName);
return fileData;
}
/**
* This method returns a text-block ItemCollection for a specified name.
*
* @param name in attribute txtname
*
*
*/
public FileData loadTextBlockFileData(String name, String fileName) {
ItemCollection textBlockItemCollection = null;
// load text-block by name....
String sQuery = "(type:\"" + TYPE_TEXTBLOCK + "\" AND txtname:\"" + name + "\")";
Collection<ItemCollection> col;
try {
// find the textblock...
col = documentService.find(sQuery, 1, 0);
if (col.size() > 0) {
textBlockItemCollection = col.iterator().next();
// fetch the fileData...
return snapshotService.getWorkItemFile(textBlockItemCollection.getUniqueID(), fileName);
} else {
logger.warning("Missing text-block : '" + name + "'");
}
} catch (QueryException e) {
logger.warning("getTextBlock - invalid query: " + e.getMessage());
}
return null;
}
}

View file

@ -0,0 +1,113 @@
<?xml version='1.0' encoding='UTF-8'?>
<rsm:CrossIndustryInvoice xmlns:a='urn:un:unece:uncefact:data:standard:QualifiedDataType:100'
xmlns:rsm='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'
xmlns:qdt='urn:un:unece:uncefact:data:standard:QualifiedDataType:10'
xmlns:ram='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'
xmlns:xs='http://www.w3.org/2001/XMLSchema'
xmlns:udt='urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'>
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID></ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102"></udt:DateTimeString>
</ram:IssueDateTime>
<ram:IncludedNote>
<ram:Content>Wir berechnen heute unter Zugrundelegung unserer allgemeinen
Geschäftsbedingungen, die Ihnen bereits
übersendet wurden oder die Sie unter www.mueller-ahlhorn.com nachlesen können, wie
folgt:</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Bitte zahlen Sie ab sofort alle Rechnungen auf unser Konto bei der
Landessparkasse zu Oldenburg, BIC
SLZODE22XXX, IBAN DE04 2805 0100 0092 2828 70. Etwaig anfallende Bankspesen gehen zu
Ihren Lasten.</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Dr. Dietrich Müller GmbH</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Geschäftsführer: Dr. Michael Müller </ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Amtsgericht Oldenburg HRB 209026</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>info@mueller-ahlhorn.com</ram:Content>
</ram:IncludedNote>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:BuyerReference />
<ram:SellerTradeParty>
<ram:Name>Dr. Dietrich Müller GmbH</ram:Name>
<ram:DefinedTradeContact>
<ram:PersonName>Müller, Michael</ram:PersonName>
<ram:TelephoneUniversalCommunication>
<ram:CompleteNumber>+4944359710261</ram:CompleteNumber>
</ram:TelephoneUniversalCommunication>
<ram:EmailURIUniversalCommunication>
<ram:URIID>info@mueller-ahlhorn.com</ram:URIID>
</ram:EmailURIUniversalCommunication>
</ram:DefinedTradeContact>
<ram:PostalTradeAddress>
<ram:PostcodeCode>26197</ram:PostcodeCode>
<ram:LineOne>Zeppelinring 18</ram:LineOne>
<ram:CityName>Ahlhorn</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:URIUniversalCommunication>
<ram:URIID schemeID="EM">info@mueller-ahlhorn.com</ram:URIID>
</ram:URIUniversalCommunication>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">DE295969093</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery>
</ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>30</ram:TypeCode>
<ram:PayeePartyCreditorFinancialAccount>
<ram:IBANID>DE04 2805 0100 0092 2828 70</ram:IBANID>
</ram:PayeePartyCreditorFinancialAccount>
<ram:PayeeSpecifiedCreditorFinancialInstitution>
<ram:BICID>SLZODE22XXX</ram:BICID>
</ram:PayeeSpecifiedCreditorFinancialInstitution>
</ram:SpecifiedTradeSettlementPaymentMeans>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>0.00</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>0.00</ram:BasisAmount>
<ram:CategoryCode>Z</ram:CategoryCode>
<ram:RateApplicablePercent>0.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradePaymentTerms>
<ram:Description>Wir berechnen heute unter Zugrundelegung unserer allgemeinen
Geschäftsbedingungen, die Ihnen bereits
übersendet wurden oder die Sie unter www.mueller-ahlhorn.com nachlesen können.</ram:Description>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>0.00</ram:LineTotalAmount>
<ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>
<ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>
<ram:TaxBasisTotalAmount>0.00</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">0.00</ram:TaxTotalAmount>
<ram:GrandTotalAmount>0.00</ram:GrandTotalAmount>
<ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
<ram:DuePayableAmount>0.00</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>

File diff suppressed because it is too large Load diff