From 1897377eb31daba160ddcfc2e676e9c2eba54a19 Mon Sep 17 00:00:00 2001 From: Ralph Soika Date: Mon, 17 Nov 2025 12:35:59 +0100 Subject: [PATCH] Started e-invoice interface --- RELEASENOTES.md | 8 +- .../einvoice/EInvoiceConverterAdapter.java | 556 ++++ .../einvoice/KSeFAdapter.java | 548 ++++ workflow/pl/e-invoice/factur-x.xml | 113 + workflow/pl/rechnungsausgang-pl-1.1.0.bpmn | 2628 +++++++++++++++++ 5 files changed, 3852 insertions(+), 1 deletion(-) create mode 100644 office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceConverterAdapter.java create mode 100644 office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapter.java create mode 100644 workflow/pl/e-invoice/factur-x.xml create mode 100644 workflow/pl/rechnungsausgang-pl-1.1.0.bpmn diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4d896aa..8ce7fef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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) diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceConverterAdapter.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceConverterAdapter.java new file mode 100644 index 0000000..4b2431d --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceConverterAdapter.java @@ -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: + * + *
+ * {@code
+        
+            textblock-ref
+            
+            true
+         
+   }
+ * 
+ * + *

+ * Wenn Debug = true gibt der Adapter alle einzelschritte auf der console aus + * und erzeugt zusätzlich die factur-x xml und txt dateien. + * + * + * + * @version 1.0 + * @author rsoika + */ +public class 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 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 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 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 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 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 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; + } + +} \ No newline at end of file diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapter.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapter.java new file mode 100644 index 0000000..4e04a87 --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapter.java @@ -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: + * + *

+ * {@code
+        
+            textblock-ref
+            
+            true
+         
+   }
+ * 
+ * + *

+ * Wenn Debug = true gibt der Adapter alle einzelschritte auf der console aus + * und erzeugt zusätzlich die factur-x xml und txt dateien. + * + * + * + * @version 1.0 + * @author rsoika + */ +public class 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 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 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 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 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 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 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; + } + +} \ No newline at end of file diff --git a/workflow/pl/e-invoice/factur-x.xml b/workflow/pl/e-invoice/factur-x.xml new file mode 100644 index 0000000..29f33c1 --- /dev/null +++ b/workflow/pl/e-invoice/factur-x.xml @@ -0,0 +1,113 @@ + + + + + urn:cen.eu:en16931:2017 + + + + + 380 + + + + + 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: + + + 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. + + + Dr. Dietrich Müller GmbH + + + Geschäftsführer: Dr. Michael Müller + + + Amtsgericht Oldenburg HRB 209026 + + + info@mueller-ahlhorn.com + + + + + + + + Dr. Dietrich Müller GmbH + + Müller, Michael + + +4944359710261 + + + info@mueller-ahlhorn.com + + + + 26197 + Zeppelinring 18 + Ahlhorn + DE + + + info@mueller-ahlhorn.com + + + DE295969093 + + + + + + + + + + EUR + + 30 + + DE04 2805 0100 0092 2828 70 + + + SLZODE22XXX + + + + 0.00 + VAT + 0.00 + Z + 0.00 + + + 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. + + + 0.00 + 0.00 + 0.00 + 0.00 + 0.00 + 0.00 + 0.00 + 0.00 + + + + \ No newline at end of file diff --git a/workflow/pl/rechnungsausgang-pl-1.1.0.bpmn b/workflow/pl/rechnungsausgang-pl-1.1.0.bpmn new file mode 100644 index 0000000..3ffeffa --- /dev/null +++ b/workflow/pl/rechnungsausgang-pl-1.1.0.bpmn @@ -0,0 +1,2628 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + +<itemvalue>$workflowstatus</itemvalue> + +]]> + + + In case you have already made the payment, please disregard this notice. If you require any information regarding your account, please do not hesitate to contact us.

+

Thank you for your cooperation.

+

 

+

Kind regards
Alexander Global Logistics GmbH

