package com.alexanderlogistics.datev; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; import org.imixs.marty.team.TeamService; import org.imixs.workflow.FileData; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.SignalAdapter; import org.imixs.workflow.datev.imports.DatevImportAdapter; import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.engine.WorkflowService; import org.imixs.workflow.exceptions.AccessDeniedException; import org.imixs.workflow.exceptions.AdapterException; 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 com.alexanderlogistics.KreditorDebitorService; /** * Der DatevCargosoftImportAdapter liest eine von Cargosoft bereitgestellte * DATEV Datei ein und erzeugt Ausgangsrechnungs Workitems. Die Cargosoft DATEV * Datei enthält auch Eingangsrechnugen, welche von diesem Adapter ignoriert * werden. *

* In der Spalte Belegart-4 steht das Fäligkeitsdatum. *

* Beim Import wird auch der Kreditor geprüft und aus diesem das * Länderkennzeichen übernommen. *

* Zusätzlich wird anhand der 1. Positionsnummer eine Zuordnung zu einem Bereich * gemacht. Dazu werden aus den Bereichen die Konfigurations Items 'pos.mapping' * ausgelesen, welche Regular Expressions enthalten können. Gibt es einen match, * wird der Space der Rechnung zugeordnet. * * @version 1.0 * @author rsoika */ public class DatevCargosoftImportAdapter implements SignalAdapter { private static Logger logger = Logger.getLogger(DatevCargosoftImportAdapter.class.getName()); public static final String ENCODING = "ISO-8859-1"; public static final String CHILD_ITEM_PROPERTY = "_ChildItems"; public static final String REGEX_IMPORTTEXTPATTERN = "^R[1-8] .*$"; @Inject WorkflowService workflowService; @Inject DocumentService documentService; @Inject KreditorDebitorService kreditorDebitorService; @Inject TeamService teamService; /** * This method finds or create the Datev Export and adds a reference * ($workitemref) to the current invoice. * * @throws PluginException */ @Override public ItemCollection execute(ItemCollection workitem, ItemCollection event) throws AdapterException, PluginException { logger.info("...parse datev file import..."); try { // Datei einlesen List log = readData(workitem); workitem.setItemValue("import.log", log); } catch (ModelException e) { throw new AdapterException(PluginException.class.getSimpleName(), DatevImportAdapter.DATEV_IMPORT_ERROR, "Failed to import datev data!", e); } return workitem; } /** * Liest die datenzeilen ab Zeile 3 aus und bildet worktiems gruppiert nach * belegnummer * *

* Belegfeld-1 ist hier ein eindeutiger Schlüssel für die Erzeugung von * Splitbuchungen * * @param document * @throws PluginException * @throws ModelException * @throws ProcessingErrorException * @throws AccessDeniedException */ private List readData(ItemCollection workitem) throws PluginException, AccessDeniedException, ProcessingErrorException, ModelException { List logBuffer = new ArrayList(); int line = 0; int positions = 0; int invoices = 0; int dupplicates = 0; int rueckstellungen = 0; int eingangsrechnungen = 0; String dataLine; // Start Datum Wirtschaftsjahr berechnen (JJJJMMTT) String wjBeginn = workitem.getItemValueString("datev.WJ-Beginn"); String wjBeginnMonat = wjBeginn.substring(4, 6); SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); // load spaces Pos Expressions Map> spaces = loadSpacesConfig(); ItemCollection invoiceWorkitem = null; List splitBuchungen = new ArrayList(); FileData fileData = workitem.getFileData().get(0); ByteArrayInputStream imputStream = new ByteArrayInputStream(fileData.getContent()); log(logBuffer, "Importdatei: " + fileData.getName()); try { BufferedReader in = new BufferedReader(new InputStreamReader(imputStream, ENCODING)); // skip header in.readLine(); in.readLine(); // read content.... line = 0; int blockSize = 0; while ((dataLine = in.readLine()) != null) { line++; if (blockSize >= 100) { blockSize = 0; logger.info(line + " entries read...."); } // alle werte parsen String[] header1List = dataLine.split(";(?=([^\"]*\"[^\"]*\")*[^\"]*$)", 99); if (header1List.length < 6) { throw new PluginException(DatevCargosoftImportAdapter.class.getName(), DatevImportAdapter.DATEV_IMPORT_ERROR, "Invalid data in line " + line); } String belegNummer = DatevImportAdapter.csvVal(header1List[10]); String gegenKonto = DatevImportAdapter.csvVal(header1List[7]); String text = DatevImportAdapter.csvVal(header1List[13]); String buchungsTag = DatevImportAdapter.csvVal(header1List[9]); // cargosoft laesst gerne die führende 0 weg.... if (buchungsTag.length() == 3) { buchungsTag = "0" + buchungsTag; } // berechne Buchungsdatum DDMM String tag = buchungsTag.substring(0, 2); String monat = buchungsTag.substring(2, 4); String jahr = wjBeginn.substring(0, 4); // compute next year if start of WJ is not January if (Integer.parseInt(wjBeginnMonat) > Integer.parseInt(monat)) { jahr = Integer.parseInt(jahr + 1) + ""; } Date dateBuchung = dateFormatter.parse(jahr + monat + tag); // Fälligkeit Date dueDate = new Date(); try { dateFormatter.parse(DatevImportAdapter.csvVal(header1List[23])); } catch (Exception e) { dueDate = new Date(); } String paymentTerm = DatevImportAdapter.csvVal(header1List[31]); String waehrung = DatevImportAdapter.csvVal(header1List[2]); // kurse double kurs = parseBetrag(DatevImportAdapter.csvVal(header1List[3])); double basisumsatz = parseBetrag(DatevImportAdapter.csvVal(header1List[4])); // Skip Eingangsrechnungen if (!gegenKonto.startsWith("1")) { eingangsrechnungen++; // skip continue; } // Skip Rückstellung // text darf nicht mit R1 bis R8 beginnen // ^R[1-8] . if (text.matches(REGEX_IMPORTTEXTPATTERN)) { rueckstellungen++; // skip continue; } positions++; logger.info("...read dbtr: " + gegenKonto + " beleg-nr: " + belegNummer); // Erzeuge einen Buchungssatz ItemCollection dataItemCol = new ItemCollection(); dataItemCol.setItemValue("datev.text", text); dataItemCol.setItemValue("datev.umsatz", parseBetrag(DatevImportAdapter.csvVal(header1List[0]))); dataItemCol.setItemValue("datev.shzeichen", DatevImportAdapter.csvVal(header1List[1])); dataItemCol.setItemValue("datev.wkz", waehrung); dataItemCol.setItemValue("datev.kurs", parseBetrag(DatevImportAdapter.csvVal(header1List[3]))); dataItemCol.setItemValue("datev.basisumsatz", parseBetrag(DatevImportAdapter.csvVal(header1List[4]))); dataItemCol.setItemValue("datev.konto", DatevImportAdapter.csvVal(header1List[6])); // Haben wir schon eine Invoice Workitem angelegt? if (invoiceWorkitem == null) { logger.info("...create new workitem...."); // erzeuge ein neues try { invoiceWorkitem = createEmptyInvoice(belegNummer, text, gegenKonto, dateBuchung, dueDate, paymentTerm, waehrung, kurs, basisumsatz); invoices++; } catch (AdapterException e) { dupplicates++; continue; } } // ist es noch die selbe Belegnummer? if (belegNummer.equals(invoiceWorkitem.getItemValueString("invoice.number"))) { // adde die Splitbuchung splitBuchungen.add(dataItemCol); logger.info("...add splitbuchung.2..."); } else { logger.info("...close workitem...."); // nein - also invoice schließen closeInvoiceWorkitem(invoiceWorkitem, splitBuchungen,spaces); invoiceWorkitem = null; // neue Splitbuchungstabelle anlegen splitBuchungen = new ArrayList(); // invoice zurücksetzten // erzeuge ein neues try { invoiceWorkitem = createEmptyInvoice(belegNummer, text, gegenKonto, dateBuchung, dueDate, paymentTerm, waehrung, kurs, basisumsatz); invoices++; } catch (AdapterException e) { dupplicates++; continue; } splitBuchungen.add(dataItemCol); } } // wile end closeInvoiceWorkitem(invoiceWorkitem, splitBuchungen,spaces); } catch (IOException | ParseException e) { throw new PluginException(DatevCargosoftImportAdapter.class.getName(), DatevImportAdapter.DATEV_IMPORT_ERROR, "Unable to read file", e); } log(logBuffer, line + " Zeilen eingelesen "); log(logBuffer, positions + " Rechnungspositionen eingelesen "); log(logBuffer, invoices + " Rechnungen importiert "); if (rueckstellungen > 0) { log(logBuffer, rueckstellungen + " Rückstellungen wurden übersprungen."); } if (eingangsrechnungen > 0) { log(logBuffer, eingangsrechnungen + " Eingangsrechnungen wurden übersprungen."); } if (dupplicates > 0) { log(logBuffer, dupplicates + " Rechnungen wurden übersprungen da diese bereits importiert wurden!"); } // return log return logBuffer; } /** * Helper method that loads all expressions from the spaces * @return List of list of expressions */ private Map> loadSpacesConfig() { Map> result =new HashMap<>(); // als erstes lesen wir die pos.mappings aus den Spaces ein, auf die dann eine // Rechnung emapped werden kann. List spaces_subs = teamService.getSpaces(); for (ItemCollection stub: spaces_subs) { // load full workitem ItemCollection space = documentService.load(stub.getUniqueID()); if (space!=null) { result.put(space.getUniqueID(),space.getItemValueList("pos.mapping", String.class)); } } return result; } /** * This method returns a new empty invoice ItemCollection. *

* The method tests if an invoice with this number already exits. If so, the * method throws a AdapterException * * @param belegNummer * @param text * @param gegenKonto * @param dateBuchung * @param dueDate * @param paymentTerm * @param currency * @param kurs * @param basisUmsatz * @return * @throws AdapterException */ private ItemCollection createEmptyInvoice(String belegNummer, String text, String gegenKonto, Date dateBuchung, Date dueDate, String paymentTerm, String currency, double kurs, double basisUmsatz) throws AdapterException { // test if invoice was already imported before String sQuery = "((type:workitem OR type:workitemarchive) AND $modelversion:rechnungsausgang* AND invoice.number:\"" + belegNummer + "\")"; try { // find the textblock... List result = documentService.find(sQuery, 1, 0); if (result.size() > 0) { throw new AdapterException(DatevCargosoftImportAdapter.class.getName(), "DUPPLICATE", "Invoice already imported"); } } catch (QueryException e) { logger.warning("failed to search invoices by query: " + e.getMessage()); } ItemCollection invoiceWorkitem = new ItemCollection(); invoiceWorkitem.setItemValue("invoice.number", belegNummer); invoiceWorkitem.setItemValue("invoice.currency", currency); invoiceWorkitem.setItemValue("invoice.text", text); invoiceWorkitem.setItemValue("invoice.date", dateBuchung); invoiceWorkitem.setItemValue("dbtr.number", gegenKonto); invoiceWorkitem.setItemValue("invoice.duedate", dueDate); invoiceWorkitem.setItemValue("invoice.reminder", dueDate); invoiceWorkitem.setItemValue("payment.term", paymentTerm); invoiceWorkitem.setItemValue("invoice.rate", kurs); invoiceWorkitem.setItemValue("invoice.base.amount", basisUmsatz); return invoiceWorkitem; } private void log(List log, String message) { log.add(new Date() + ": " + message + "\n"); } /** * . entfernen , mit punkt ersetzten * * @param value * @return */ public Double parseBetrag(String value) { value = value.replace(".", ""); value = value.replace(",", "."); return new Double(value); } /** * Diese Hilfsmethod schließt das Invoice Workitem ab * * @throws ModelException * @throws PluginException * @throws ProcessingErrorException * @throws AccessDeniedException */ private void closeInvoiceWorkitem(ItemCollection invoiceWorkitem, List splitBuchungen, Map> expressionsMap) throws AccessDeniedException, ProcessingErrorException, PluginException, ModelException { if (invoiceWorkitem == null) { return; // no op } // berechne Total: double total = 0; for (ItemCollection zeile : splitBuchungen) { if ("H".equals(zeile.getItemValueString("datev.shzeichen"))) { total = total + zeile.getItemValueDouble("datev.umsatz"); } else { total = total - zeile.getItemValueDouble("datev.umsatz"); } } invoiceWorkitem.setItemValue("invoice.total", total); invoiceWorkitem.setItemValue("invoice.saldo", total); // implode die Splitbuchungen implodeChildList(invoiceWorkitem, splitBuchungen); // set dbtr.name ItemCollection dbtrItemCol = kreditorDebitorService .findDebitor(invoiceWorkitem.getItemValueString("dbtr.number")); if (dbtrItemCol != null) { invoiceWorkitem.setItemValue("dbtr.name", dbtrItemCol.getItemValueString("_VENDOR_Name")); // Anhand der Creditoren Stammdaten errrechnen wir die Sprache für diese // Rechnung String country = dbtrItemCol.getItemValueString("_VENDOR_COUNTRY"); invoiceWorkitem.setItemValue("dbtr.country", country); if (country.equalsIgnoreCase("de") || country.equalsIgnoreCase("ch") || country.equalsIgnoreCase("at")) { invoiceWorkitem.setItemValue("invoice.language", "DE"); } else { invoiceWorkitem.setItemValue("invoice.language", "EN"); } } // Jezt noch das Space mapping // dazu kucken wir in alle spaces den config wert 'pos.mapping' String text = invoiceWorkitem.getItemValueString("invoice.text"); if (!text.isEmpty()) { for (Map.Entry > entry : expressionsMap.entrySet()) { List expressionsList=entry.getValue(); for (String expr : expressionsList) { Pattern p = Pattern.compile(expr); Matcher m = p.matcher(text); if (m.find()) { // we have a match invoiceWorkitem.setItemValue("space.ref", entry.getKey()); break; } } } } // Processe die Invoice invoiceWorkitem.model("rechnungsausgang-de-1.0").task(5001).event(980); workflowService.processWorkItem(invoiceWorkitem); } /** * Convert the List of ItemCollections back into a List of Map elements * * @param workitem */ @SuppressWarnings({ "rawtypes" }) private void implodeChildList(ItemCollection workitem, List childItems) { List mapOrderItems = new ArrayList(); // convert the child ItemCollection elements into a List of Map if (childItems != null) { logger.fine("Convert child items into Map..."); // iterate over all order items.. for (ItemCollection orderItem : childItems) { mapOrderItems.add(orderItem.getAllItems()); } workitem.replaceItemValue(CHILD_ITEM_PROPERTY, mapOrderItems); } } }