diff --git a/office-alexander-logistics-app/pom.xml b/office-alexander-logistics-app/pom.xml index c053516..ccee67d 100644 --- a/office-alexander-logistics-app/pom.xml +++ b/office-alexander-logistics-app/pom.xml @@ -315,6 +315,13 @@ compile + + + org.imixs.util + imixs-e-invoice + ${org.imixs.e-invoice.version} + + net.sf.saxon 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 index 4b2431d..13fe39a 100644 --- 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 @@ -6,17 +6,15 @@ 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 javax.xml.transform.TransformerException; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; @@ -39,6 +37,7 @@ import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.QueryException; import org.imixs.workflow.util.XMLParser; +import com.alexanderlogistics.BusinessPartnerService; import com.alexanderlogistics.InvoiceUtil; import jakarta.inject.Inject; @@ -100,6 +99,9 @@ public class EInvoiceConverterAdapter implements SignalAdapter { @Inject SnapshotService snapshotService; + @Inject + BusinessPartnerService businessPartnerService; + /** * This method * @@ -126,12 +128,21 @@ public class EInvoiceConverterAdapter implements SignalAdapter { logger.info("│ ├── Source File: " + pdfFileData.getName()); + updateEInvoice(xmlFileData, workitem); + + // append document + logger.info("│ ├── append new e-invoice"); + workitem.addFileData(xmlFileData); + + // Embedd XML..... + logger.info("│ ├── embed e-invoice into PDF"); + FileData eInvoice = embeddXML(pdfFileData, xmlFileData); + workitem.removeFile(pdfFileData.getName()); + // set new pdf file... + workitem.addFileData(eInvoice); + } catch (PluginException e) { throw new AdapterException(e); - // throw new AdapterException( - // EInvoiceConverterAdapter.class.getSimpleName(), e.getErrorCode(), - // e.getMessage()); - } return workitem; @@ -241,7 +252,6 @@ public class EInvoiceConverterAdapter implements SignalAdapter { * @param workitem * @throws PluginException */ - @SuppressWarnings("unchecked") private void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException { try { @@ -250,51 +260,46 @@ public class EInvoiceConverterAdapter implements SignalAdapter { model.setId(workitem.getItemValueString("invoice.number")); // Update Rechnungssummen... - if (workitem.getItemValueDouble("invoice.vat.rate") > 0) { + if (workitem.getItemValueDouble("invoice.total.tax") > 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")); + model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.total") + - workitem.getItemValueDouble("invoice.total.net"))); + model.setTaxRate(workitem.getItemValueDouble("invoice.total.tax")); } - // // 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(); - // } + // Update Invoice Items + List invoiceItems = InvoiceUtil.explodeChildList(workitem, "_childitems"); + 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")); + model.setNetTotalAmount(workitem.getItemValueDouble("invoice.total.net")); + model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.total")); + model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date")); - // // Update Addresses - // TradeParty billingAddress = - // parseAddress(workitem.getItemValue("dbtr.address"), "buyer"); - // model.setTradeParty(billingAddress); + // Update Addresses + TradeParty billingAddress = buildAddress(workitem.getItemValueString("partner.id"), "buyer"); + model.setTradeParty(billingAddress); - // TradeParty shippingAddress = - // parseAddress(workitem.getItemValue("dbtr.address.shipping"), "ship_to"); - // model.setTradeParty(shippingAddress); + TradeParty shippingAddress = buildAddress(workitem.getItemValueString("partner.id"), "ship_to"); + model.setTradeParty(shippingAddress); + // References>AB230321 // model.setOrderReferenceId(workitem.getItemValueString("order.number")); - // // finally update the template file - // fileDataXMLTemplate.setContent(model.getContent()); + // finally update the template file + fileDataXMLTemplate.setContent(model.getContent()); - } catch (FileNotFoundException | EInvoiceFormatException e) { + } catch (FileNotFoundException | EInvoiceFormatException | TransformerException 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. * @@ -302,98 +307,28 @@ public class EInvoiceConverterAdapter implements SignalAdapter { * @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"); + public TradeParty buildAddress(String partnerID, String type) throws PluginException { + if (partnerID == null || partnerID.isEmpty()) { + throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, "Missing partnerID"); } + ItemCollection businessPartner = businessPartnerService.getBusinessPartnerByID(partnerID); + if (businessPartner == null) { + throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, + "Business Partner ID '" + partnerID + "' does not exist"); + } 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); - } - } + tradeParty.setName(businessPartner.getItemValueString("partner.name")); + tradeParty.setCountryId(businessPartner.getItemValueString("partner.country")); + tradeParty.setCityName(businessPartner.getItemValueString("partner.city")); + tradeParty.setPostcodeCode(businessPartner.getItemValueString("partner.zip")); + tradeParty.setStreetAddress(businessPartner.getItemValueString("partner.address")); 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! @@ -403,80 +338,17 @@ public class EInvoiceConverterAdapter implements SignalAdapter { return null; } - TradeLineItem tradeLineItem = new TradeLineItem(orderItem.getItemValueString("id")); + TradeLineItem tradeLineItem = new TradeLineItem(orderItem.getItemValueString("numpos")); - 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")); + tradeLineItem.setName(orderItem.getItemValueString("datev.text")); + tradeLineItem.setQuantity(1); + tradeLineItem.setNetPrice(orderItem.getItemValueDouble("datev.umsatz")); + tradeLineItem.setTotal(orderItem.getItemValueDouble("datev.umsatz")); + // 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. 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 index 4e04a87..fc7e1bc 100644 --- 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 @@ -1,28 +1,16 @@ 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 javax.xml.transform.TransformerException; + import org.imixs.archive.core.SnapshotService; import org.imixs.einvoice.EInvoiceFormatException; import org.imixs.einvoice.EInvoiceModel; @@ -39,6 +27,7 @@ import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.QueryException; import org.imixs.workflow.util.XMLParser; +import com.alexanderlogistics.BusinessPartnerService; import com.alexanderlogistics.InvoiceUtil; import jakarta.inject.Inject; @@ -89,6 +78,9 @@ public class KSeFAdapter implements SignalAdapter { @Inject SnapshotService snapshotService; + @Inject + BusinessPartnerService businessPartnerService; + /** * This method * @@ -112,116 +104,20 @@ public class KSeFAdapter implements SignalAdapter { // Load the e-invoice template.... FileData xmlFileData = loadXMLTemplate(workitem, ksefDefinition); - FileData pdfFileData = loadPDF(workitem); - logger.info("│ ├── Source File: " + pdfFileData.getName()); + updateEInvoice(xmlFileData, workitem); + + // append XML document + logger.info("│ ├── export KSeF e-invoice"); + workitem.addFileData(xmlFileData); - 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. @@ -233,7 +129,6 @@ public class KSeFAdapter implements SignalAdapter { * @param workitem * @throws PluginException */ - @SuppressWarnings("unchecked") private void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException { try { @@ -242,51 +137,46 @@ public class KSeFAdapter implements SignalAdapter { model.setId(workitem.getItemValueString("invoice.number")); // Update Rechnungssummen... - if (workitem.getItemValueDouble("invoice.vat.rate") > 0) { + if (workitem.getItemValueDouble("invoice.total.tax") > 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")); + model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.total") + - workitem.getItemValueDouble("invoice.total.net"))); + model.setTaxRate(workitem.getItemValueDouble("invoice.total.tax")); } - // // 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(); - // } + // Update Invoice Items + List invoiceItems = InvoiceUtil.explodeChildList(workitem, "_childitems"); + 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")); + model.setNetTotalAmount(workitem.getItemValueDouble("invoice.total.net")); + model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.total")); + model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date")); - // // Update Addresses - // TradeParty billingAddress = - // parseAddress(workitem.getItemValue("dbtr.address"), "buyer"); - // model.setTradeParty(billingAddress); + // Update Addresses + TradeParty billingAddress = buildAddress(workitem.getItemValueString("partner.id"), "buyer"); + model.setTradeParty(billingAddress); - // TradeParty shippingAddress = - // parseAddress(workitem.getItemValue("dbtr.address.shipping"), "ship_to"); - // model.setTradeParty(shippingAddress); + TradeParty shippingAddress = buildAddress(workitem.getItemValueString("partner.id"), "ship_to"); + model.setTradeParty(shippingAddress); + // References>AB230321 // model.setOrderReferenceId(workitem.getItemValueString("order.number")); - // // finally update the template file - // fileDataXMLTemplate.setContent(model.getContent()); + // finally update the template file + fileDataXMLTemplate.setContent(model.getContent()); - } catch (FileNotFoundException | EInvoiceFormatException e) { + } catch (FileNotFoundException | EInvoiceFormatException | TransformerException 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. * @@ -294,98 +184,28 @@ public class KSeFAdapter implements SignalAdapter { * @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"); + public TradeParty buildAddress(String partnerID, String type) throws PluginException { + if (partnerID == null || partnerID.isEmpty()) { + throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, "Missing partnerID"); } + ItemCollection businessPartner = businessPartnerService.getBusinessPartnerByID(partnerID); + if (businessPartner == null) { + throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, + "Business Partner ID '" + partnerID + "' does not exist"); + } 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); - } - } + tradeParty.setName(businessPartner.getItemValueString("partner.name")); + tradeParty.setCountryId(businessPartner.getItemValueString("partner.country")); + tradeParty.setCityName(businessPartner.getItemValueString("partner.city")); + tradeParty.setPostcodeCode(businessPartner.getItemValueString("partner.zip")); + tradeParty.setStreetAddress(businessPartner.getItemValueString("partner.address")); 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! @@ -395,80 +215,17 @@ public class KSeFAdapter implements SignalAdapter { return null; } - TradeLineItem tradeLineItem = new TradeLineItem(orderItem.getItemValueString("id")); + TradeLineItem tradeLineItem = new TradeLineItem(orderItem.getItemValueString("numpos")); - 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")); + tradeLineItem.setName(orderItem.getItemValueString("datev.text")); + tradeLineItem.setQuantity(1); + tradeLineItem.setNetPrice(orderItem.getItemValueDouble("datev.umsatz")); + tradeLineItem.setTotal(orderItem.getItemValueDouble("datev.umsatz")); + // 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. @@ -488,7 +245,7 @@ public class KSeFAdapter implements SignalAdapter { debug = Boolean.parseBoolean(config.getItemValueString("debug")); } catch (Exception e) { } - String targetName = "factur-x.xml"; + String targetName = "ksef.xml"; // adapt text.... sourceName = workflowService.adaptText(sourceName, workitem); diff --git a/pom.xml b/pom.xml index 198e75c..f04362c 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ 3.0.3 2.1.1-SNAPSHOT 1.0.2-SNAPSHOT + 1.0.3-SNAPSHOT 6.0 9.9.1-4 2.0.26 diff --git a/workflow/pl/e-invoice/examples/003_6362_251117_R.pdf b/workflow/pl/e-invoice/examples/003_6362_251117_R.pdf new file mode 100644 index 0000000..3e032cc Binary files /dev/null and b/workflow/pl/e-invoice/examples/003_6362_251117_R.pdf differ diff --git a/workflow/pl/e-invoice/examples/003_6365_251117_R.pdf b/workflow/pl/e-invoice/examples/003_6365_251117_R.pdf new file mode 100644 index 0000000..d0b6689 Binary files /dev/null and b/workflow/pl/e-invoice/examples/003_6365_251117_R.pdf differ diff --git a/workflow/pl/e-invoice/examples/80002_EX-GCA-2405-068.pdf b/workflow/pl/e-invoice/examples/80002_EX-GCA-2405-068.pdf new file mode 100644 index 0000000..9bfb99c Binary files /dev/null and b/workflow/pl/e-invoice/examples/80002_EX-GCA-2405-068.pdf differ diff --git a/workflow/pl/e-invoice/factur-x.xml b/workflow/pl/e-invoice/templates/factur-x.xml similarity index 66% rename from workflow/pl/e-invoice/factur-x.xml rename to workflow/pl/e-invoice/templates/factur-x.xml index 29f33c1..44859a4 100644 --- a/workflow/pl/e-invoice/factur-x.xml +++ b/workflow/pl/e-invoice/templates/factur-x.xml @@ -17,28 +17,20 @@ - 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: + Payment Instructions: - 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. + Please ensure that the payment reference includes the invoice number, BL or + AWB number, container or shipment number and place of loading and discharge. + The full invoice amount must be transferred without any deductions and all bank charges + must be covered by the sender. + Otherwise, the beneficiary’s bank will be unable to process the incoming payment. - Dr. Dietrich Müller GmbH + Alexander Global Logistics - Geschäftsführer: Dr. Michael Müller - - - Amtsgericht Oldenburg HRB 209026 - - - info@mueller-ahlhorn.com + Numer NIP: PL9552521552 @@ -46,27 +38,27 @@ - Dr. Dietrich Müller GmbH + Alexander Global Logistics - Müller, Michael + - +4944359710261 + - info@mueller-ahlhorn.com + - 26197 - Zeppelinring 18 - Ahlhorn - DE + 70-660 + Gdanska 36 + Szczecin + PL - info@mueller-ahlhorn.com + info@alexander-logistics.com - DE295969093 + PL9552521552 @@ -80,10 +72,10 @@ 30 - DE04 2805 0100 0092 2828 70 + PL79116022020000000654306674 - SLZODE22XXX + BIGBPLPWXXX @@ -94,9 +86,7 @@ 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 diff --git a/workflow/pl/e-invoice/templates/ksef.xml b/workflow/pl/e-invoice/templates/ksef.xml new file mode 100644 index 0000000..88b49f1 --- /dev/null +++ b/workflow/pl/e-invoice/templates/ksef.xml @@ -0,0 +1,138 @@ + + + + FA + 3 + 2026-06-05T02:45:33.422866Z + Generator Imixs + + + + #nip# + Bogusz - Sienkiewicz + + + PL + al. Różycki 7594 + 87-607 Środa Śląska + + + Lucja17@example.net + 86-588-03-53 + + + + + 3861610227 + Błaszczak - Lenart + + + PL + al. Szewczyk 7247 + 84-642 Żelechów + + + Jakubina82@gmail.com + 75-461-31-12 + + KL-2936 + 2 + 2 + + + PLN + #invoicing_date# + Lidzbark + FA/GRQMB-#invoice_number#/05/2025 + 2025-07-11 + 10652.38 + 2450.05 + 13102.43 + + 2 + 2 + 2 + 2 + + 1 + + + 1 + + 2 + + 1 + + + VAT + + 1 + 66465935-48e0-b7d9-ea88-d9d7ff4c6023 + Refined Concrete Pants + szt. + 8 + 295.81 + 2366.48 + 23 + + + 2 + 16d35141-c43f-98fc-5249-27d7b2845cf7 + Sleek Steel Pants + szt. + 10 + 402.96 + 4029.60 + 23 + + + 3 + 8647a6b5-f12a-3817-869e-010e4353686a + Incredible Steel Salad + szt. + 8 + 11.13 + 89.04 + 23 + + + 4 + 6a9b993d-33c1-eba2-f06c-7d8f22e7430b + Ergonomic Soft Fish + szt. + 8 + 34.03 + 272.24 + 23 + + + 5 + 05e42590-9bfb-10c2-a946-fd1641f705e7 + Intelligent Frozen Cheese + szt. + 3 + 501.86 + 1505.58 + 23 + + + 6 + dbb7aed5-fedb-96d7-0afd-c3d0c23740a7 + Ergonomic Metal Bike + szt. + 6 + 398.24 + 2389.44 + 23 + + + 1 + 2025-07-17 + 7 + + + \ 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 index 3ffeffa..f6640bb 100644 --- a/workflow/pl/rechnungsausgang-pl-1.1.0.bpmn +++ b/workflow/pl/rechnungsausgang-pl-1.1.0.bpmn @@ -117,6 +117,11 @@ th { font-weight: bold;} gateway_k00hrA event_AujEAA event_FYxkhQ + event_7H4Aaw + event_NT60FA + event_S9HJ1A + event_qIhyWA + gateway_utXBiw @@ -1400,6 +1405,7 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht $workflowgroup - $workflowstatus]]> sequenceFlow_4IITzA sequenceFlow_sO35Dg + sequenceFlow_jpNRMg sequenceFlow_2BSFNw @@ -1779,7 +1785,7 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + false @@ -1902,6 +1908,7 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht sequenceFlow_4IITzA sequenceFlow_UOahyg sequenceFlow_BaNbmQ + sequenceFlow_ATB00A sequenceFlow_nrxqhg sequenceFlow_6VjCnQ @@ -1928,11 +1935,11 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - sequenceFlow_6VjCnQ + sequenceFlow_0cMadw - + @@ -2015,6 +2022,178 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + sequenceFlow_ATB00A + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + sequenceFlow_VI5qMw + sequenceFlow_jpNRMg + + + sequenceFlow_VI5qMw + + + + + + + + + + + sequenceFlow_sNNdbA + + + + + + sequenceFlow_6VjCnQ + sequenceFlow_0cMadw + sequenceFlow_sNNdbA + + + + + + + @@ -2032,27 +2211,27 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + - + - + - + - + - + - + - + @@ -2062,7 +2241,7 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + @@ -2071,18 +2250,18 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + - + - + - + - + @@ -2095,15 +2274,15 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + - + - + - + @@ -2113,42 +2292,42 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -2206,24 +2385,24 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + - + - + - + - + - + - + @@ -2239,45 +2418,45 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -2287,61 +2466,91 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - + @@ -2351,53 +2560,54 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - - + + - - + + - - + + - - + + - - + + - - - - + + + + - - - - - + + + + + - - + + - + + @@ -2427,39 +2637,39 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - - - + + + - - + + - + - - + + - - + + - - + + - - + + @@ -2472,27 +2682,25 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + - + - + - + - - - - - - + + + + @@ -2500,24 +2708,24 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + - - - - - - + + + + + + - - + + - + @@ -2532,93 +2740,115 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - - + + - - + + - - + + - - + + - - + + - - - - + + + + - - - - + + + + - - + + - - - - - + + + + + - - + + - - + + - - - - + + + + - - - + + + - - + + - - + + - - - + + + - - - + + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + +