package com.alexanderlogistics; import java.io.Serializable; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.ParseException; import java.time.LocalDate; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.logging.Logger; import java.util.stream.Collectors; import org.imixs.marty.team.TeamService; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.exceptions.QueryException; import org.imixs.workflow.faces.data.WorkflowController; import org.imixs.workflow.office.forms.AnalyticController; import org.imixs.workflow.office.forms.AnalyticEvent; import jakarta.enterprise.context.ConversationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.inject.Named; /** * Der AGLAnalyticControllerDebitor berechnet verschiedene Analyse Daten von * Ausgangsrechnungen * * @author rsoika * */ @Named @ConversationScoped public class AGLAnalyticControllerDebitor implements Serializable { private static final long serialVersionUID = 1L; private static Logger logger = Logger.getLogger(AnalyticController.class.getName()); @Inject protected DocumentService documentService; @Inject protected AGLConfigController aglConfigController; @Inject protected WorkflowController workflowController; @Inject TeamService teamService; TreeMap stats = null; List invoices = null; int countAll = 0; int countOpen = 0; int countDue = 0; int countDunning = 0; double totalAllCurrency1 = 0; double totalAllCurrency2 = 0; double totalOpenCurrency1 = 0; double totalOpenCurrency2 = 0; double totalDueCurrency1 = 0; double totalDueCurrency2 = 0; double totalDunningCurrency1 = 0; double totalDunningCurrency2 = 0; double averagePaymentDue = 0; double averagePaymentDays = 0; String chartData = ""; public void onEvent(@Observes AnalyticEvent event) { if (!"workitem".equals(event.getWorkitem().getType()) || !event.getWorkitem().getModelVersion().startsWith("analyse-debitor")) { // no op return; } String dbtrNumber = event.getWorkitem().getItemValueString("dbtr.number"); String dbtrNumberLast = event.getWorkitem().getItemValueString("dbtr.number.last"); // Recompute only if last dbtr.number has changed or no values yet computed // load all invoices? if (invoices == null || (!dbtrNumber.isEmpty() && !dbtrNumber.equals(dbtrNumberLast))) { resetStats(event); event.getWorkitem().setItemValue("dbtr.number.last", dbtrNumber); logger.info("Analyse new debitor data for : " + dbtrNumber); loadRechnungen(event); chartData = buildChartData(); } // use cache? if (event.getWorkitem().hasItem(event.getKey())) { // logger.info(" use cache for " + event.getKey()); // no op return; } String link = "/pages/workitems/worklist.xhtml" + "?phrase=" + getDbtNr(); logger.fine("process ref=" + workflowController.getWorkitem().getItemValueString("process.ref")); ItemCollection process = documentService .load(workflowController.getWorkitem().getItemValueString("process.ref")); if (process != null) { link = link + "&processref=" + process.getUniqueID() + "&phrase=" + getDbtNr(); } if ("analytic.invoices.count.all".equals(event.getKey())) { event.setValue("" + countAll); event.setLabel(formatTotals(totalAllCurrency1, totalAllCurrency2)); event.setDescription("Total invoices"); event.getWorkitem().setItemValue("invoices.total", event.getValue()); event.setLink(link); } if ("analytic.invoices.count.open".equals(event.getKey())) { event.setValue("" + countOpen); event.setLabel(formatTotals(totalOpenCurrency1, totalOpenCurrency2)); event.setDescription("Total invoices not yet in due"); event.getWorkitem().setItemValue("invoices.total.open", event.getValue()); event.setLink(link); } if ("analytic.invoices.count.due".equals(event.getKey())) { event.setValue("" + countDue); event.setLabel(formatTotals(totalDueCurrency1, totalDueCurrency2)); event.setDescription("Total invoices in due"); event.getWorkitem().setItemValue("invoices.total.due", event.getValue()); event.setLink(link); } if ("analytic.invoices.count.dunning".equals(event.getKey())) { event.setValue("" + countDunning); event.setLabel(formatTotals(totalDunningCurrency1, totalDunningCurrency2)); event.setDescription("Total invoices in dunning"); event.getWorkitem().setItemValue("invoices.total.dunning", event.getValue()); event.setLink(link); } if ("analytic.payment.avg.due".equals(event.getKey())) { event.setValue("" + averagePaymentDue); event.setLabel("days"); event.setDescription("Average terms of credit in the last 12 months."); event.getWorkitem().setItemValue("payment.avg.due", event.getValue()); event.setLink(link); } if ("analytic.payment.avg.days".equals(event.getKey())) { event.setValue("" + averagePaymentDays); event.setLabel("days"); event.setDescription("Average duration for payment during the last 12 months."); event.getWorkitem().setItemValue("payment.avg.days", event.getValue()); event.setLink(link); } if ("analytic.invoices.trend".equals(event.getKey())) { event.setValue(chartData); event.setLabel("Payment duration in days"); event.setDescription("Payment practice by week"); } } /** * Formatiert zwei währungen untereinander * * @param totalCurrency1 * @param totalCurrency2 * @return */ private String formatTotals(double totalCurrency1, double totalCurrency2) { List currencies = aglConfigController.getWorkitem().getItemValueList("currency.out", String.class); if (currencies.size() < 2) { currencies.add(currencies.get(0)); } String label = "" + formatCurrency(totalCurrency1) + " " + currencies.get(0) + ""; label = label + "
" + formatCurrency(totalCurrency2) + " " + currencies.get(1) + ""; return label; } private void loadRechnungen(AnalyticEvent event) { if (getDbtNr().isEmpty()) { invoices = new ArrayList(); stats = null; } else { logger.info(" ├──load invoices for " + getDbtNr() + "...."); String query = "(type:workitem) AND dbtr.number:" + getDbtNr() + " AND $modelversion:rechnungsausgang-*"; try { logger.info(" ├──refresh invoice stats for " + getDbtNr() + "...."); invoices = documentService.find(query, 999, 0, "$created", false); calculateStats(event); } catch (QueryException e) { logger.warning("Failed to query invoices: " + query + " - Error: " + e.getMessage()); invoices = new ArrayList(); } } } /** * Läd die statistik daten zu einem debitor aus den aktuellen Rechnungen */ private void calculateStats(AnalyticEvent event) { countAll = 0; countOpen = 0; countDue = 0; countDunning = 0; logger.info(" ├──calculate stats for " + getDbtNr() + "...."); // do we have data? if (invoices != null && invoices.size() > 0) { countAll = invoices.size(); for (ItemCollection invoice : invoices) { // währung 1 oder 2? if (invoice.getItemValueDouble("invoice.rate") == 0) { totalAllCurrency1 = totalAllCurrency1 + invoice.getItemValueDouble("invoice.saldo"); } else { totalAllCurrency2 = totalAllCurrency2 + invoice.getItemValueDouble("invoice.saldo"); } int task = invoice.getTaskID(); // Open if (task >= 5000 && task <= 5099) { countOpen++; // währung 1 oder 2? if (invoice.getItemValueDouble("invoice.rate") == 0) { totalOpenCurrency1 = totalOpenCurrency1 + invoice.getItemValueDouble("invoice.saldo"); } else { totalOpenCurrency2 = totalOpenCurrency2 + invoice.getItemValueDouble("invoice.saldo"); } } // Due if (task >= 5100 && task <= 5199) { countDue++; // währung 1 oder 2? if (invoice.getItemValueDouble("invoice.rate") == 0) { totalDueCurrency1 = totalDueCurrency1 + invoice.getItemValueDouble("invoice.saldo"); } else { totalDueCurrency2 = totalDueCurrency2 + invoice.getItemValueDouble("invoice.saldo"); } } // Dunning if (task >= 5200) { countDunning++; // währung 1 oder 2? if (invoice.getItemValueDouble("invoice.rate") == 0) { totalDunningCurrency1 = totalDunningCurrency1 + invoice.getItemValueDouble("invoice.saldo"); } else { totalDunningCurrency2 = totalDunningCurrency2 + invoice.getItemValueDouble("invoice.saldo"); } } } totalAllCurrency1 = InvoiceUtil.round(totalAllCurrency1); totalAllCurrency2 = InvoiceUtil.round(totalAllCurrency2); totalOpenCurrency1 = InvoiceUtil.round(totalOpenCurrency1); totalOpenCurrency2 = InvoiceUtil.round(totalOpenCurrency2); totalDueCurrency1 = InvoiceUtil.round(totalDueCurrency1); totalDueCurrency2 = InvoiceUtil.round(totalDueCurrency2); totalDunningCurrency1 = InvoiceUtil.round(totalDunningCurrency1); totalDunningCurrency2 = InvoiceUtil.round(totalDunningCurrency2); } else { // reset data because no invoices are available ... resetStats(event); } } /** * Setzt alle Statistikwerte zurück */ private void resetStats(AnalyticEvent event) { logger.info(" ├──reset stats for " + getDbtNr() + "...."); stats = null; totalAllCurrency1 = 0; totalAllCurrency2 = 0; totalOpenCurrency1 = 0; totalOpenCurrency2 = 0; totalDueCurrency1 = 0; totalDueCurrency2 = 0; totalDunningCurrency1 = 0; totalDunningCurrency2 = 0; // chartData = null; chartData = "{}"; averagePaymentDue = 0; averagePaymentDays = 0; event.getWorkitem().removeItem("analytic.invoices.count.all"); event.getWorkitem().removeItem("analytic.invoices.count.open"); event.getWorkitem().removeItem("analytic.invoices.count.due"); event.getWorkitem().removeItem("analytic.invoices.count.dunning"); event.getWorkitem().removeItem("analytic.payment.avg.due"); event.getWorkitem().removeItem("analytic.payment.avg.days"); event.getWorkitem().removeItem("analytic.invoices.trend"); } private void getDurchschnittZahlungsziel() { if (stats == null) { averagePaymentDue = 0; return; } int count = 0; long gesamt = 0; for (Map.Entry entry : stats.entrySet()) { DebitorStatistikData statData = entry.getValue(); if (statData.count > 0) { gesamt = gesamt + statData.getAverageDueDays(); count++; } } averagePaymentDue = Math.round((double) gesamt / count); } private void getDurchschnittZahlungsdauer() { if (stats == null) { averagePaymentDays = 0; return; } int count = 0; long gesamt = 0; for (Map.Entry entry : stats.entrySet()) { DebitorStatistikData statData = entry.getValue(); if (statData.count > 0) { gesamt = gesamt + statData.getAveragePaymentDays(); count++; } } averagePaymentDays = Math.round((double) gesamt / count); } private String formatCurrency(Double value) { DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.getDefault()); symbols.setGroupingSeparator('.'); symbols.setDecimalSeparator(','); DecimalFormat formatter = new DecimalFormat("#,##0.00", symbols); return formatter.format(value); } /** * Hilfsmethode die das fuehrende K/D aus der Debitorennummer entfernt * * @return */ private String getDbtNr() { String dbtNr = workflowController.getWorkitem().getItemValueString("dbtr.number"); if (dbtNr.startsWith("D") || dbtNr.startsWith("K")) { dbtNr = dbtNr.substring(1); } return dbtNr; } /** * Sucht alle Rechnugnen aus einem Zeitraum und sammelt Zahlungsziel und * Zahlungszeitspanne gruppiert nach monaten * */ public Map loadStats() { if (stats != null) { return stats; } logger.info(" ├──load stats for " + getDbtNr() + "...."); // recompute stats = new TreeMap(); if (getDbtNr().isEmpty()) { return stats; } // Letzen 12 Monate LocalDate endDate = LocalDate.now(); LocalDate startDate = endDate.minusMonths(12); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); String startDateStr = startDate.format(formatter); String endDateStr = endDate.format(formatter); String query = "(type:workitemarchive) AND " + " invoice.date:[" + startDateStr + " TO " + endDateStr + "] AND " + "dbtr.number:" + getDbtNr() + " AND $modelversion:rechnungsausgang-* AND ($taskid:[5900 TO 5999])"; logger.fine("query = " + query); try { List invoices = documentService .findStubs(query, 9999, 0, "$created", false); for (ItemCollection invoice : invoices) { try { logger.fine("Rechnung: " + invoice.getUniqueID()); Date invoiceDate = invoice.getItemValueDate("invoice.date"); Date invoiceDueDate = invoice.getItemValueDate("invoice.duedate"); Date paymentDate = findPaymentDateByWorkitem(invoice); if (paymentDate == null) { logger.fine("Es wurde kein Zahlungseingang gefunden"); continue; } if (invoiceDate != null && paymentDate != null && invoiceDueDate != null) { // do we have a stats object? DebitorStatistikData statData = stats.get(getYearMonth(invoiceDate)); if (statData == null) { // create a new one statData = new DebitorStatistikData(invoiceDate); } // jetzt irgendwas ausrechnen statData.compute(invoiceDate, invoiceDueDate, paymentDate); // .. und wieder speichern stats.put(statData.toString(), statData); } else { logger.warning("No payment Date found for invoice " + invoice.getUniqueID()); } } catch (ParseException e) { logger.warning("Unable to parse payment Date for invoice " + invoice.getUniqueID()); } } } catch (QueryException e) { logger.severe("Failed to get statistic: " + e.getMessage()); } // Step 2: Sort the list based on the alphanumeric order of the keys completeMissingMonths(); // berechne zahluntsziel durchscnitt getDurchschnittZahlungsziel(); getDurchschnittZahlungsdauer(); return stats; } /** * Finds the payment.date for a invoice. * * Wir suchen alle zugeordneten Zahlungseingägne und nehmen den letzten. * * WICHTIG: Es muss ggf. der index neu aufgebaut werden, da payment.date nun ein * index feld ist * * @param invoice * @return * @throws ParseException */ public Date findPaymentDateByWorkitem(ItemCollection invoice) throws ParseException { // Wir selektieren alle Zahlungseingänge interessieren uns aber nur für den // letzten String sQuery = " (type:\"workitem\" OR type:\"workitemarchive\") " + // " AND ($modelversion:zahlungseingang-*) AND ($workitemref:\"" + invoice.getUniqueID() + "\" )"; List workitems = null; try { workitems = documentService.findStubs(sQuery, 99, 0, "payment.date", true); if (workitems.size() > 0) { return workitems.get(0).getItemValueDate("payment.date"); } } catch (QueryException e) { e.printStackTrace(); } // no date found! return null; } /** * Diese Methode baut die Datenstruktur für das Chart Diagram zusammen * * *
		{
		 labels: ["January", "February", "March", "April", "May", "June", "July"],
		 datasets: [{
			label: 'Dataset 1',
			//backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(),
			//borderColor: window.chartColors.red,
			borderWidth: 1,
			data: [
				70, 70, 70, 70, 79, 50, 50
			]
		}, {
			label: 'Dataset 2',
			//backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(),
			//borderColor: window.chartColors.blue,
			borderWidth: 1,
			data: [
				70, 70, 170, 7, 79, 50, 50
			]
		}]
	   }
	 * 
* * @return */ public String buildChartData() { loadStats(); logger.info(" ├──build chart for " + getDbtNr() + "...."); // build a list of all lables.... List statusLabels = new ArrayList(); Set keys = stats.keySet(); for (String _key : keys) { statusLabels.add(_key); } String result = "{\n \"type\": \"bar\",\n \"data\": { "; // Lables result = result + "\"labels\" : [ "; result = result + statusLabels.stream().collect(Collectors.joining("\",\"", "\"", "\"")); result = result + "],"; result = result + "\"datasets\": ["; // Datasets 1 result = result + "{\"label\": \"Days for payment\",\"borderWidth\": 1,"; result = result + " \"borderColor\": [\"#3B6B82\"],"; result = result + " \"backgroundColor\" : [\"#CFE9F5\"], \"fill\": true,\"tension\": 0.5,"; result = result + "\"data\": ["; for (Map.Entry entry : stats.entrySet()) { result = result + entry.getValue().getAverageDueDays() + ","; } // cut last comma if (result.endsWith(",")) { result = result.substring(0, result.length() - 1); } result = result + "]"; result = result + "}, "; // Datasets 2 result = result + "{\"label\": \"Payment duration\",\"borderWidth\": 1,"; result = result + " \"borderColor\": [\"#E73B65\"],\"backgroundColor\" : [\"#70B088\" ], \"tension\": 0.5,\"fill\": true,"; result = result + " \"trendlineLinear\": { \"colorMin\": \"red\", \"colorMax\": \"green\", \"lineStyle\": \"dotted\", \"width\": 2 , \"projection\": true },"; result = result + "\"data\": ["; for (Map.Entry entry : stats.entrySet()) { result = result + entry.getValue().getAveragePaymentDays() + ","; } // cut last comma if (result.endsWith(",")) { result = result.substring(0, result.length() - 1); } result = result + "]"; result = result + "} "; // ende result = result + "] }"; result = result + "}"; return result; } /** * Hilfsmethode ergänzt die fehlenden Monate * * @param yearMonths * @return */ private void completeMissingMonths() { if (stats == null || stats.size() < 3) { return; } // Convert the list of strings to a list of LocalDate objects List dates = new ArrayList<>(); Set yearMonths = this.stats.keySet(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMM"); for (String yearMonth : yearMonths) { dates.add(YearMonth.parse(yearMonth, formatter).atDay(1)); } // Find the lowest and highest dates LocalDate lowestDate = Collections.min(dates); LocalDate highestDate = Collections.max(dates); LocalDate current = lowestDate; current = current.plusMonths(1); while (!current.isAfter(highestDate)) { // existiert der monat? String formattedDate = current.format(formatter); DebitorStatistikData entry = stats.get(formattedDate); if (entry == null) { // add missing entry stats.put(formattedDate, new DebitorStatistikData(current.getYear(), current.getMonthValue())); } current = current.plusMonths(1); } } /** * returns * * 202304 from a given date */ public String getYearMonth(Date date) { // Create a Calendar instance and set the date Calendar calendar = Calendar.getInstance(); calendar.setTime(date); // Get the year from the Calendar object int year = calendar.get(Calendar.YEAR); // Get the month from the Calendar object int month = calendar.get(Calendar.MONTH); // Increment the month by 1 since Calendar months are zero-based month++; // Convert the month to a String with leading "0" if necessary return "" + year + (month < 10 ? "0" + month : "" + month); } }