package com.alexanderlogistics; import java.time.LocalDate; import java.util.List; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.engine.plugins.AbstractPlugin; import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.QueryException; import org.imixs.workflow.faces.util.ResourceBundleHandler; /** * Das InvoicePlugin prüft die Eingaben auf Gültigkeit, so dass diese problemlos * nach Cargosoft exportiert werden können. *

* Konkret geht es darum, das im Status 5200 in den ChildItems keine leeren * Zeilen vorkommen dürfen. *

* Im Status 5000 muss immer der payment.type ausgewählt werden. Dies wird vom * Plugin explizit geprüft *

* Das Plugin prüft auch auf doppelte Rechnungsnummern. Dabei wird Systemweit * geprüft ob die Rechnungsnummer schon einmal vorkam. Falls ja, wird eine * Warnung ausgegeben. Der Benutzer kann diese dann skippen. *

* Zusäztlich berechnet das Plugin das feld invoice.positions mit einer Value * liste der Positionsnummern. Dieses Feld wird indiziert, so dass gezielt nach * Positionsnummern gesucht werden kann. *

* 2.2.22 - Das Plugin erzeugt auch das Feld _img welches die Images von * Sofortüberweisung, Mahnung und Ablehnung anzeigt. * * * @author rsoika * @version 1.0 * */ public class InvoicePlugin extends AbstractPlugin { public static final int TASK_ERFASSUNG = 5001; public static final int TASK_SACHPRUEFUNG = 5200; public static final int TASK_SACHPRUEFUNG_DELEGATION = 5210; public static final int EVENT_FREIGEBEN = 20; public static final String ERROR_MISSING_DATA = "MISSING_DATA"; public static final String ERROR_DUPPLICATE_INVOICE_NUMBER = "DUPPLICATE_INVOICE_NUMBER"; public static final String ERROR_NEW_IBANBIC = "NEW_IBANBIC"; // XX-YYY-####-### public static final String REGEX_POSNUMER = "([A-Z]{2}-[A-Z]{3}-[0-9]{4}-[0-9]{3})"; private static Logger logger = Logger.getLogger(InvoicePlugin.class.getName()); @Inject KreditorDebitorService kreditorService; @Inject ResourceBundleHandler resourceBundleHandler; /** * Test childworkitems for empty lines * * @throws PluginException - if data is missing * **/ @Override public ItemCollection run(ItemCollection workitem, ItemCollection event) throws PluginException { updateImg(workitem); // Update Invoice.positions InvoiceUtil.updateInvoicePositions(workitem); // skip if validaten tag is required=false ItemCollection evalItemCollection = this.getWorkflowService().evalWorkflowResult(event, "validation", workitem); if (evalItemCollection != null) { // evaluate the validation rules... if ("false".equalsIgnoreCase(evalItemCollection.getItemValueString("required"))) { return workitem; } } boolean isPublicEvent = !("0".equals(event.getItemValueString("keypublicresult"))); // Payment.type muss immer eingetragne werden! if (isPublicEvent && workitem.getTaskID() >= TASK_ERFASSUNG && workitem.getEventID() < 900) { if (workitem.getItemValueString("payment.type").trim().isEmpty()) { // throw a plugin exception! String message = resourceBundleHandler.findMessage("ERROR_MISSING_PAYMENT"); throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA, message); } // doppelte Rechnungsnummer prüfen if (workitem.getTaskID() == TASK_ERFASSUNG) { validateInvoiceNumber(workitem); checkIBANNumber(workitem); } if (workitem.getTaskID() == TASK_ERFASSUNG || ((workitem.getTaskID() == TASK_SACHPRUEFUNG || workitem.getTaskID() == TASK_SACHPRUEFUNG_DELEGATION) && workitem.getEventID() == EVENT_FREIGEBEN)) { // Buchungsperiode auf plausi prüfen if (isCargoRechnung(workitem) && "workitem".equals(workitem.getType())) { validateBuchungsperiode(workitem.getItemValueString(InvoiceUtil.ITEM_INVOICE_PERIOD)); } // Cargosoft Kreditorennnummer prüfen if (isCargoRechnung(workitem)) { validateCargosoftCdtrNumber(workitem); } } } // die prüfung der positionsnummern und Category erfolgt nur im Status // Sachprüfung (5200) und nur beim Freigeben (20)! if (isCargoRechnung(workitem) && ((workitem.getTaskID() == TASK_SACHPRUEFUNG || workitem.getTaskID() == TASK_SACHPRUEFUNG_DELEGATION) && workitem.getEventID() == EVENT_FREIGEBEN)) { List childs = InvoiceUtil.explodeChildList(workitem); for (ItemCollection posItem : childs) { if (posItem.getItemValueString("name").trim().isEmpty()) { // throw a plugin exception - because name is missing! String message = resourceBundleHandler.findMessage("ERROR_MISSING_POSNO"); message = message.replace("{1}", posItem.getItemValueString("numpos")); throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA, message); } // validate pos for regex pattern 'XX-YYY-####-###' if (!posItem.getItemValueString("name").matches(REGEX_POSNUMER)) { // throw a plugin exception - because name is missing! String message = resourceBundleHandler.findMessage("ERROR_FORMAT_POSNO"); message = message.replace("{1}", posItem.getItemValueString("numpos")); throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA, message); } if (posItem.getItemValueString("category").trim().isEmpty()) { // throw a plugin exception - because name is missing! String message = resourceBundleHandler.findMessage("ERROR_MISSING_CATEGORY"); message = message.replace("{1}", posItem.getItemValueString("numpos")); throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA, "Eingabefehler - Die Leistungsart in Zeile " + posItem.getItemValueString("numpos") + " muss ausgefüllt sein!"); } if (posItem.getItemValueString("tax").trim().isEmpty()) { // throw a plugin exception - because name is missing! String message = resourceBundleHandler.findMessage("ERROR_MISSING_TAX"); message = message.replace("{1}", posItem.getItemValueString("numpos")); throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA, message); } if (posItem.getItemValueFloat("amount") == 0) { // throw a plugin exception - because name is missing! String message = resourceBundleHandler.findMessage("ERROR_MISSING_NET"); message = message.replace("{1}", posItem.getItemValueString("numpos")); throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA, message); } // Buchunsperiode if (!posItem.getItemValueString(InvoiceUtil.ITEM_INVOICE_PERIOD).trim().isEmpty()) { validateBuchungsperiode(posItem.getItemValueString(InvoiceUtil.ITEM_INVOICE_PERIOD)); } } // Ergaenzung 18.05.2021: // es kann vorkommen, das die Anwender abweichende Buchungsperioden eingeben so // das die Hauptbuchungsperiode gar ncht merh vorkommt. Das darf aber nicht der // fall sein. // Im folgenden überprüfen wir ob eine Buchungszeile vorkommt in der keine oder // die Hauptbuchunsperiode ausgewählt wurde. Ist das nicht der Fal gibt es eine // Fehlermeldung für den Anwendere if (isCargoRechnung(workitem) && childs.size() > 0) { String hauptBuchungsperiode = workitem.getItemValueString("invoice.period"); boolean buchungsperiodenValid = false; for (ItemCollection posItem : childs) { String posBuchungsperiode = posItem.getItemValueString("invoice.period"); if (posBuchungsperiode.isEmpty() || posBuchungsperiode.equals(hauptBuchungsperiode)) { // alles fein! buchungsperiodenValid = true; } } if (buchungsperiodenValid == false) { // fehlerhafte Buchungsperioden. String message = resourceBundleHandler.findMessage("ERROR_MAIN_BOOKING_PERIOD"); throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA, message); } } } return workitem; } /** * Hilfsroutine die das _img item aktualisiert. */ private void updateImg(ItemCollection workitem) { // Update _img icon list String img = workitem.getItemValueString("_img"); // 031=ablehnen // 100=sofortüberweichung // 074=mahnen // sofortüberweisung if (workitem.getEventID() == 96 && !img.contains("100.png")) { img = img + ""; } // mahnen if (workitem.getEventID() == 95 && !img.contains("103.png")) { img = img + ""; } // ablehnen if (workitem.getTaskID() == 5100 && workitem.getEventID() == 90 && !img.contains("028.png")) { img = img + ""; } if (workitem.getTaskID() == 5200 && workitem.getEventID() == 90 && !img.contains("028.png")) { img = img + ""; } workitem.setItemValue("_img", img); } /** * Diese Method prüft die Buchungsperiode auf Plausibilität * * YYYY(+1)01-12 * * z.b. 202110 oder 202107 oder 202201 * * @param workitem * @throws PluginException */ private void validateBuchungsperiode(String period) throws PluginException { // buchungsperionde nur prüfen wenn noch nicht archiviert if (!period.isEmpty()) { LocalDate localDate = LocalDate.now(); int year = localDate.getYear(); // build regex.... String regex = "(" + year + "|" + (year + 1) + "|" + (year - 1) + ")(1[0-2]|0[1-9])"; if (!period.matches(regex)) { // throw a plugin exception - because name is missing! String message = resourceBundleHandler.findMessage("ERROR_BOOKING_PERIOD"); throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA, message); } } } /** * Diese Method prüft ob die "invoice.number" bereits einmal im * Rechnugnsworkflow vergeben wurde. Falls ja wird eine PluginExcpetion * ausgelöst. *

* Query Example: *

* NOT $uniqueid:"9f72fa50-4845-41ea-b6b9-ccd518c353be" AND txtcooperatespace:"9dba107e-f8ef-4150-a832-040d75a6eda7" AND invoice.number:"45" * *

* in case a duplicate invoice was detected the item invoice.number.duplicate is * filled. This item is used for a conditional event. The case is displayed as a * warning in the form. * * @throws PluginException * @throws QueryException * */ private void validateInvoiceNumber(ItemCollection workitem) throws PluginException { String invoiceNumber = workitem.getItemValueString("invoice.number"); // strip String invoiceNumberStripped = invoiceNumber.replace(" ", ""); workitem.setItemValue("invoice.number.stripped", invoiceNumberStripped); String invoiceNumberDuplicate = workitem.getItemValueString("invoice.number.duplicate"); // wenn keine Rechnungsnummer eingegeben wurde gehts weiter! if (invoiceNumber.isEmpty()) { return; } // rechnungseingang only workitems... // Change 11.4.22 - we have now an additional field: invoice.number.stripped String query = "(type:workitem OR type:workitemarchive) AND ($modelversion:rechnungseingang*) AND NOT ($uniqueid:\"" + workitem.getUniqueID() + "\") AND ((invoice.number:\"" + invoiceNumberStripped + "\") OR (invoice.number.stripped:\"" + invoiceNumberStripped + "\"))"; try { int result = this.getWorkflowService().getDocumentService().count(query, 1); if (result > 0) { // wenn _invoicenumber_duplicate bereits gesetzt ist - dann geht es ohne prüfung // weiter if (!invoiceNumberDuplicate.isEmpty()) { logger.warning("...validateion skipped by user with duplicate invoice number: " + invoiceNumber); } else { // set _invoicenumber_duplicate - dadurch wird die warnmeldung ausgegeben und // der Vorgang nicht weitergeleitet workitem.replaceItemValue("invoice.number.duplicate", invoiceNumber); String message = resourceBundleHandler.findMessage("ERROR_INVOICENO"); throw new PluginException(InvoicePlugin.class.getName(), ERROR_DUPPLICATE_INVOICE_NUMBER, message); } } else { // clear ! workitem.appendItemValue("invoice.number.duplicate.history", workitem.getItemValueString("invoice.number.duplicate")); workitem.replaceItemValue("invoice.number.duplicate", ""); } } catch (QueryException e) { throw new PluginException(PluginException.class.getName(), "QUERY ERROR", e.getMessage(), e); } } /** * Diese Method prüft ob die eingegebene IBAN nummer bereits in dem ausgewälten * Kreditor bekannt ist. Fallst nicht wird diese schnell mal in den Kreditoren * Datensatz übertragen. Dadurch muss man nicht extra die Kreditoren Verwaltung * aufrufen. Es ist ein art selbst-lernendes System. *

* Die Mehtode ruft eine Plugin Exception aus um den User zu fragen ob er das * möchte. * * @throws PluginException * @throws QueryException * */ private void checkIBANNumber(ItemCollection workitem) throws PluginException { String cdtrNumber = workitem.getItemValueString("cdtr.number"); String iban = workitem.getItemValueString("cdtr.iban"); String bic = workitem.getItemValueString("cdtr.bic"); String overtakeIBAN = workitem.getItemValueString("ibanbic.overtake"); if (cdtrNumber.isEmpty() || iban.isEmpty() || bic.isEmpty()) { // no op return; } // wenn bereits eine Überneahme angedroht wurde dann übernehemen! if (!overtakeIBAN.isEmpty()) { kreditorService.addNewIBANBIC(cdtrNumber, iban, bic); workitem.setItemValue("ibanbic.overtake", ""); return; } // search the kreditor ItemCollection cdtr = kreditorService.findCreditorDebitor(cdtrNumber); if (cdtr != null) { // Prüfen ob wir die IBAN schon kennen if (iban.equals(cdtr.getItemValueString("cdtr.iban")) && bic.equals(cdtr.getItemValueString("cdtr.bic"))) { return; } if (iban.equals(cdtr.getItemValueString("cdtr.iban2")) && bic.equals(cdtr.getItemValueString("cdtr.bic2"))) { return; } if (iban.equals(cdtr.getItemValueString("cdtr.iban3")) && bic.equals(cdtr.getItemValueString("cdtr.bic3"))) { return; } if (iban.equals(cdtr.getItemValueString("cdtr.iban4")) && bic.equals(cdtr.getItemValueString("cdtr.bic4"))) { return; } // OK - scheinbar ist diese IBAN/BIC nicht bekannt. Also fragen wir mal nach.... workitem.setItemValue("ibanbic.overtake", iban + bic); String message = resourceBundleHandler.findMessage("ERROR_IBAN_UNKNOWN"); throw new PluginException(InvoicePlugin.class.getName(), ERROR_NEW_IBANBIC, message); } } /** * Hilfsmethod prüft ob die cdtr.nummer mit einer nummer aus der Cargosoft * Import Datei übereinstimmt. * * @param workitem * @throws PluginException */ private void validateCargosoftCdtrNumber(ItemCollection workitem) throws PluginException { String crdtrNumber = workitem.getItemValueString("cdtr.number"); // wenn keine Nummer eingegeben wurde gehts weiter! if (crdtrNumber.isEmpty()) { return; } // search creditor number in cargosoft... // die cargosoft Kreditornummer beginnt seltsamerweise mit einem K..... try { String query = "(type:cargosoftkreditor) AND (name:K" + crdtrNumber + " OR name:" + crdtrNumber + ")"; List result = this.getWorkflowService().getDocumentService().find(query, 1, 0); if (result == null || result.size() == 0) { String message = resourceBundleHandler.findMessage("ERROR_CDTR_INVALID"); throw new PluginException(InvoicePlugin.class.getName(), ERROR_MISSING_DATA, message); } else { // update cargosoft crediotr name ItemCollection cargo = result.get(0); workitem.setItemValue("cdtr.name.cargosoft", cargo.getItemValueString("_VENDOR_Name")); } } catch (QueryException e) { e.printStackTrace(); } } /** * This method returns true if the current workitem is a Cargosoft Invoice. * * This can be based on different model versions e.g. rechnungseingang-de-1.2, * rechnungseingang-pl-1.0, ... * * @param workitem * @return */ public static boolean isCargoRechnung(ItemCollection workitem) { String REGEX_PATTERN = "rechnungseingang-([a-z]{2})-\\d.\\d"; // Erstellen Sie ein Pattern-Objekt Pattern pattern = Pattern.compile(REGEX_PATTERN); Matcher matcher = pattern.matcher(workitem.getModelVersion()); // Überprüfen, ob das Muster übereinstimmt return matcher.matches(); } }