diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/AGLAnalyticControllerDebitor.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/AGLAnalyticControllerDebitor.java index 4a87d1b..e4476fa 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/AGLAnalyticControllerDebitor.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/AGLAnalyticControllerDebitor.java @@ -1,9 +1,23 @@ 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; @@ -26,7 +40,6 @@ import jakarta.inject.Named; * */ @Named -// @RequestScoped @ConversationScoped public class AGLAnalyticControllerDebitor implements Serializable { @@ -36,13 +49,36 @@ public class AGLAnalyticControllerDebitor implements Serializable { @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; + + String chartData = ""; public void onEvent(@Observes AnalyticEvent event) { @@ -68,28 +104,62 @@ public class AGLAnalyticControllerDebitor implements Serializable { + "&workflowgroup=Steuerbescheid"; if ("analytic.invoices.count.all".equals(event.getKey())) { - event.setValue("" + getRechnungen()); - event.setLabel(""); + 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("" + getRechnungenOffen()); - event.setLabel(""); + 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("" + getRechnungenFaellig()); - event.setLabel(""); + 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.invoices.trend".equals(event.getKey())) { + event.setValue(chartData); + event.setLabel("Zahlung Tage"); + event.setDescription("Zahlungsmoral nach KW"); + } + } + + /** + * 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() { @@ -100,8 +170,8 @@ public class AGLAnalyticControllerDebitor implements Serializable { + " AND $modelversion:rechnungsausgang-*"; try { logger.info("...debitor analysis for " + getDbtNr() + "...."); - invoices = documentService.find(query, 999, 0, "$created", false); + refreshStats(); } catch (QueryException e) { logger.warning("Failed to query invoices: " + query + " - Error: " + e.getMessage()); invoices = new ArrayList(); @@ -110,67 +180,87 @@ public class AGLAnalyticControllerDebitor implements Serializable { } - /** - * Berechnet die Anzahl aller Rechnungen - * - * @return - */ - public int getRechnungen() { - int result = 0; + private void refreshStats() { + countAll = 0; + countOpen = 0; + countDue = 0; + countDunning = 0; if (invoices != null) { - result = invoices.size(); + 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); + + chartData = buildChartData(); + System.out.println(chartData); } - return result; + } - /** - * Berechnet die Anzahl aller noch nicht fälligen rechnungen - * - * @return - */ - public int getRechnungenOffen() { - // int result = 0; - // if (getDbtNr().isEmpty()) { - // return 0; - // } - // try { - // result = documentService - // .count("(type:workitem) AND dbtr.number:" + getDbtNr() - // + " AND $workflowgroup:\"Rechnungsausgang\" AND ($taskid:[5000 TO 5099])"); - // } catch (QueryException e) { - // logger.severe("Failed to get statistic: " + e.getMessage()); - // } - // return result; - int result = 0; - if (invoices != null) { - result = invoices.size(); - } - return result; - } + private String formatCurrency(Double value) { - /** - * Berechnet die Anzahl aller fälligen rechnungen - * - * @return - */ - public int getRechnungenFaellig() { - // int result = 0; - // if (getDbtNr().isEmpty()) { - // return 0; - // } - // try { - // result = documentService - // .count("(type:workitem) AND dbtr.number:" + getDbtNr() - // + " AND $workflowgroup:\"Rechnungsausgang\" AND ($taskid:[5100 TO 5199])"); - // } catch (QueryException e) { - // logger.severe("Failed to get statistic: " + e.getMessage()); - // } - // return result; - int result = 0; - if (invoices != null) { - result = invoices.size(); - } - return result; + DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.getDefault()); + symbols.setGroupingSeparator('.'); + symbols.setDecimalSeparator(','); + + DecimalFormat formatter = new DecimalFormat("#,##0.00", symbols); + + return formatter.format(value); + + // %, => local-specific thousands separator + // .2f => positions after decimal point + // return String.format("%,.2f", value); } /** @@ -186,4 +276,267 @@ public class AGLAnalyticControllerDebitor implements Serializable { return dbtNr; } + /** + * Sucht alle Rechnugnen aus einem Zeitraum und sammelt Zahlungsziel und + * Zahlungszeitspanne gruppiert nach monaten + * + */ + public Map getStats() { + + if (stats != null) { + return stats; + } + + // recompute + stats = new TreeMap(); + if (getDbtNr().isEmpty()) { + return stats; + } + + // Letzen 6 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.warning("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(); + 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() { + + getStats(); + + // 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: 'Zahlungsziel',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 + result = result.substring(0, result.length() - 1); + result = result + "]"; + result = result + "}, "; + + // Datasets 2 + result = result + "{label: 'Zahldauer',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 + 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); + + } + } diff --git a/office-alexander-logistics-app/src/main/webapp/pages/workitems/parts/alexander/analyze_plain.xhtml b/office-alexander-logistics-app/src/main/webapp/pages/workitems/parts/alexander/analyze_plain.xhtml index ac18535..7b100f9 100644 --- a/office-alexander-logistics-app/src/main/webapp/pages/workitems/parts/alexander/analyze_plain.xhtml +++ b/office-alexander-logistics-app/src/main/webapp/pages/workitems/parts/alexander/analyze_plain.xhtml @@ -48,7 +48,9 @@ -
#{analyticController.getLabel(item.name)}
+
+ +