diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 14ddaa8..1ed3eea 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,12 @@ # Versionen +### 1.2.13 -### 1.2.12 (Development) + - Payment Statistik + - Neue Indexfelder 'invoice.date' 'payment.date' + +**WICHTIG:** Es muss nach dem Update einmal der Index neu berechnet werden. + +### 1.2.12 - Neue Debitoren/Kreditoren Verwaltung - Zusätzliche E-Mail Realisiert über neue Custom Feld 'textlist' diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/DebitorStatistikController.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/DebitorStatistikController.java index 933502b..c082f9c 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/DebitorStatistikController.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/DebitorStatistikController.java @@ -4,12 +4,19 @@ import java.io.Serializable; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +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.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeMap; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.enterprise.context.ConversationScoped; import javax.inject.Inject; @@ -37,6 +44,8 @@ public class DebitorStatistikController implements Serializable { @Inject protected DocumentService documentService; + TreeMap stats = null; + @Inject protected WorkflowController workflowController; @@ -141,29 +150,42 @@ public class DebitorStatistikController implements Serializable { * Zahlungszeitspanne gruppiert nach monaten * */ - public void collect(Date from, Date to) { + public Map getStats() { - Map stats = new HashMap(); + if (stats != null) { + return stats; + } + + // recompute + stats = new TreeMap(); // invoice.date:[20210101 TO 20210201] DateFormat df = new SimpleDateFormat("yyyymmdd"); String query = "(type:workitemarchive) AND dbtr.number:" + getDbtNr() - + " AND $workflowgroup:\"Rechnungsausgang\" AND ($taskid:[5300 TO 5399])"; + + " AND $workflowgroup:\"Rechnungsausgang\" AND ($taskid:[5900 TO 5999])"; - query = query + " AND (invoice.date:[" + df.format(from) + " TO " + df.format(to) + "])"; + // query = query + " AND (invoice.date:[" + df.format(from) + " TO " + + // df.format(to) + "])"; logger.info("query = " + query); try { List invoices = documentService - .find(query, 9999, 0); + .findStubs(query, 9999, 0, "$created", false); for (ItemCollection invoice : invoices) { try { - + logger.warning("Rechnung: " + invoice.getUniqueID()); Date invoiceDate = invoice.getItemValueDate("invoice.date"); - Date paymentDate = findLastEventDate(invoice); + Date invoiceDueDate = invoice.getItemValueDate("invoice.duedate"); + Date paymentDate = findPaymentDateByWorkitem(invoice); + if (paymentDate == null) { + // fall back method + logger.warning("Es wurde kein Zahlungseingang gefunden"); + // paymentDate = findPaymentDateByLog(invoice); + continue; + } - if (invoiceDate != null && paymentDate != null) { + if (invoiceDate != null && paymentDate != null && invoiceDueDate != null) { // do we have a stats object? DebitorStatistikData statData = stats.get(getYearMonth(invoiceDate)); if (statData == null) { @@ -172,6 +194,7 @@ public class DebitorStatistikController implements Serializable { } // jetzt irgendwas ausrechnen + statData.compute(invoiceDate, invoiceDueDate, paymentDate); // .. und wieder speichern stats.put(statData.toString(), statData); @@ -189,8 +212,53 @@ public class DebitorStatistikController implements Serializable { logger.severe("Failed to get statistic: " + e.getMessage()); } + // Step 2: Sort the list based on the alphanumeric order of the keys + + // stats = new TreeMap<>(stats); + completeMissingMonths(); + return stats; + } + /** + * 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 * @@ -214,7 +282,49 @@ public class DebitorStatistikController implements Serializable { } /** - * Finds the date when a workitem last reached the current task + * 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 usn aber nur für den + // letzten + String sQuery = " (type:\"workitem\" OR type:\"workitemarchive\") " + // + " AND ($workflowgroup: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; + + } + + /** + * Finds the date when a workitem last reached the current task 5900 by reading + * the eventLog + * + * This method is not so save as the method findPaymentDateByWorkitem that is + * searching the payment workitem. * * 2023-06-28T12:40:17.437|rechnungsausgang-de-1.0|5001.50|5900| * @@ -222,7 +332,7 @@ public class DebitorStatistikController implements Serializable { * @return * @throws ParseException */ - public Date findLastEventDate(ItemCollection invoice) throws ParseException { + public Date findPaymentDateByLog(ItemCollection invoice) throws ParseException { // Create a SimpleDateFormat instance with the desired format SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); @@ -248,4 +358,92 @@ public class DebitorStatistikController implements Serializable { } + /** + * 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 = "{"; + + // 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 + " \"backgroundColor\" : [\"#EBA05F\"],"; + + 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 + " \"backgroundColor\" : [\"#70B088\" ],"; + + result = result + + " trendlineLinear: { colorMin: \"red\", colorMax: \"green\", lineStyle: \"dotted\", width: 2 , projection: 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 + "] }"; + + return result; + } + } diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/DebitorStatistikData.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/DebitorStatistikData.java index 7bf11d2..cb895fa 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/DebitorStatistikData.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/DebitorStatistikData.java @@ -2,18 +2,20 @@ package com.alexanderlogistics; import java.util.Calendar; import java.util.Date; +import java.util.logging.Logger; /** * Speichert Payment statistik */ public class DebitorStatistikData { + private static Logger logger = Logger.getLogger(DebitorStatistikData.class.getName()); int month; int year; int count = 0; - int paymentTime; // Zahlungsziel - int paymentDays; // Tatsächliche Zahlung + int averageDueDays; // Zahlungsziel + int averagePaymentDays; // Tatsächliche Zahlung public DebitorStatistikData(Date date) { // Create a Calendar instance and set the date @@ -40,8 +42,40 @@ public class DebitorStatistikData { * @param invoiceDate * @param paymentDate */ - public void compute(Date invoiceDate, Date paymentDate) { + public void compute(Date invoiceDate, Date invoiceDueDate, Date paymentDate) { // und was nun? + count++; + + logger.info("...invoiceDate= " + invoiceDate); + logger.info("...invoiceDueDate= " + invoiceDueDate); + logger.info("...paymentDate= " + paymentDate); + + // Fälligkeit in Tagen + long milliseconds1 = invoiceDate.getTime(); + long milliseconds2 = invoiceDueDate.getTime(); + // Berechnen Sie die Differenz in Millisekunden + long diffMilliseconds = milliseconds2 - milliseconds1; + long dueTime = diffMilliseconds / (24 * 60 * 60 * 1000); + logger.info("...Fälligkeit in tagen= " + dueTime); + averageDueDays = (int) (averageDueDays + dueTime / count); + + // Bezahlt nach Tagen + milliseconds1 = invoiceDate.getTime(); + milliseconds2 = paymentDate.getTime(); + // Berechnen Sie die Differenz in Millisekunden + diffMilliseconds = milliseconds2 - milliseconds1; + long paymentTime = diffMilliseconds / (24 * 60 * 60 * 1000); + logger.info("...Zahlung erfolgte nach tagen= " + paymentTime); + averagePaymentDays = (int) (averagePaymentDays + paymentTime / count); + + } + + public int getAverageDueDays() { + return averageDueDays; + } + + public int getAveragePaymentDays() { + return averagePaymentDays; } /** diff --git a/office-alexander-logistics-app/src/main/resources/imixs.properties b/office-alexander-logistics-app/src/main/resources/imixs.properties index 0548dc4..7c019ec 100644 --- a/office-alexander-logistics-app/src/main/resources/imixs.properties +++ b/office-alexander-logistics-app/src/main/resources/imixs.properties @@ -4,8 +4,8 @@ lucence.indexDir=${imixs-office.IndexDir} index.fields=txtsearchstring,txtSubject,txtname,txtEmail,txtUserName,namCreator,txtworkflowgroup,txtworkflowstatus,txtWorkflowAbstract,txtWorkflowSummary,txtworkflowhistory,txtspacename,txtprocessname,_subject,_description,_name,_projectnumber,_projectname,_ordernumber,_contractnumber,datDueDate,txtcommentlog,htmldescription,htmldocumentation,dms,dms_names,invoice.number.stripped,_childitems,$file.names,_VENDOR_NAME,dbtr.number,cdtr.number index.fields.analyze=txtUsername -index.fields.noanalyze=type,$UniqueIDRef,$created,$modified,$ModelVersion,$participants,namCreator,$ProcessID,datDate,txtWorkflowGroup,txtemail, datdate, datfrom, datto, numsequencenumber,dms_count,invoice.number,invoice.number.stripped,invoice.duedate,taxonomy.verteilung.stop,taxonomy.sachpruefung.stop,taxonomy.verteilung.start,taxonomy.sachpruefung.start,taxonomy.buchhaltung.start,taxonomy.buchhaltung.stop,dbtr.number,cdtr.number -index.fields.store=process.name,txtProcessName,txtWorkflowImageURL +index.fields.noanalyze=type,$UniqueIDRef,$created,$modified,$ModelVersion,$participants,namCreator,$ProcessID,datDate,txtWorkflowGroup,txtemail, datdate, datfrom, datto, numsequencenumber,dms_count,invoice.number,invoice.number.stripped,invoice.date,invoice.duedate,taxonomy.verteilung.stop,taxonomy.sachpruefung.stop,taxonomy.verteilung.start,taxonomy.sachpruefung.start,taxonomy.buchhaltung.start,taxonomy.buchhaltung.stop,dbtr.number,cdtr.number,payment.date +index.fields.store=process.name,txtProcessName,txtWorkflowImageURL,payment.date,invoice.number,invoice.date,invoice.duedate index.fields.category=space.name,space.ref,taxonomy.verteilung.stop.by,taxonomy.sachpruefung.stop.by,taxonomy.buchhaltung.stop.by office.search.noanalyze=invoice.number,invoice.number.stripped diff --git a/office-alexander-logistics-app/src/main/webapp/js/chartjs-plugin-trendline.min.js b/office-alexander-logistics-app/src/main/webapp/js/chartjs-plugin-trendline.min.js new file mode 100644 index 0000000..a131839 --- /dev/null +++ b/office-alexander-logistics-app/src/main/webapp/js/chartjs-plugin-trendline.min.js @@ -0,0 +1,18 @@ +/** + * Minified by jsDelivr using Terser v5.17.1. + * Original file: /npm/chartjs-plugin-trendline@2.0.3/src/chartjs-plugin-trendline.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/*! + * chartjs-plugin-trendline.js + * Version: 2.0.3 + * + * Copyright 2023 Marcus Alsterfjord + * Released under the MIT license + * https://github.com/Makanz/chartjs-plugin-trendline/blob/master/README.md + * + * Mod by: vesal: accept also xy-data so works with scatter + */ +var pluginTrendlineLinear={id:"chartjs-plugin-trendline",afterDatasetsDraw:function(t){var i,e;for(var s in t.scales)if("x"==s[0]?e=t.scales[s]:i=t.scales[s],e&&i)break;var a=t.ctx;t.data.datasets.forEach((function(i,s){var n=i.alwaysShowTrendline||t.isDatasetVisible(s);if(i.trendlineLinear&&n&&i.data.length>1){var r=t.getDatasetMeta(s);addFitter(r,a,i,e,t.scales[r.yAxisID])}})),a.setLineDash([])}};function addFitter(t,i,e,s,a){var n=e.borderColor||"rgba(169,169,169, .6)",r=e.trendlineLinear.colorMin||n,o=e.trendlineLinear.colorMax||n,l=e.trendlineLinear.width||e.borderWidth,h=e.trendlineLinear.lineStyle||"solid",u=e.trendlineLinear.fillColor;l=void 0!==l?l:3;var d=new LineFitter,m=e.data.findIndex((t=>null!=t)),c=e.data.length-1,x=t.data[m].x,f=t.data[c].x,X="object"==typeof e.data[m];e.data.forEach((function(t,i){if(null!=t)if(["time","timeseries"].includes(s.options.type)){var e=null!=t.x?t.x:t.t;d.add(new Date(e).getTime(),t.y)}else X?isNaN(t.x)||isNaN(t.y)?isNaN(t.x)?isNaN(t.y)||d.add(i,t.y):d.add(i,t.x):d.add(t.x,t.y):d.add(i,t)}));var p,g,v=s.getPixelForValue(d.minx),y=a.getPixelForValue(d.f(d.minx));if(e.trendlineLinear.projection&&d.scale()<0){var w=d.fo();wL){var Y=y-L,T=y-g;y=L,v+=F*(Y/T)}else if(g>L){Y=g-L,T=g-y;g=L,p=F-(p-(F-F*(Y/T)))}i.lineWidth=l,"dotted"===h&&i.setLineDash([2,3]),i.beginPath(),i.moveTo(v,y),i.lineTo(p,g);var P=i.createLinearGradient(v,y,p,g);gthis.maxx&&(this.maxx=t),i>this.maxy&&(this.maxy=i)},f:function(t){t=parseFloat(t);var i=this.count*this.sumX2-this.sumX*this.sumX;return(this.sumX2*this.sumY-this.sumX*this.sumXY)/i+t*((this.count*this.sumXY-this.sumX*this.sumY)/i)},fo:function(){var t=this.count*this.sumX2-this.sumX*this.sumX;return-((this.sumX2*this.sumY-this.sumX*this.sumXY)/t)/((this.count*this.sumXY-this.sumX*this.sumY)/t)},scale:function(){var t=this.count*this.sumX2-this.sumX*this.sumX;return(this.count*this.sumXY-this.sumX*this.sumY)/t}},"undefined"!=typeof window&&window.Chart&&(window.Chart.hasOwnProperty("register")?window.Chart.register(pluginTrendlineLinear):window.Chart.plugins.register(pluginTrendlineLinear));try{module.exports=exports=pluginTrendlineLinear}catch(t){} +//# sourceMappingURL=/sm/a0bca30b6b7f4fbdef5c66a04bd36e4926fa058e81cc14753a8b9dbb36676c5d.map \ No newline at end of file diff --git a/office-alexander-logistics-app/src/main/webapp/pages/workitems/forms/alexander/section_debitor_statistic.xhtml b/office-alexander-logistics-app/src/main/webapp/pages/workitems/forms/alexander/section_debitor_statistic.xhtml index 4a03772..ec6853a 100644 --- a/office-alexander-logistics-app/src/main/webapp/pages/workitems/forms/alexander/section_debitor_statistic.xhtml +++ b/office-alexander-logistics-app/src/main/webapp/pages/workitems/forms/alexander/section_debitor_statistic.xhtml @@ -49,25 +49,43 @@ - - - - + +
+ +
+ + + +