Started e-invoice interface
This commit is contained in:
parent
1f2d3b4cd5
commit
1897377eb3
5 changed files with 3852 additions and 1 deletions
|
|
@ -1,6 +1,12 @@
|
||||||
# Versionen
|
# 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
|
- Finalisierung Business Partner Interface
|
||||||
|- Neue Plugin Logik (Aktualisierung der BP Nummer und Aktivierung von BP Objekten falls diese archiviert waren)
|
|- Neue Plugin Logik (Aktualisierung der BP Nummer und Aktivierung von BP Objekten falls diese archiviert waren)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
113
workflow/pl/e-invoice/factur-x.xml
Normal file
113
workflow/pl/e-invoice/factur-x.xml
Normal 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>
|
||||||
2628
workflow/pl/rechnungsausgang-pl-1.1.0.bpmn
Normal file
2628
workflow/pl/rechnungsausgang-pl-1.1.0.bpmn
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue