Kreditoren Statistik

This commit is contained in:
Ralph Soika 2023-07-22 14:31:17 +02:00
parent ff6ff80506
commit 520c0e996c
6 changed files with 301 additions and 27 deletions

View file

@ -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'

View file

@ -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<String, DebitorStatistikData> 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<String, DebitorStatistikData> getStats() {
Map<String, DebitorStatistikData> stats = new HashMap<String, DebitorStatistikData>();
if (stats != null) {
return stats;
}
// recompute
stats = new TreeMap<String, DebitorStatistikData>();
// 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<ItemCollection> 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<LocalDate> dates = new ArrayList<>();
Set<String> 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<ItemCollection> 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
*
*
* <pre>
{
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
]
}]
}
* </pre>
*
* @return
*/
public String buildChartData() {
getStats();
// build a list of all lables....
List<String> statusLabels = new ArrayList<String>();
Set<String> 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<String, DebitorStatistikData> 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<String, DebitorStatistikData> 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;
}
}

View file

@ -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;
}
/**

View file

@ -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

View file

@ -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();w<d.minx&&(w=d.maxx),p=s.getPixelForValue(w),g=a.getPixelForValue(d.f(w))}else p=s.getPixelForValue(d.maxx),g=a.getPixelForValue(d.f(d.maxx));X||(v=x,p=f);var L=t.controller.chart.chartArea.bottom,F=t.controller.chart.width;if(y>L){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);g<y?(P.addColorStop(0,o),P.addColorStop(1,r)):(P.addColorStop(0,r),P.addColorStop(1,o)),i.strokeStyle=P,i.stroke(),i.closePath(),u&&(i.fillStyle=u,i.beginPath(),i.moveTo(v,y),i.lineTo(p,g),i.lineTo(p,L),i.lineTo(v,L),i.closePath(),i.fill())}function LineFitter(){this.count=0,this.sumX=0,this.sumX2=0,this.sumXY=0,this.sumY=0,this.minx=1e100,this.maxx=-1e100,this.maxy=-1e100}LineFitter.prototype={add:function(t,i){t=parseFloat(t),i=parseFloat(i),this.count++,this.sumX+=t,this.sumX2+=t*t,this.sumXY+=t*i,this.sumY+=i,t<this.minx&&(this.minx=t),t>this.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

View file

@ -49,24 +49,42 @@
</dd>
</dl>
</h:panelGroup>
<!-- Chart Diagramm -->
<div id="container" style="width: 75%;">
<canvas id="canvas"></canvas>
</div>
<script type="text/javascript"
src="#{facesContext.externalContext.requestContextPath}/js/chartjs/chart.min.js?build=#{app.application_build_timestamp}"></script>
<script type="text/javascript"
src="#{facesContext.externalContext.requestContextPath}/js/chartjs-plugin-trendline.min.js?build=#{app.application_build_timestamp}"></script>
<script type="text/javascript">
/*<![CDATA[*/
var barChartData = #{ debitorStatistikController.buildChartData() };
$(document).ready(
function () {
// update the checkboxes of the stored invoice uniqueIDs
// updateInvoiceSelection();
// update the diagram
var ctx = document.getElementById("canvas").getContext("2d");
window.myBar = new Chart(ctx, {
type: 'bar',
data: barChartData,
options: {
responsive: true,
legend: {
position: 'top',
},
title: {
display: true,
text: 'Zahlungsstatistik'
}
}
});
});
/*
/*]]>*/
</script>