/******************************************************************************* * Imixs Workflow * Copyright (C) 2001, 2011 Imixs Software Solutions GmbH, * http://www.imixs.com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You can receive a copy of the GNU General Public * License at http://www.gnu.org/licenses/gpl.html * * Project: * http://www.imixs.org * http://java.net/projects/imixs-workflow * * Contributors: * Imixs Software Solutions GmbH - initial API and implementation * Ralph Soika - Software Developer *******************************************************************************/ package com.alexanderlogistics.api; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import org.imixs.marty.team.TeamService; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.engine.WorkflowService; import org.imixs.workflow.engine.index.UpdateService; import org.imixs.workflow.exceptions.AccessDeniedException; import org.imixs.workflow.exceptions.ModelException; import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.ProcessingErrorException; import org.imixs.workflow.exceptions.QueryException; import org.imixs.workflow.xml.XMLDataCollectionAdapter; import com.alexanderlogistics.BusinessPartnerService; import com.alexanderlogistics.InvoiceUtil; import com.alexanderlogistics.mahnlauf.MahnlaufService; import com.alexanderlogistics.xml.BusinessPartnerImportService; import jakarta.ejb.TransactionAttribute; import jakarta.ejb.TransactionAttributeType; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.xml.bind.JAXBException; /** ** Dieser Service korrigiert falsch importierte Cargosoft daten. * Ausgangsrechnungen die ein falsches Jahr haben. * * Oder DAten die über die XML Schnittstelle falsch reinkamen... * * @author rsoika * @version 1.1 */ @ApplicationScoped @Produces({ MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML, MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON, MediaType.TEXT_XML }) @Path("/cargosoft") public class CargosoftMigrationRestService implements Serializable { private static final long serialVersionUID = 1L; String log = ""; int errors = 0; int count = 0; boolean isRunning = false; @Inject DocumentService documentService; @Inject WorkflowService workflowService; @Inject MahnlaufService mahnlaufService; @Inject UpdateService updateService; @Inject TeamService teamService; @Inject BusinessPartnerService businessPartnerService; @Inject BusinessPartnerImportService businessPartnerImportService; private static Logger logger = Logger.getLogger(CargosoftMigrationRestService.class.getName()); public CargosoftMigrationRestService() { super(); } /** * Dieser agent speichert einfach alle Cargosoft Krediotren/Debitoren Daten , * was dann zur automatischen Neuanlage bzw. aktualisierung des businesspartner * workflows führt. * * * curl -H "Cookie: * JSESSIONID=ShAegfNiXmqLFLSFwcUZ7hrLLQDKZGKiu4oj_xeX.imixs-office-workflow-8586d7d58d-h6k4q" * https://alexander-logistics-dwc.office-workflow.de/api/cargosoft/bp-sync?maxcount=1000 * * @return * @throws QueryException * @throws AccessDeniedException * @throws ProcessingErrorException * @throws PluginException * @throws ModelException */ // @GET // @Path("/bp-sync") // @Produces({ MediaType.TEXT_PLAIN }) public String syncBusinessPartner(@QueryParam("maxcount") int maxcount) throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException { StringBuffer messageBuffer = new StringBuffer(); if (maxcount <= 0) { maxcount = 100; } String query = "(type:cargosoftkreditor)"; int syncs = 0; long l = System.currentTimeMillis(); int batchSize = 100; int totalObjects = 0; log("├── sync business partner objects....", messageBuffer); if (isRunning) { log("├── sync process already running!", messageBuffer); return messageBuffer.toString(); } isRunning = true; int totalCount = documentService.count(query); log("│   ├── found " + totalCount + " cargosoft entries", messageBuffer); // Berechne Anzahl der benötigten Pages int totalPages = (int) Math.ceil((double) totalCount / batchSize); // Verarbeite Page für Page for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) { List cargosoftDataList = documentService.find(query, batchSize, pageIndex); for (ItemCollection cargosoftItemCol : cargosoftDataList) { totalObjects++; if (businessPartnerImportService.syncCargosoftBusinessPartner(cargosoftItemCol) == true) { updateService.updateIndex(); syncs++; if (syncs >= maxcount) break; } } // Fortschritt loggen log("│ ├── Processed page " + (pageIndex + 1) + " of " + totalPages + " (" + totalObjects + " objects read, " + syncs + " total syncs)", messageBuffer); // Optional: Kurze Pause nach jedem 5. Batch try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } if (syncs >= maxcount) break; } long duration = System.currentTimeMillis() - l; double objectsPerSecond = totalObjects / (duration / 1000.0); log("├── Successfully " + syncs + " business partner objects synced in " + duration + "ms (" + String.format("%.1f", objectsPerSecond) + " objects/sec)", messageBuffer); isRunning = false; return messageBuffer.toString(); } /** * Migriert die Rechnungen mit den neuen cdtr. Nummern * * */ // @GET // @Path("/cargosoft-renew_cdtr_number") // @Produces({ MediaType.TEXT_PLAIN }) public String migrationAdresswandlungOpenInvoices(@QueryParam("maxcount") int maxcount, @QueryParam("type") String type) throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException { StringBuffer messageBuffer = new StringBuffer(); if (maxcount <= 0) { maxcount = 10; } if (type == null || type.isEmpty()) { type = "workitem"; } if (isRunning) { log("├── sync process already running!", messageBuffer); return messageBuffer.toString(); } isRunning = true; log("├── read cargosoft migration file....", messageBuffer); Map mapping = readAdressMapping(); log("├── found " + mapping.size() + " mappings", messageBuffer); String query = "(type:" + type + ") AND ($modelversion:rechnungseingang*)"; // int syncs = 0; long l = System.currentTimeMillis(); int batchSize = 100; int totalUpdates = 0; log("├── migration cargosoft addresses....", messageBuffer); log("│ ├── query=" + query, messageBuffer); int totalCount = documentService.count(query); log("│ ├── found " + totalCount + " invoices", messageBuffer); // Berechne Anzahl der benötigten Pages int totalPages = (int) Math.ceil((double) totalCount / batchSize); // Verarbeite Page für Page for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) { log("│ ├── read page " + pageIndex, messageBuffer); List invoiceList = documentService.find(query, batchSize, pageIndex); long l1 = System.currentTimeMillis(); log("│ ├── ...verifying " + invoiceList.size() + " invoices...", messageBuffer); int updates = migratateInvioceBPAddress(invoiceList, mapping); totalUpdates = totalUpdates + updates; log("│ ├── " + updates + " invoices migrated in " + (System.currentTimeMillis() - l1) + "ms ", messageBuffer); // break; if (totalUpdates >= maxcount) { break; } } long duration = System.currentTimeMillis() - l; double objectsPerSecond = totalUpdates / (duration / 1000.0); log("├── Successfully " + totalUpdates + " invoices migrated in " + duration + "ms (" + String.format("%.1f", objectsPerSecond) + " objects/sec)", messageBuffer); isRunning = false; return messageBuffer.toString(); } @TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW) private int migratateInvioceBPAddress(List invoices, Map map) { int count = 0; for (ItemCollection invoice : invoices) { String cdtrNumber = invoice.getItemValueString("cdtr.number"); if (cdtrNumber.isBlank()) { logger.warning(" ..Invoice " + invoice.getUniqueID() + " has no cdtr.number!"); continue; } // logger.info("==> check cdtr.number: " + cdtrNumber); // K überspringen String newNumber = map.get(cdtrNumber.substring(1)); if (newNumber != null) { // migrate number invoice.setItemValue("cdtr.number.old", cdtrNumber); invoice.setItemValue("cdtr.number", "K" + newNumber); logger.info("│ ├── replace " + cdtrNumber + " -> K" + newNumber + " : " + invoice.getUniqueID()); documentService.save(invoice); // logger.info("-- kein update! disabled save!!"); count++; } } return count; } /** * Migriert die Rechnungen mit den neuen cdtr. Nummern * * */ // @GET // @Path("/fix-separun") // @Produces({ MediaType.TEXT_PLAIN }) public String fixSepaRun(@QueryParam("maxcount") int maxcount) throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException { StringBuffer messageBuffer = new StringBuffer(); int totalUpdates = 0; if (maxcount <= 0) { maxcount = 10; if (isRunning) { log("├── sync process already running!", messageBuffer); return messageBuffer.toString(); } isRunning = true; String query = "(type:workitem) AND ($modelversion:rechnungseingang*) AND ($taskid:5500)"; // int syncs = 0; long l = System.currentTimeMillis(); log("├── migration sepa run....", messageBuffer); log("│ ├── query=" + query, messageBuffer); int totalCount = documentService.count(query); log("│ ├── found " + totalCount + " invoices", messageBuffer); List invoiceList = documentService.find(query, 999, 0); long l1 = System.currentTimeMillis(); log("│ ├── ...verifying " + invoiceList.size() + " invoices...", messageBuffer); for (ItemCollection invoice : invoiceList) { String cdtrNumber = invoice.getItemValueString("cdtr.number"); String cdtrNumberOld = invoice.getItemValueString("cdtr.number.old"); String bpPartnerID = invoice.getItemValueString("partner.id"); if (bpPartnerID.isEmpty()) { continue; } if (cdtrNumberOld.isEmpty()) { continue; } // lookup bp .... ItemCollection bpPartner = businessPartnerService.getBusinessPartnerByID(InvoiceUtil.getBPID(invoice)); String fixPartnerName = bpPartner.getItemValueString("partner.name"); String fixPartnerId = bpPartner.getItemValueString("partner.id"); if (!bpPartnerID.equals(fixPartnerId)) { logger.info("Problem found with invoice: " + invoice.getUniqueID() + " wrong partnerID=" + bpPartnerID + " correct= " + fixPartnerId); logger.info(" correct name= " + fixPartnerName); invoice.setItemValue("partner.id", fixPartnerId); invoice.setItemValue("cdtr.name", fixPartnerName); invoice.setItemValue("partner.name", fixPartnerName); documentService.save(invoice); // workflowService.processWorkItem(invoice.event(10)); totalUpdates++; } } log("│ ├── " + totalUpdates + " invoices migrated in " + (System.currentTimeMillis() - l1) + "ms ", messageBuffer); } log("├── Successfully " + totalUpdates + " invoices migrated", messageBuffer); isRunning = false; return messageBuffer.toString(); } /** * Diese hilfsmethode liest die Cargosoft Datei mit den Mapping - alt->neu ein * * @return */ private Map readAdressMapping() { Map result = new HashMap<>(); try (InputStream inputStream = getClass().getClassLoader() .getResourceAsStream("agl_wandlung_20250822.csv"); BufferedReader reader = new BufferedReader( new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { String line; boolean firstLine = true; while ((line = reader.readLine()) != null) { if (firstLine) { firstLine = false; continue; // Header überspringen } String[] parts = line.split(";"); if (parts.length == 2) { String oldID = parts[0].trim(); String newID = parts[1].trim(); result.put(oldID, newID); } } } catch (IOException e) { // Logging framework verwenden System.err.println("Fehler beim Laden der CSV: " + e.getMessage()); } return result; } /** * Hilfsmethode speichert eine cargoosft kreditor object... * * @param bpID * @param messageBuffer * @return */ @TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW) public boolean syncBusinessPartner(ItemCollection cargosoftItemCol) { String bpip = InvoiceUtil.buildBPID(cargosoftItemCol.getItemValueString("name")); if (businessPartnerService.getBusinessPartnerByID(bpip) == null) { // not available so we save just the cargosoft entry once... documentService.save(cargosoftItemCol); return true; } return false; } /** * Dieser agent löscht einfach alle Businesspartner objekte. Wird eigentlich * nicht mehr benötigt. * * curl -H "Cookie: * JSESSIONID=ShAegfNiXmqLFLSFwcUZ7hrLLQDKZGKiu4oj_xeX.imixs-office-workflow-8586d7d58d-h6k4q" * https://alexander-logistics-dwc.office-workflow.de/api/cargosoft/bp-delete?maxcount=10000 * * @return * @throws QueryException * @throws AccessDeniedException * @throws ProcessingErrorException * @throws PluginException * @throws ModelException */ // @GET // @Path("/bp-delete") // @Produces({ MediaType.TEXT_PLAIN }) public String deleteBusinessPartner(@QueryParam("maxcount") int maxcount) throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException { StringBuffer messageBuffer = new StringBuffer(); String query = "(type:workitem*) AND ($modelversion: businesspartner*)"; int deletions = 0; long l = System.currentTimeMillis(); int batchSize = 100; int totalObjects = 0; log("├── delete old business partner objects....", messageBuffer); if (isRunning) { log("├── sync process already running!", messageBuffer); return messageBuffer.toString(); } isRunning = true; log("│   ├── maxcount= " + maxcount, messageBuffer); int totalCount = documentService.count(query); log("│   ├── found " + totalCount + " entities ", messageBuffer); // Berechne Anzahl der benötigten Pages int totalPages = (int) Math.ceil((double) totalCount / batchSize); // Verarbeite Page für Page int iterations = 0; while (deletions < maxcount) { List cargosoftDataList = documentService.find(query, batchSize, 0); if (cargosoftDataList.size() == 0) { break; } for (ItemCollection cargosoftItemCol : cargosoftDataList) { totalObjects++; if (deleteBusinessPartner(cargosoftItemCol, messageBuffer)) { deletions++; } if (deletions >= maxcount) break; } // Explicit flush the lucene search event log updateService.updateIndex(); // Fortschritt loggen iterations++; log("│ ├── Processed page " + (iterations) + " of " + totalPages + " (" + deletions + " total deletions)", messageBuffer); // Optional: Kurze Pause nach jedem 5. Batch try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } if (deletions >= maxcount) break; } long duration = System.currentTimeMillis() - l; double objectsPerSecond = totalObjects / (duration / 1000.0); log("├── Successfully " + deletions + " business partner objects deleted in " + duration + "ms (" + String.format("%.1f", objectsPerSecond) + " objects/sec)", messageBuffer); isRunning = false; return messageBuffer.toString(); } /** * Entfernt doppelte Eintrage - sind irgnedwie entstandnen :((( * * @return * @throws QueryException * @throws AccessDeniedException * @throws ProcessingErrorException * @throws PluginException * @throws ModelException */ // @GET // @Path("/remove-dubletten") // @Produces({ MediaType.TEXT_PLAIN }) public String deleteCargosoftDupplicates(@QueryParam("maxcount") int maxcount) throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException { StringBuffer messageBuffer = new StringBuffer(); String query = "(type:cargosoftkreditor)"; int deletions = 0; long l = System.currentTimeMillis(); int batchSize = 100; int totalObjects = 0; log("├── delete cargosoft dupplicaates....", messageBuffer); if (isRunning) { log("├── sync process already running!", messageBuffer); return messageBuffer.toString(); } isRunning = true; log("│   ├── maxcount= " + maxcount, messageBuffer); int totalCount = documentService.count(query); log("│   ├── found " + totalCount + " cargosoft entries", messageBuffer); // Berechne Anzahl der benötigten Pages int totalPages = (int) Math.ceil((double) totalCount / batchSize); // Verarbeite Page für Page for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) { List cargosoftDataList = documentService.find(query, batchSize, pageIndex); if (cargosoftDataList.size() == 0) { break; } for (ItemCollection cargosoftItemCol : cargosoftDataList) { totalObjects++; String name = cargosoftItemCol.getItemValueString("name"); String subQuery = "(type:cargosoftkreditor) AND (name:" + name + ")"; List dupplicateList = documentService.find(subQuery, 100, 0, "$modified", true); if (dupplicateList.size() > 1) { // alle bis auf den ersten löschen log("│   ├── found duplicate: " + name, messageBuffer); boolean first = true; for (ItemCollection dubel : dupplicateList) { if (first) { first = false; continue; } // löschen! documentService.remove(dubel); deletions++; } } if (deletions >= maxcount) break; } // Explicit flush the lucene search event log updateService.updateIndex(); // Fortschritt loggen // Fortschritt loggen log("│ ├── Processed page " + (pageIndex + 1) + " of " + totalPages + " (" + deletions + " total deletions)", messageBuffer); // Optional: Kurze Pause nach jedem 5. Batch try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } if (deletions >= maxcount) break; } long duration = System.currentTimeMillis() - l; double objectsPerSecond = totalObjects / (duration / 1000.0); log("├── Successfully " + deletions + " duplicated cargosoft objects deleted in " + duration + "ms (" + String.format("%.1f", objectsPerSecond) + " objects/sec)", messageBuffer); isRunning = false; return messageBuffer.toString(); } /** * Hilfsmethode - löscht ein cargoosoft object... * * @param bpID * @param messageBuffer * @return */ @TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW) public boolean deleteBusinessPartner(ItemCollection cargosoftItemCol, StringBuffer messageBuffer) { documentService.remove(cargosoftItemCol); return true; } /** * Importiert vom Server Filesystem eine XML Datei * * Dies dient zum testen in dem wir unter /transfer/ xml Daten aus der * Produktion ablegen die wir so importiern können * * */ @GET @Path("/import") @Produces({ MediaType.TEXT_PLAIN }) public String importXMLFile(@QueryParam("file") String filePath) throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException { StringBuffer messageBuffer = new StringBuffer(); if (filePath != null && filePath.endsWith(".xml")) { log("├── import xml file: " + filePath, messageBuffer); List col = null; try (InputStream inputStream = new FileInputStream(filePath)) { col = XMLDataCollectionAdapter.readCollectionFromInputStream(inputStream); if (col != null) { log("├── import ok - " + col.size() + " documents found", messageBuffer); // Mahnwesen Process = 227af2bc-22ad-4332-8781-98ff853ca3fd // documentService.find(filePath, totalUpdates, totalUpdates) for (ItemCollection workitem : col) { workitem.setItemValue("process.ref", "227af2bc-22ad-4332-8781-98ff853ca3fd"); workitem.removeItem("$immutable"); workitem.removeItem("$snapshotid"); workitem.setType("workitem"); // fix id String id = workitem.getUniqueID(); id = id.substring(0, id.lastIndexOf("-")); workitem.setItemValue("$uniqueid", id); log("├── import : " + id, messageBuffer); documentService.save(workitem); } } else { log("├── Import Failed: no data found!", messageBuffer); } } catch (JAXBException | IOException e) { log("├── Import Failed: " + e.getMessage(), messageBuffer); } log("├── Import finished", messageBuffer); } return messageBuffer.toString(); } private void log(String message, StringBuffer messageLog) { logger.info(message); messageLog.append(message + "\n"); } }