office-alexander-logistics/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/xml/CargosoftXMLInvoiceImportService.java
2026-04-28 16:44:10 +02:00

867 lines
38 KiB
Java

/*
* Imixs-Workflow
*
* Copyright (C) 2001-2020 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:
* https://www.imixs.org
* https://github.com/imixs/imixs-workflow
*
* Contributors:
* Imixs Software Solutions GmbH - Project Management
* Ralph Soika - Software Developer
*/
package com.alexanderlogistics.xml;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPSClient;
import org.imixs.workflow.FileData;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.engine.ModelService;
import org.imixs.workflow.engine.WorkflowService;
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.importer.DocumentImportEvent;
import org.imixs.workflow.importer.DocumentImportService;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.alexanderlogistics.InvoiceUtil;
import com.alexanderlogistics.mahnlauf.MahnlaufService;
import jakarta.ejb.EJB;
import jakarta.ejb.Stateless;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
/**
* Der CargosoftXMLInvoiceImportImportService erweitert den FTPImportService und
* kann XML Invoice Dokumente von Cargosoft importieren. Der Service reagiert
* auf DocumentImportEvents und kann über das Options Feld konfiguriert werden.
* <p>
* Im Gegensatz zum FTPImportService werden die XML Dateien nach einem
* speziellen Verfahren geparsed und dann als Ausgangsrechnungen importiert. Der
* Service ersetzt den alten DATEV Import. Falls eine Rechnung schon existiert
* (Rechnungsnummer) wird der Datensatz vom FTP Laufwerk gelöscht und nicht
* importiert.
* <p>
* In den Optionen muss die option "mandant.id" hinterlegt sein. Stimmt diese
* beim Import nicht mit der Mandanten Nummer in der XML Datei überein, wird das
* XML Dokument gelöscht und nicht importiert.
*
*
*
* @author rsoika
*
*/
@Stateless
public class CargosoftXMLInvoiceImportService {
private static Logger logger = Logger.getLogger(CargosoftXMLInvoiceImportService.class.getName());
public static final String OPTION_MANDANT_ID = "mandant.id";
public static final String TYPE_CARGOSOFTKREDITOR = "cargosoftkreditor";
public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
public static final String REGEX_IMPORTTEXTPATTERN = "^R[1-8] .*$";
@EJB
WorkflowService workflowService;
@EJB
DocumentService documentService;
@EJB
ModelService modelService;
@EJB
DocumentImportService documentImportService;
@Inject
MahnlaufService mahnlaufService;
/**
* This method reacts on a CDI ImportEvent and reads documents form a ftp
* server.
*
*
*/
public void onEvent(@Observes DocumentImportEvent event) {
// check if source is already completed
if (event.getResult() == DocumentImportEvent.PROCESSING_COMPLETED) {
logger.finest("...... import source already completed - no processing will be performed.");
return;
}
if (!"CARGOSOFT_INVOICE_XML".equalsIgnoreCase(event.getSource().getItemValueString("type"))) {
// ignore data source
logger.finest("...... type '" + event.getSource().getItemValueString("type") + "' skipped.");
return;
}
Properties sourceOptions = documentImportService.getOptionsProperties(event.getSource());
// Read Mandand ID form Option list
String mandantID = sourceOptions.getProperty(OPTION_MANDANT_ID, "");
if (mandantID == null || mandantID.isEmpty()) {
documentImportService.logMessage("...CARGOSOFT_INVOICE_XML Import failed - missing mandant.id in options!",
event);
event.setResult(DocumentImportEvent.PROCESSING_ERROR);
return;
}
documentImportService.logMessage("...CARGOSOFT_INVOICE_XML Import started: mandant.id=" + mandantID, event);
String ftpServer = event.getSource().getItemValueString(DocumentImportService.SOURCE_ITEM_SERVER);
String ftpPort = event.getSource().getItemValueString(DocumentImportService.SOURCE_ITEM_PORT);
String ftpUser = event.getSource().getItemValueString(DocumentImportService.SOURCE_ITEM_USER);
String ftpPassword = event.getSource().getItemValueString(DocumentImportService.SOURCE_ITEM_PASSWORD);
String ftpPath = event.getSource().getItemValueString(DocumentImportService.SOURCE_ITEM_SELECTOR);
if (!ftpPath.startsWith("/") && !ftpPath.startsWith("./")) {
ftpPath = "/" + ftpPath;
}
if (!ftpPath.endsWith("/")) {
ftpPath = ftpPath + "/";
}
// if no server is given we exit
if (ftpServer.isEmpty()) {
logger.warning("...... no server specified!");
return;
}
if (ftpPort.isEmpty()) {
// set default port
ftpPort = "21";
}
try {
documentImportService.logMessage("...connecting to FTP server: " + ftpServer, event);
documentImportService.logMessage("...working directory: " + ftpPath, event);
FTPClient ftpClient = connectServer(ftpServer, Integer.parseInt(ftpPort), ftpUser, ftpPassword);
scannFTPDirectory(ftpClient, ftpPath, mandantID, event);
} catch (PluginException pe) {
documentImportService.logMessage(pe.getMessage(), event);
event.setResult(DocumentImportEvent.PROCESSING_ERROR);
return;
}
// completed
event.setResult(DocumentImportEvent.PROCESSING_COMPLETED);
}
/**
* Connects to the FTP Server and returns a ftpClient instance.
*/
private FTPClient connectServer(String ftpServer, int ftpPort, String ftpUser, String ftpPassword)
throws PluginException {
FTPClient ftpClient = null;
// TLS
ftpClient = new FTPSClient("TLS", false);
ftpClient.setControlEncoding("UTF-8");
try {
ftpClient.connect(ftpServer, ftpPort);
if (ftpClient.login(ftpUser, ftpPassword) == false) {
throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(), "FTP_ERROR",
"FTP file transfer failed: login failed!");
}
ftpClient.enterLocalPassiveMode();
logger.finest("...... FileType=" + FTP.BINARY_FILE_TYPE);
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
ftpClient.setControlEncoding("UTF-8");
} catch (IOException e) {
throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(), "FTP_ERROR", e.getMessage());
}
return ftpClient;
}
/**
* Helper method reads the working FTP directory and starts importing the files.
*
* @param ftpClient
* @param ftpPath
* @throws PluginException
*/
private void scannFTPDirectory(FTPClient ftpClient, String ftpPath, String mandantID, DocumentImportEvent event)
throws PluginException {
try {
// load spaces Pos Expressions
Map<String, List<String>> spacePosMappings = mahnlaufService.loadSpacePosMappings();
logger.finest("......read directory " + ftpPath);
// try to enter the working directory and read all files...
boolean bWorkingDir = ftpClient.changeWorkingDirectory(ftpPath);
if (bWorkingDir == false) {
throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(), "FTP_ERROR",
"...failed to change into working directory: ");
}
FTPFile[] allFiles = ftpClient.listFiles(ftpPath);
// FTPFile[] allFiles = ftpClient.listFiles();
int count = 0;
if (allFiles.length > 0) {
documentImportService.logMessage("..." + allFiles.length + " files found ", event);
for (FTPFile file : allFiles) {
// if this is a directory or symlink then we do ignore this entry
if (!file.isFile()) {
logger.warning("...'" + file.getName() + "' is not a valid file, object will be ignored!");
continue;
}
ItemCollection invoice = null;
logger.info("import file " + file.getName() + "...");
// String fullFileName = ftpPath + "/" + file.getName();
try (ByteArrayOutputStream is = new ByteArrayOutputStream();) {
ftpClient.retrieveFile(file.getName(), is);
byte[] rawData = is.toByteArray();
if (rawData != null && rawData.length > 0) {
logger.finest("......file '" + file.getName() + "' successful read - bytes size = "
+ rawData.length);
// create new workitem
invoice = createWorkitem(event.getSource(), file.getName(), rawData, spacePosMappings);
if (invoice != null) {
// persist workitem only if not exists and with a matching mandant.id
if (!invoice.getItemValueString("mandant.id").equals(mandantID)) {
throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(),
"FTP_ERROR", "Invoice does not match mandantID " + mandantID);
}
count++;
workflowService.processWorkItemByNewTransaction(invoice);
// .processWorkItem(invoice);
}
}
// finally delete teh file from the transfer folder
ftpClient.deleteFile(ftpPath + file.getName());
// String sourceFilePath = ftpPath + file.getName();
// String destinationFilePath = ftpPath + "processed/" + file.getName();
// boolean success = ftpClient.rename(sourceFilePath, destinationFilePath);
// if (!success) {
// logger.warning("Failed to move file " + file.getName() + " to " + ftpPath +
// "processed/");
// }
} catch (AccessDeniedException | ProcessingErrorException | PluginException | ModelException
| XPathExpressionException | TransformerException e) {
throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(), "FTP_ERROR",
e.getMessage());
}
}
documentImportService.logMessage("..." + count + " new files imported.", event);
}
} catch (IOException e) {
logger.severe("FTP I/O Error: " + e.getMessage());
int r = ftpClient.getReplyCode();
logger.severe("FTP ReplyCode=" + r);
throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(), "FTP_ERROR", e.getMessage());
} finally {
// do logout....
try {
ftpClient.logout();
ftpClient.disconnect();
} catch (IOException e) {
documentImportService.logMessage("...FTP file transfer failed: " + e.getMessage(), event);
throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(), "FTP_ERROR",
e.getMessage());
}
}
}
/**
* Creates and processes a new workitem with a given xml information from the
* fileData
*
* Hier wird das Corgosoft XML geparst und die Felder in Imixs gefüllt. Z.b.
* werden hier auch die ChildItems angelegt.
*
* @return
* @throws ModelException
* @throws PluginException
* @throws ProcessingErrorException
* @throws AccessDeniedException
* @throws TransformerException
* @throws XPathExpressionException
*/
public ItemCollection createWorkitem(ItemCollection source, String fileName, byte[] rawData,
Map<String, List<String>> spacePosMappings) throws AccessDeniedException, ProcessingErrorException,
PluginException, ModelException, XPathExpressionException, TransformerException {
ItemCollection workitem = new ItemCollection();
workitem.model(source.getItemValueString(DocumentImportService.SOURCE_ITEM_MODELVERSION));
workitem.task(source.getItemValueInteger(DocumentImportService.SOURCE_ITEM_TASK));
workitem.event(source.getItemValueInteger(DocumentImportService.SOURCE_ITEM_EVENT));
workitem.setWorkflowGroup(source.getItemValueString("workflowgroup"));
workitem.setItemValue("cargosoft.import.filename", fileName);
// Now we parse the rawData....
InputStream inputStream = new ByteArrayInputStream(rawData);
InputSource inputSource = new InputSource(inputStream);
DocumentBuilder documentBuilder;
try {
documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = documentBuilder.parse(inputSource);
// Now we parse the following item values
// invoice.number
// invoice.currency
// invoice.text
// invoice.date
// dbtr.number
// invoice.duedate
// invoice.reminder
// payment.term
// invoice.rate
// invoice.base.amount
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/Client/Codes/Code", workitem, "mandant.id",
String.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceNumber", workitem, "invoice.number",
String.class);
// verify if invoice is already imported.
if (alreadyImported(workitem.getItemValueString("invoice.number"))) {
logger.warning("Invoice " + workitem.getItemValueString("invoice.number") + " already imported");
return null;
}
// Invoice Type , Correction and CorrectionInvoiceNumber
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceType/Codes/Code", workitem, "invoice.type",
String.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/Correction", workitem, "invoice.correction",
String.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/CorrectionInvoiceNumber", workitem,
"invoice.CorrectionInvoiceNumber",
String.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceType/Description", workitem,
"invoice.type.description", String.class);
// Rechnungssumme errechnen.....
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceAmount/NetAmount/Amount/Value", workitem,
"invoice.total.net", Double.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceAmount/VATInformation/VATAmount/Amount/Value",
workitem, "invoice.total.tax", Double.class);
if (!workitem.hasItem("invoice.total.tax")) {
workitem.setItemValue("invoice.total.tax", 0.0);
}
// Total = net + tax
double _total = workitem.getItemValueDouble("invoice.total.net");
_total = _total + workitem.getItemValueDouble("invoice.total.tax");
_total = Math.round(_total * 100) / 100.0;
workitem.setItemValue("invoice.total", _total);
workitem.setItemValue("invoice.saldo", _total);
// Bei Gutschrift und Stornorechnung Wert negieren...
if (isGutschrift(workitem)) {
workitem.setItemValue("invoice.saldo", -workitem.getItemValueDouble("invoice.total"));
workitem.setItemValue("invoice.total", -workitem.getItemValueDouble("invoice.total"));
}
// Steuer
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceAmount/VATInformation/VAT/VATRate", workitem,
"invoice.vatrate", Double.class);
// Booking Period
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/BookingPeriod", workitem, "invoice.bookingperiod",
String.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceCurrency/Codes/Code", workitem,
"invoice.currency", String.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceDate", workitem, "invoice.date", Date.class);
readXMLValue(doc,
"/Invoices/Invoice/InvoiceHeader/PaymentConditions/PaymentCondition/Codes/Code[@Type='cs']",
workitem, "payment.term", String.class);
// kostenstelle
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/CostUnit/Description", workitem, "invoice.CostUnit",
String.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/PaymentConditions/DueDate", workitem, "invoice.duedate",
Date.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/PaymentConditions/DueDate", workitem, "invoice.reminder",
Date.class);
// Performance Date
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/PerformanceDate", workitem, "invoice.performancedate",
Date.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceAddress/Codes/Code", workitem, "dbtr.number",
String.class);
String dbtrNumber = workitem.getItemValueString("dbtr.number");
if (dbtrNumber.startsWith("K") || dbtrNumber.startsWith("D")) {
workitem.setItemValue("dbtr.number", dbtrNumber.substring(1));
}
// lookup debitor and set name
try {
ItemCollection dbtr = findDebitor(dbtrNumber);
if (dbtr != null) {
workitem.setItemValue("dbtr.name", dbtr.getItemValueString("_vendor_name"));
// Anhand der Creditoren Stammdaten errrechnen wir die Sprache für diese
// Rechnung
String country = dbtr.getItemValueString("_VENDOR_COUNTRY");
workitem.setItemValue("invoice.country", country);
if (country.equalsIgnoreCase("de") || country.equalsIgnoreCase("ch")
|| country.equalsIgnoreCase("at")) {
workitem.setItemValue("invoice.language", "DE");
} else {
workitem.setItemValue("invoice.language", "EN");
}
}
} catch (PluginException e) {
// e.printStackTrace();
}
// Row Positions lesen
readXMLRows(doc, workitem);
// Suche die ATC Nummern....
resolveATCNumbers(doc, workitem);
// Jetzt noch das Space mapping
mahnlaufService.mapInvoiceTextToSpace(workitem, spacePosMappings);
// Rückstellungen ignorieren
if (isRueckStellung(workitem)) {
// Ignore invoice!
return null;
}
// Finally we attache the pdf file and the XML content to the workitem.
attacheFiles(doc, workitem);
} catch (ParserConfigurationException | SAXException | IOException e) {
throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(), "XML_ERROR", e.getMessage());
}
return workitem;
}
/**
* Prüft ob die Rechnung eine Rückstellung ist also mit R1 bis R8 beginnt
*
* ^R[1-8] .
*
* @param workitem
* @return
*/
private boolean isRueckStellung(ItemCollection workitem) {
String text = workitem.getItemValueString("invoice.text");
return text.matches(REGEX_IMPORTTEXTPATTERN);
}
/**
* Reads a tag value from the xml tree and set the value into the given
* workitem.
*
* /Invoices/Invoice/InvoiceHeader/Client/Code
*
* Beispiel Datum: <InvoiceDate>2024-05-13T00:00:00+02:00</InvoiceDate>
*
* @param doc - xml doc
* @param expression - xpath expression
* @param workitem
* @param itemName
* @param itemType
*/
public static <T> void readXMLValue(Document doc, String expression, ItemCollection workitem, String itemName,
Class<T> itemType) {
// create XPath...
XPathFactory xpathFactory = XPathFactory.newInstance();
XPath xpath = xpathFactory.newXPath();
XPathExpression xPathExpression;
try {
xPathExpression = xpath.compile(expression);
// extract node value
Node valueNode = (Node) xPathExpression.evaluate(doc, XPathConstants.NODE);
if (valueNode != null) {
String value = valueNode.getTextContent();
if (itemType == Date.class) {
// 2024-05-13T00:00:00+02:00
SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
formatter.setTimeZone(TimeZone.getTimeZone("CET"));
try {
workitem.setItemValue(itemName, formatter.parse(value));
} catch (ParseException e) {
logger.warning("Invalid Date Format");
}
return;
}
if (itemType == Double.class && value != null && !value.isEmpty()) {
workitem.setItemValue(itemName, Double.parseDouble(value));
return;
}
if (itemType == Integer.class && value != null && !value.isEmpty()) {
workitem.setItemValue(itemName, Integer.parseInt(value));
return;
}
// Default String format
workitem.setItemValue(itemName, value);
}
} catch (XPathExpressionException e) {
logger.warning("Unable to read data field '" + expression + "' : " + e.getMessage());
}
}
/**
* Hilfsmethode die die ATC Nummer sucht und in das workitem feld
* invoice.atc.number einträgt. Wir gehen davon aus, das wir über die ChildItems
* diese aus dem billingtexts schon ermittel haben. Es können mehrere ATC
* nummern existieren. Diese werden in einer Liste gespeichert
*
* @param doc
* @param workitem
*/
private void resolveATCNumbers(Document doc, ItemCollection workitem) {
String atcNumber = null;
List<ItemCollection> childs = InvoiceUtil.explodeChildList(workitem);
for (ItemCollection child : childs) {
if (child.hasItem("atc.number")) {
atcNumber = child.getItemValueString("atc.number");
workitem.appendItemValue("invoice.atc.number", atcNumber);
// break;
}
}
// if (atcNumber != null && !atcNumber.isEmpty()) {
// workitem.setItemValue("invoice.atc.number", atcNumber);
// }
}
/**
* Reads the positions rows
*
* InvoiceRows/InvoiceRow
*
* Each row has the following items: _childitems { datev.shzeichen=[H],
* datev.kurs=[0.0], datev.basisumsatz=[0.0], datev.text=[R LA-PAP-2405-051],
* datev.konto=[3851], datev.umsatz=[3307.81], datev.wkz=[EUR]}}
*
* @param doc - xml doc
* @param workitem
*/
private void readXMLRows(Document doc, ItemCollection workitem) {
// create XPath...
List<ItemCollection> childItems = new ArrayList<>();
XPathFactory xpathFactory = XPathFactory.newInstance();
XPath xPath = xpathFactory.newXPath();
try {
// Compile the XPath expressions
XPathExpression fileNumberExpr = xPath.compile("FileNumber/text()");
XPathExpression netAmountCurrencyExpr = xPath
.compile("InvoiceAmount/NetAmount/Amount/Currency/Codes/Code[@Type='cs']/text()");
XPathExpression netAmountValueExpr = xPath.compile("InvoiceAmount/NetAmount/Amount/Value/text()");
XPathExpression netAmountExchangeRateExpr = xPath
.compile("InvoiceAmount/NetAmount/Amount/ExchangeRate/text()");
XPathExpression vatInformationExpr = xPath.compile("InvoiceAmount/VATInformation/VAT/VATRate/text()");
XPathExpression vatCodeTypeExpr = xPath
.compile("InvoiceAmount/VATInformation/VAT/Codes/Code[@Type='cs']/text()");
XPathExpression netActivityTypeExpr = xPath.compile("ActivityType/Codes/Code[@Type='cs']/text()");
NodeList rowList = doc.getElementsByTagName("InvoiceRow");
double _kurs = 0.0;
double _baseAmount = 0.0;
for (int i = 0; i < rowList.getLength(); i++) {
ItemCollection childItemCol = new ItemCollection();
Node rowNode = rowList.item(i);
if (rowNode.getNodeType() == Node.ELEMENT_NODE) {
// XPath-Ausdruck für den BillingCode
XPathExpression billingCodeExpr = xPath.compile("BillingCode/Codes/Code");
String billingCode = (String) billingCodeExpr.evaluate(rowNode, XPathConstants.STRING);
// skip row wenn billing code = "WÄHRUNG"
// # Ergänzung 16.4.2026
// Da dieses flag von Cargosoft machmachmal auch 'KURS' lautet müssen wir das
// auch berücksichtigen.
if ("WÄHRUNG".equals(billingCode) || "KURS".equals(billingCode)) {
continue; // skip this row!
}
// Get the FileNumber value
String fileNumber = (String) fileNumberExpr.evaluate(rowNode, XPathConstants.STRING);
childItemCol.setItemValue("datev.text", fileNumber);
// get VAT Rate
try {
String vatRateText = (String) vatInformationExpr.evaluate(rowNode, XPathConstants.STRING);
childItemCol.setItemValue("datev.vatrate", Double.valueOf(vatRateText));
} catch (Exception e) {
logger.warning("Unable to pase cs vat rate: " + e.getMessage());
}
// get VAT Code
try {
String vatCodeText = (String) vatCodeTypeExpr.evaluate(rowNode, XPathConstants.STRING);
childItemCol.setItemValue("cargosoft.vat.code", vatCodeText);
} catch (Exception e) {
logger.warning("Unable to pase cs vat code: " + e.getMessage());
}
// speichere die Position in invoice.positions zur suche nach dem
// Positionskennzeichen
workitem.appendItemValue("invoice.positions", fileNumber);
String currency = (String) netAmountCurrencyExpr.evaluate(rowNode, XPathConstants.STRING);
childItemCol.setItemValue("datev.wkz", currency);
// Umsatz
String umsatz = (String) netAmountValueExpr.evaluate(rowNode, XPathConstants.STRING);
double dUmsatz = Double.parseDouble(umsatz);
childItemCol.setItemValue("datev.umsatz", dUmsatz);
// Kurs
String _kursByRow = (String) netAmountExchangeRateExpr.evaluate(rowNode, XPathConstants.STRING);
if (_kursByRow != null && !_kursByRow.isEmpty()) {
Double dKurs = Double.parseDouble(_kursByRow);
_kurs = dKurs;
childItemCol.setItemValue("datev.kurs", dKurs);
// Basisumsatz
if (dKurs != 0) {
double basisUmsatz = dUmsatz / dKurs;
double gerundeterBasisUmsatz = Math.round(basisUmsatz * 100) / 100.0;
if (isGutschrift(workitem)) {
_baseAmount = _baseAmount - gerundeterBasisUmsatz;
} else {
_baseAmount = _baseAmount + gerundeterBasisUmsatz;
}
childItemCol.setItemValue("datev.basisumsatz", gerundeterBasisUmsatz);
}
}
String activityType = (String) netActivityTypeExpr.evaluate(rowNode, XPathConstants.STRING);
childItemCol.setItemValue("category", activityType);
/**
* S/H Kennzeichen auflösen
*
* Bei G = Gutschrift oder SR = Stornorechnung müssen wir das vorzeichen ändern
* Invoice Type S/H
*/
if (isGutschrift(workitem)) {
// Gutschrift
childItemCol.setItemValue("datev.shzeichen", "S");
} else {
// Rechnung
childItemCol.setItemValue("datev.shzeichen", "H");
}
/**
* Liste der BillingText-Elemente lesen....
*
* und ggf. die ATC Nummer finden....
**/
childItemCol.setItemValue("BillingCode", billingCode);
// XPath-Ausdruck für die BillingTexts
XPathExpression billingTextsExpr = xPath.compile("BillingTexts/BillingText");
NodeList billingTextNodes = (NodeList) billingTextsExpr.evaluate(rowNode, XPathConstants.NODESET);
for (int bi = 0; bi < billingTextNodes.getLength(); bi++) {
String val = billingTextNodes.item(bi).getTextContent();
childItemCol.appendItemValue("BillingText", val);
// handelt es sich um eine ATC Nummer?
// Änderung 22.10.2024 - jetzt auch auf 24DE prüfen
if (val.startsWith("ATC") || val.startsWith("24DE") || val.startsWith("25DE")
|| val.startsWith("26DE")) {
childItemCol.setItemValue("atc.number", val);
}
}
childItems.add(childItemCol);
}
}
// Update invoice.rate und invoice.base.amount
workitem.setItemValue("invoice.rate", _kurs);
_baseAmount = Math.round(_baseAmount * 100) / 100.0;
workitem.setItemValue("invoice.base.amount", _baseAmount);
} catch (XPathExpressionException e) {
logger.warning("Unable to read row : " + e.getMessage());
}
// Set child items
InvoiceUtil.implodeChildList(workitem, childItems);
// invoice.text ist erste position
workitem.setItemValue("invoice.text", workitem.getItemValueString("invoice.positions"));
}
/**
* G = Gutschrift SR = Stornorechnung GK = Sammelgutschrift SS = Sammelrechnung
* Storno
*
* müssen wir das vorzeichen ändern dies wird über den Invoice.Type geprüft
*
* @return
*/
private boolean isGutschrift(ItemCollection workitem) {
String type = workitem.getItemValueString("invoice.type");
return ("G".equals(type) || "SR".equals(type) || "GK".equals(type) || "SS".equals(type));
}
/**
* This method attache the pdf file and the XML file to the workitem. The method
* removes the file content first form the xml tree and attache the XML without
* the pdf data.
*
* @throws XPathExpressionException
* @throws TransformerException
*/
private void attacheFiles(Document doc, ItemCollection workitem)
throws XPathExpressionException, TransformerException {
// create XPath...
XPathFactory xpathFactory = XPathFactory.newInstance();
XPath xpath = xpathFactory.newXPath();
// read filename and content....
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/Attachments/Attachment/Filename", workitem,
"import.filename", String.class);
readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/Attachments/Attachment/Content", workitem, "import.filedata",
String.class);
// attache the PDF file content
String fileDataString = workitem.getItemValueString("import.filedata");
byte[] decodedPDFData = java.util.Base64.getDecoder().decode(fileDataString);
FileData fileData = new FileData(workitem.getItemValueString("import.filename"), decodedPDFData,
"application/pdf", null);
workitem.addFileData(fileData);
// remove the temp items..
workitem.removeItem("import.filename");
workitem.removeItem("import.filedata");
// next remove the Attachments/AttachmentContent from the dom tree
XPathExpression attachmentsExpr = xpath
.compile("/Invoices/Invoice/InvoiceHeader/Attachments/Attachment/Content");
NodeList contentNodes = (NodeList) attachmentsExpr.evaluate(doc, XPathConstants.NODESET);
// Remove the matching nodes
for (int i = 0; i < contentNodes.getLength(); i++) {
Node contentNode = contentNodes.item(i);
Node parentNode = contentNode.getParentNode();
parentNode.removeChild(contentNode);
}
// Now attache the XML tree without the file...
StringWriter stringWriter = new StringWriter();
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(stringWriter);
transformer.transform(source, result);
// byte[] decodedXMLData =
// java.util.Base64.getDecoder().decode(stringWriter.toString().);
FileData fileDataXML = new FileData(workitem.getItemValueString("cargosoft.import.filename"),
stringWriter.toString().getBytes(), "application/xml", null);
workitem.addFileData(fileDataXML);
}
/**
* Prüft ob die Belegnummer schon existiert (importiert wurde)
*
* @param belegNummer
* @return
*/
protected boolean alreadyImported(String belegNummer) {
String sQuery = "((type:workitem OR type:workitemarchive) AND $modelversion:rechnungsausgang* AND invoice.number:\""
+ belegNummer + "\")";
try {
// find the textblock...
List<ItemCollection> result = documentService.find(sQuery, 1, 0);
if (result.size() > 0) {
return true;
}
} catch (QueryException e) {
logger.warning("failed to search invoices by query: " + e.getMessage());
}
return false;
}
/**
* Diese Method sucht einen Debitor.
* <p>
* Diese Methode erwartet das führende D
*
* @throws PluginException
*/
public ItemCollection findDebitor(String dbtrNumber) throws PluginException {
if ((!dbtrNumber.startsWith("D"))) {
dbtrNumber = "D" + dbtrNumber;
}
try {
String query = "(type:" + TYPE_CARGOSOFTKREDITOR + ") AND (name:" + dbtrNumber + ")";
List<ItemCollection> result = documentService.find(query, 1, 0, "$modified", true);
if (result.size() > 0) {
return result.get(0);
}
} catch (QueryException e) {
throw new PluginException(PluginException.class.getName(), "QUERY ERROR", e.getMessage(), e);
}
return null;
}
}