+ +]]> + + + + + + + + + + + + + + + StartEvent_1 + task_LqbtSQ + IntermediateCatchEvent_6 + task_nKKaIg + event_sGMk3g + task_rhD89g + event_chRsuA + event_nQBQjw + event_0YONsg + event_NIn1DQ + event_IkxO0Q + event_o1nouw + event_iAvEHw + task_iWUeSw + gateway_DjDNAA + gateway_k00hrA + event_AujEAA + event_FYxkhQ + + + + Task_5 + Task_8 + Task_6 + IntermediateCatchEvent_5000-20 + EndEvent_2 + EndEvent_1 + IntermediateCatchEvent_2 + IntermediateCatchEvent_27 + IntermediateCatchEvent_5 + EndEvent_3 + TextAnnotation_1 + textAnnotation_ru52BA + Task_2 + IntermediateCatchEvent_23 + IntermediateCatchEvent_17 + IntermediateCatchEvent_18 + IntermediateCatchEvent_3 + IntermediateThrowEvent_8 + IntermediateThrowEvent_11 + event_iUSXSg + ExclusiveGateway_1 + EventBasedGateway_1 + ExclusiveGateway_5 + DataObject_2 + + + + TextAnnotation_2 + Task_1 + IntermediateCatchEvent_1 + IntermediateThrowEvent_9 + IntermediateCatchEvent_9 + IntermediateThrowEvent_10 + event_OzINHA + ExclusiveGateway_3 + dataObject_V0KVQg + event_w96sOg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + home]]> + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + SequenceFlow_22 + SequenceFlow_27 + + + + sequenceFlow_nPTjjw + SequenceFlow_33 + + SequenceFlow_15 + + + + + + + sequenceFlow_sN9AQQ + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_33 + SequenceFlow_14 + SequenceFlow_39 + SequenceFlow_22 + sequenceFlow_OttgXA + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_52 + SequenceFlow_11 + SequenceFlow_47 + + + SequenceFlow_47 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dunning]]> + + + + + + + + + + + + false + + + + sequenceFlow_4IITzA + + + sequenceFlow_sN9AQQ + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_27 + sequenceFlow_EZX5kQ + SequenceFlow_18 + SequenceFlow_34 + sequenceFlow_zhcE7Q + sequenceFlow_I5jpvQ + + + SequenceFlow_61 + + + + SequenceFlow_39 + SequenceFlow_17 + SequenceFlow_65 + SequenceFlow_13 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + SequenceFlow_52 + + + + + + + + + SequenceFlow_18 + SequenceFlow_25 + SequenceFlow_64 + SequenceFlow_9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + + + + SequenceFlow_42 + SequenceFlow_8 + + + + + + + + + + SequenceFlow_42 + + + + + SequenceFlow_8 + SequenceFlow_10 + SequenceFlow_12 + + + + + + + + + invoice.saldo]]> + + + + + + SequenceFlow_10 + SequenceFlow_11 + + + + + + + + + + + + + invoice.saldo]]> + + + + + + SequenceFlow_12 + SequenceFlow_14 + + + + + + + + + + SequenceFlow_17 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + home]]> + + + + + + + + + + + + false + + + + + SequenceFlow_34 + SequenceFlow_15 + + + + + + SequenceFlow_25 + + + + + + + + + + + + + + + + cdtr.name invoice.number]]> + + + + + + + + + cdtr.name +Rechnungsnummer: invoice.number +Betrag: invoice.total invoice.currency + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + 7 +1. Dunning + +]]> + + + + + + + + + + + + false + + + + + + + + + sequenceFlow_zhcE7Q + association_P36Fzw + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_60 + SequenceFlow_61 + + + + + invoice.saldo]]> + + + + + + SequenceFlow_60 + + + + + + + + + + SequenceFlow_64 + + + + + + + + SequenceFlow_65 + + + + + + + + + + + + + + + + _subject]]> + + + + + + + + + _amount (Brutto € _amount_brutto) +_description]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + home]]> + + + + + + + + + + + + false + + + + + + + SequenceFlow_9 + SequenceFlow_13 + SequenceFlow_5 + + + + + + + + + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_5 + SequenceFlow_19 + + + SequenceFlow_19 + + + + + + + + + + Trigger über ZahlungseingangsSaldoAdapter + + + + + + + Triggered by MahnlaufExecuteAdapter + +Wiedervorlage wird um 7 bzw. 5 Tage erhöht + + + + + + + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_14 + sequenceFlow_4IITzA + sequenceFlow_1xQ7aA + sequenceFlow_UOahyg + SequenceFlow_22 + sequenceFlow_nrxqhg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + sequenceFlow_OttgXA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + association_xVkeww + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (type:workitem) AND ($taskid:1000) AND ($modelversion:outbound-tax-export*) AND (name:"invoice.date") + outbound-tax-export-1.0 + 1000 + 100 + invoice.date + true +]]> + + + + + + + + + + + + false + + + + sequenceFlow_EZX5kQ + + + + + + + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + sequenceFlow_4IITzA + sequenceFlow_sO35Dg + sequenceFlow_2BSFNw + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + e-invoice-template + + + true + ]]> + + + + + + + sequenceFlow_nrxqhg + sequenceFlow_0A0wqQ + sequenceFlow_gjnUAA + + + + + + + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + sequenceFlow_gjnUAA + sequenceFlow_eadkWg + sequenceFlow_0aNMvQ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + e-invoice-template + + + true + ]]> + + + + + + + + + + + + false + + + + sequenceFlow_2BSFNw + sequenceFlow_JNYd0A + sequenceFlow_F90iWQ + + + + + + + + + + + sequenceFlow_0A0wqQ + + + + + sequenceFlow_JNYd0A + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + sequenceFlow_0aNMvQ + sequenceFlow_nPTjjw + + + + + + + + + + sequenceFlow_UQnJtA + + + + + + + + sequenceFlow_I5jpvQ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + sequenceFlow_UQnJtA + sequenceFlow_1xQ7aA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + sequenceFlow_UOahyg + + + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + sequenceFlow_4IITzA + sequenceFlow_UOahyg + sequenceFlow_BaNbmQ + sequenceFlow_nrxqhg + sequenceFlow_6VjCnQ + + + + sequenceFlow_gjnUAA + sequenceFlow_sO35Dg + sequenceFlow_79GZ8w + + + + sequenceFlow_F90iWQ + sequenceFlow_eadkWg + sequenceFlow_DCA0Tw + + + + + + + + + + + + + sequenceFlow_6VjCnQ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + adapter.error_code: adapter.error_message]]> + + + + + + + + + adapter.error_code: adapter.error_message]]> + + + false + + + + sequenceFlow_DCA0Tw + sequenceFlow_BaNbmQ + + sequenceFlow_79GZ8w + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +