package com.alexanderlogistics.einvoice; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.logging.Logger; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; 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.EInvoiceModelKSeF; import org.imixs.einvoice.EInvoiceNS; 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 org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.alexanderlogistics.BusinessPartnerService; import com.alexanderlogistics.InvoiceService; import com.alexanderlogistics.InvoiceUtil; import com.alexanderlogistics.xml.CargosoftXMLInvoiceImportService; 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
         

   }
 * 
* *

* NIP: The adapter needs the NIP (vat-id). If we do not find the partner.vat in * the business object, we do a lookup on the D-Cargosoft object and try the vat * id from there. This is because the partner.vat is a field which was not * defined before. * *

* 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 KSeFAdapterDeprecated 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 API_ERROR = "API_ERROR"; public static final String BUSINESSPARTNER_ERROR = "BUSINESSPARTNER_ERROR"; public static final String LINE_ITEMS_PROPERTY = "invoice.items"; private static Logger logger = Logger.getLogger(EInvoiceAdapter.class.getName()); public static SimpleDateFormat dateFormatter = new SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN); public static NumberFormat numberFormat = NumberFormat.getInstance(Locale.GERMANY); boolean debug = false; @Inject WorkflowService workflowService; @Inject DocumentService documentService; @Inject SnapshotService snapshotService; @Inject BusinessPartnerService businessPartnerService; @Inject InvoiceService invoiceService; /** * 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, "ksef", workitem, false); if (eInvoiceConfig == null || !eInvoiceConfig.hasItem("CREATE")) { throw new PluginException(EInvoiceAdapter.class.getSimpleName(), CONFIG_ERROR, "missing e-invoice/ksef configuration in model event - please check model configuration"); } ItemCollection ksefCreateDefinition = XMLParser.parseItemStructure(eInvoiceConfig.getItemValueString("CREATE")); try { // Load the e-invoice template.... FileData xmlFileData = loadXMLTemplate(workitem, ksefCreateDefinition); updateEInvoice(xmlFileData, workitem); // append XML document logger.info("│ ├── attach KSeF e-invoice..."); workitem.addFileData(xmlFileData); } catch (PluginException e) { throw new AdapterException(e); } return workitem; } /** * 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 */ public void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException { try { EInvoiceModel model = EInvoiceModelFactory.read(new ByteArrayInputStream(fileDataXMLTemplate.getContent())); model.setId(workitem.getItemValueString("invoice.number")); // date model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date")); // Set Performance Date if (workitem.getItemValueDate("invoice.performancedate") == null) { // hilfs code um das invoice.performancedate nachträglich zu parsen syncPerformanceDate(workitem); } ((EInvoiceModelKSeF) model) .setPerformanceDateTime(workitem.getItemValueLocalDate("invoice.performancedate")); // Update Addresses TradeParty billingAddress = buildAddress(workitem, "buyer", model); model.setTradeParty(billingAddress); // set Tax Type based on vatID - as a result taxType is 1=Poland 2=EU 3=Other ((EInvoiceModelKSeF) model).setTaxType(workitem.getItemValueString("partner.vat")); workitem.setItemValue("invoice.tax.type", ((EInvoiceModelKSeF) model).getTaxType()); // Update Invoice Type (RodzajFaktury) -> VAT | KOR Element elementFa = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Fa"); if (isKorrekturRechnung(workitem)) { ((EInvoiceModelKSeF) model).setRodzajFaktury("KOR"); logger.info("│ ├── invoice type=KOR"); // Set correction data (DaneFaKorygowanej) - required for KOR invoices Element daneFaKorygowanej = model.findOrCreateChildNodeAfter( elementFa, EInvoiceNS.KSEF, "DaneFaKorygowanej", "RodzajFaktury"); Date correctionInvoiceDate = workitem.getItemValueDate("invoice.date"); // try to lookup original invoice by invoice.CorrectionInvoiceNumber ItemCollection correctionInvoice = invoiceService .findOutboundInvoiceByNumber(workitem.getItemValueString("invoice.CorrectionInvoiceNumber")); if (correctionInvoice != null) { correctionInvoiceDate = correctionInvoice.getItemValueDate("invoice.date"); logger.info("│ ├── found original invoice, date: " + correctionInvoiceDate); } else { logger.info("│ ├── ⚠️ original invoice not found, using current date as fallback"); } // Original invoice date (DataWystFaKorygowanej) - required if (correctionInvoiceDate != null) { SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); String sCorrectionDate = formatter.format(correctionInvoiceDate); model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF, "DataWystFaKorygowanej", sCorrectionDate); } // Original invoice number (NrFaKorygowanej) - required model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF, "NrFaKorygowanej", workitem.getItemValueString("invoice.CorrectionInvoiceNumber")); // NrKSeFN = 1 means the original invoice was issued outside KSeF model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF, "NrKSeFN", "1"); } else { // normale rechnung ((EInvoiceModelKSeF) model).setRodzajFaktury("VAT"); logger.info("│ ├── invoice type=VAT"); } // set currency model.updateElementValue(elementFa, EInvoiceNS.KSEF, "KodWaluty", workitem.getItemValueString("invoice.currency")); // Update Invoice Items // Here we compute the pos number independent if the _childitems hold a posnum. // This is because a new imported cargosoft invoice does not provide numPos // items in the child list. So it is important to generate the pos numbers here // one by one List invoiceItems = InvoiceUtil.explodeChildList(workitem, "_childitems"); double lineTotalAmount = 0.00; int pos = 1; for (ItemCollection invoiceItem : invoiceItems) { TradeLineItem tradeLineItem = buildTradeLineItem(invoiceItem, pos); model.setTradeLineItem(tradeLineItem); lineTotalAmount = lineTotalAmount + tradeLineItem.getTotal(); pos++; } // Summenbildung /* * Field mapping: *

*/ model.setNetTotalAmount(workitem.getItemValueDouble("invoice.total.net")); // ?? has no function model.setTaxRate(workitem.getItemValueDouble("invoice.total.tax")); // Tax /* * P_14_1 * * This call also generate P_14_1W bei fremdwährung */ model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.total") - workitem.getItemValueDouble("invoice.total.net"))); /* * If we have Foreign Currency we need to set P_14_1W in case we are tax type 1 */ if (!"PLN".equals(workitem.getItemValueString("invoice.currency"))) { if ("1".equals(((EInvoiceModelKSeF) model).getTaxType())) { // compute rate double rate = workitem.getItemValueDouble("invoice.rate"); logger.info("│ ├── rate=" + rate); double totalTax = InvoiceUtil.round(workitem.getItemValueDouble("invoice.total.tax")); logger.info("│ ├── total.tax=" + totalTax); BigDecimal value = BigDecimal.valueOf(totalTax) .divide(BigDecimal.valueOf(rate), 10, RoundingMode.HALF_UP); // P_14_1W must come directly after P_14_1 logger.info("│ ├── P_14_1W=" + value); Element element = model.findOrCreateChildNodeAfter(elementFa, EInvoiceNS.KSEF, "P_14_1W", "P_14_1"); element.setTextContent(value.setScale(2, RoundingMode.HALF_UP).toPlainString()); } } // Brutto /* * P_15 */ model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.total")); /* * KursWalutyZ * * Für Rechnungen in Fremdwährung obligatorisch. Anzuwenden ist der NBP- * Durchschnittskurs des Tages vor Entstehen der Steuerpflicht (Art. 31a * UStG-PL). * Dezimalformat mit Punkt. */ String currency = workitem.getItemValueString("invoice.currency"); if (!currency.isBlank() && !"PLN".equals(currency)) { Element p15 = model.findChildNode(elementFa, EInvoiceNS.KSEF, "P_15"); if (p15 != null) { Double rate = workitem.getItemValueDouble("invoice.rate"); logger.info("│ ├── set KursWalutyZ = " + rate); Element element = model.findOrCreateChildNodeAfter(elementFa, EInvoiceNS.KSEF, "KursWalutyZ", "P_15"); element.setTextContent(rate.toString()); } } // finally set the due date at the end of the XML tree model.setDueDateTime(workitem.getItemValueLocalDate("invoice.duedate")); /* * finally update the template file */ fileDataXMLTemplate.setContent(model.getContent()); } catch (FileNotFoundException | EInvoiceFormatException | TransformerException e) { throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, e.getMessage(), e); } } /** * Dies ist eine Hilfsmethode die nachträglich das invoice.performancedate aus * dem Cargosoft XML ausliest * * @param workitem * @throws PluginException */ private void syncPerformanceDate(ItemCollection workitem) throws PluginException { if (!workitem.hasItem("invoice.performancedate")) { try { logger.info("----Resync cargosoft performancedate...."); DocumentBuilder documentBuilder; // hole die XML Datei FileData cargoXML = snapshotService.getWorkItemFile(workitem.getUniqueID(), workitem.getItemValueString("cargosoft.import.filename")); if (cargoXML != null) { InputStream inputStream = new ByteArrayInputStream(cargoXML.getContent()); InputSource inputSource = new InputSource(inputStream); documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = documentBuilder.parse(inputSource); // Performance Date CargosoftXMLInvoiceImportService.readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/PerformanceDate", workitem, "invoice.performancedate", Date.class); // falls keines gefunden wurde nehmen wir das invoice date if (workitem.getItemValueDate("invoice.performancedate") == null) { workitem.setItemValue("invoice.performancedate", workitem.getItemValueDate("invoice.date")); } } } catch (ParserConfigurationException | SAXException | IOException e) { throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(), "XML_ERROR", e.getMessage()); } } } /** * Gibt True zurück wenn es eine korrektur rechnung ist * * InvoiceHeader/Correction=true * InvoiceHeader/CorrectionInvoiceNumber !empty * * * @param workitem * @return */ private boolean isKorrekturRechnung(ItemCollection workitem) { return ("true".equals(workitem.getItemValueString("invoice.correction")) && !workitem.getItemValueString("invoice.CorrectionInvoiceNumber").isEmpty()); } /** * Erstellt ein TradeParty Objekt aus einer Liste von Adresszeilen. * * @param addressLines Liste der Adresszeilen * @param type Der Typ der TradeParty * @return TradeParty */ public TradeParty buildAddress(ItemCollection workitem, String type, EInvoiceModel model) throws PluginException { String partnerID = workitem.getItemValueString("partner.id"); 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"); } /** * Migration: * If the business partner does not yet have the item "partner.vat" we try here * to resync this information from the cargosoftkreditor object. This is needed * because the partner.vat was not initially defined by the synch processor and * so many partner objects do not yet provide this information. */ if (businessPartner.getItemValueString("partner.vat").isEmpty()) { logger.info("│ ├── BusinessPartner " + partnerID + " does not provide vat-id - try to migrate...."); String dNumber = businessPartner.getItemValueString("dbtr.number"); if (!dNumber.isEmpty()) { try { String query = "(type:cargosoftkreditor) AND (name:" + dNumber + ")"; List result; result = documentService.find(query, 1, 0); if (result != null && result.size() > 0) { ItemCollection creditorData = result.get(0); String vatID = creditorData.getItemValueString("_vendor_vat_registration_id"); logger.info("│ ├── synchronize VAT Registrytion ID: " + vatID); // update business partner businessPartner.setItemValue("partner.vat", vatID); documentService.saveByNewTransaction(businessPartner); } } catch (Exception e) { // should not happen! logger.info("│ ├── ⚠️ Failed to sync BusinessPartner : " + e.getMessage()); } } } TradeParty tradeParty = new TradeParty(type); // Name ist immer die erste Zeile 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")); if ("buyer".equals(type)) { // if we do NOT have a partner.vat we can not upload the invoice! String partnerVAT = businessPartner.getItemValueString("partner.vat"); if (partnerVAT.isBlank()) { // Just a warning logger.warning("│ ├── ⚠️ Business Partner ID '" + partnerID + "' does not contain a VAT ID !"); // throw new PluginException(this.getClass().getName(), BUSINESSPARTNER_ERROR, // "Business Partner ID '" + partnerID + "' does not contain a VAT ID !"); } workitem.setItemValue("partner.vat", partnerVAT); tradeParty.setVatNumber(partnerVAT); // Update or create NrKlienta element directly under Podmiot2 Element podmiot2 = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Podmiot2"); model.updateElementValue(podmiot2, EInvoiceNS.KSEF, "NrKlienta", businessPartner.getItemValueString("dbtr.number")); } return tradeParty; } /** * Parses the data list of an invoice line and returns a TradeLineItem object. * The order of the list items must be exactly! * * # 16.04.2026 * Laut Herrn Grzegorz Grzelczyk dürfen in den Rechnungspositionen nur netto * werte eingetragen sein. Siehe buildTradeLineItem() * * Des weiteren soll anstatt datev.text (pos nummer) der original billing text * ausgewiesen werden */ private TradeLineItem buildTradeLineItem(ItemCollection orderItem, int pos) { if (orderItem == null) { return null; } // TradeLineItem tradeLineItem = new // TradeLineItem(orderItem.getItemValueString("numpos")); // We do not trust the item 'numPos' here because it can be empty for new // invoices! TradeLineItem tradeLineItem = new TradeLineItem(pos + ""); // Herrn Grzegorz Grzelczyk möchte hier BillingText // tradeLineItem.setName(orderItem.getItemValueString("datev.text")); // tradeLineItem.setName(orderItem.getItemValueString("billingtext")); // Herr Grzelczyk will das alle billing texte aus der liste, verkettet // ausgegeben werden. List billingTexts = orderItem.getItemValue("billingtext"); String billingTextName = billingTexts == null ? "" : String.join(", ", billingTexts); final int MAX_LENGTH = 512; if (billingTextName.length() > MAX_LENGTH) { billingTextName = billingTextName.substring(0, MAX_LENGTH); } tradeLineItem.setName(billingTextName); tradeLineItem.setQuantity(1); double vat = orderItem.getItemValueDouble("datev.vatrate"); // 23.00 double netto = orderItem.getItemValueDouble("datev.umsatz"); // 970.00 // # 16.04.2026 // Der total bezieht sich laut Herrn Grzegorz Grzelczyk ausschließlich auf den // Nettowert x Menge. // Der Brutto Wert ist hier falsch! Da die Menge immer 1 ist ist das beide male // der selbe wert // WRONG: tradeLineItem.setTotal(brutto); // double brutto = netto * (1 + (vat / 100)); // double steuer = brutto - netto; tradeLineItem.setTaxRate(vat); tradeLineItem.setNetPrice(netto); tradeLineItem.setTotal(netto); // menge x netto return tradeLineItem; } /** * 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 = "ksef.xml"; // adapt text.... sourceName = workflowService.adaptText(sourceName, workitem); if ((template == null || template.isEmpty()) || (textblock == null || textblock.isEmpty())) { throw new PluginException(EInvoiceAdapter.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(EInvoiceAdapter.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; } }