office-alexander-logistics/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/AGLAnalyticControllerDebitor.java

653 lines
19 KiB
Java

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<String, DebitorStatistikData> stats = null;
List<ItemCollection> 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<String> currencies = aglConfigController.getWorkitem().getItemValueList("currency.out", String.class);
if (currencies.size() < 2) {
currencies.add(currencies.get(0));
}
String label = "<span class=\"pull-right\">" + formatCurrency(totalCurrency1) + " " + currencies.get(0)
+ "</span>";
label = label + "</br><span class=\"pull-right\">" +
formatCurrency(totalCurrency2) + " " + currencies.get(1) + "</span>";
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<String, DebitorStatistikData> 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<String, DebitorStatistikData> 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<String, DebitorStatistikData> loadStats() {
if (stats != null) {
return stats;
}
logger.info(" ├──load stats for " + getDbtNr() + "....");
// recompute
stats = new TreeMap<String, DebitorStatistikData>();
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<ItemCollection> 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<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;
}
/**
* 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() {
loadStats();
logger.info(" ├──build chart for " + getDbtNr() + "....");
// 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 = "{\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<String, DebitorStatistikData> 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<String, DebitorStatistikData> 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<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
*
* 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);
}
}