mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-19 21:50:45 +00:00
QQQ-42 checkpoint of qqq reports
This commit is contained in:
@ -22,7 +22,6 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
@ -67,7 +66,8 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
|
||||
private Workbook workbook;
|
||||
private Worksheet worksheet;
|
||||
private int row = 0;
|
||||
private int row = 0;
|
||||
private int sheetCount = 0;
|
||||
|
||||
|
||||
|
||||
@ -112,6 +112,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
table = exportInput.getTable();
|
||||
outputStream = this.exportInput.getReportOutputStream();
|
||||
this.row = 0;
|
||||
this.sheetCount++;
|
||||
|
||||
if(workbook == null)
|
||||
{
|
||||
@ -127,11 +128,11 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
worksheet.finish();
|
||||
}
|
||||
|
||||
worksheet = workbook.newWorksheet(label);
|
||||
worksheet = workbook.newWorksheet(Objects.requireNonNullElse(label, "Sheet " + sheetCount));
|
||||
|
||||
writeReportHeaderRow();
|
||||
}
|
||||
catch(IOException e)
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error starting worksheet", e));
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
@ -154,17 +155,17 @@ public class FormulaInterpreter
|
||||
case "ADD":
|
||||
{
|
||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).add(numbers.get(1)));
|
||||
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).add(numbers.get(1)));
|
||||
}
|
||||
case "MINUS":
|
||||
{
|
||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).subtract(numbers.get(1)));
|
||||
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).subtract(numbers.get(1)));
|
||||
}
|
||||
case "MULTIPLY":
|
||||
{
|
||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).multiply(numbers.get(1)));
|
||||
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).multiply(numbers.get(1)));
|
||||
}
|
||||
case "DIVIDE":
|
||||
{
|
||||
@ -173,7 +174,7 @@ public class FormulaInterpreter
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).divide(numbers.get(1), 4, RoundingMode.HALF_UP));
|
||||
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).divide(numbers.get(1), 4, RoundingMode.HALF_UP));
|
||||
}
|
||||
case "DIVIDE_SCALE":
|
||||
{
|
||||
@ -182,23 +183,83 @@ public class FormulaInterpreter
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).divide(numbers.get(1), numbers.get(2).intValue(), RoundingMode.HALF_UP));
|
||||
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).divide(numbers.get(1), numbers.get(2).intValue(), RoundingMode.HALF_UP));
|
||||
}
|
||||
case "ROUND":
|
||||
{
|
||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).round(new MathContext(numbers.get(1).intValue())));
|
||||
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).round(new MathContext(numbers.get(1).intValue())));
|
||||
}
|
||||
case "SCALE":
|
||||
{
|
||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP));
|
||||
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP));
|
||||
}
|
||||
case "NVL":
|
||||
{
|
||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||
return Objects.requireNonNullElse(numbers.get(0), numbers.get(1));
|
||||
}
|
||||
case "IF":
|
||||
{
|
||||
// IF(CONDITION,TRUE,ELSE)
|
||||
List<Serializable> actualArgs = getArgumentList(args, 3, variableInterpreter);
|
||||
Serializable condition = actualArgs.get(0);
|
||||
boolean conditionBoolean;
|
||||
if(condition == null)
|
||||
{
|
||||
conditionBoolean = false;
|
||||
}
|
||||
else if(condition instanceof Boolean b)
|
||||
{
|
||||
conditionBoolean = b;
|
||||
}
|
||||
else if(condition instanceof BigDecimal bd)
|
||||
{
|
||||
conditionBoolean = (bd.compareTo(BigDecimal.ZERO) != 0);
|
||||
}
|
||||
else if(condition instanceof String s)
|
||||
{
|
||||
if("true".equalsIgnoreCase(s))
|
||||
{
|
||||
conditionBoolean = true;
|
||||
}
|
||||
else if("false".equalsIgnoreCase(s))
|
||||
{
|
||||
conditionBoolean = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
conditionBoolean = StringUtils.hasContent(s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
conditionBoolean = false;
|
||||
}
|
||||
|
||||
return conditionBoolean ? actualArgs.get(1) : actualArgs.get(2);
|
||||
}
|
||||
case "LT":
|
||||
{
|
||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) < 0);
|
||||
}
|
||||
case "LTE":
|
||||
{
|
||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) <= 0);
|
||||
}
|
||||
case "GT":
|
||||
{
|
||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) > 0);
|
||||
}
|
||||
case "GTE":
|
||||
{
|
||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) >= 0);
|
||||
}
|
||||
default:
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -235,7 +296,21 @@ public class FormulaInterpreter
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Serializable nullIfAnyNullArgsElse(List<BigDecimal> numbers, Supplier<BigDecimal> supplier)
|
||||
private static Serializable nullIfAnyNullArgsElseBigDecimal(List<BigDecimal> numbers, Supplier<BigDecimal> supplier)
|
||||
{
|
||||
if(numbers.stream().anyMatch(Objects::isNull))
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
return supplier.get();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Serializable nullIfAnyNullArgsElseBoolean(List<BigDecimal> numbers, Supplier<Boolean> supplier)
|
||||
{
|
||||
if(numbers.stream().anyMatch(Objects::isNull))
|
||||
{
|
||||
@ -275,4 +350,35 @@ public class FormulaInterpreter
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static List<Serializable> getArgumentList(List<Serializable> originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
|
||||
{
|
||||
if(howMany != null)
|
||||
{
|
||||
if(!howMany.equals(originalArgs.size()))
|
||||
{
|
||||
throw (new QFormulaException("Wrong number of arguments (required: " + howMany + ", received: " + originalArgs.size() + ")"));
|
||||
}
|
||||
}
|
||||
|
||||
List<Serializable> rs = new ArrayList<>();
|
||||
for(Serializable originalArg : originalArgs)
|
||||
{
|
||||
try
|
||||
{
|
||||
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null);
|
||||
rs.add(interpretedArg);
|
||||
}
|
||||
catch(QValueException e)
|
||||
{
|
||||
throw (new QFormulaException("Could not process [" + originalArg + "] as a number"));
|
||||
}
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -469,6 +469,9 @@ public class GenerateReportAction
|
||||
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
|
||||
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(fieldAggregates));
|
||||
|
||||
HashMap<String, Serializable> thisRowValues = new HashMap<>();
|
||||
variableInterpreter.addValueMap("thisRow", thisRowValues);
|
||||
|
||||
if(!variancePivotAggregates.isEmpty())
|
||||
{
|
||||
Map<PivotKey, Map<String, AggregatesInterface<?>>> varianceMap = variancePivotAggregates.getOrDefault(view.getName(), Collections.emptyMap());
|
||||
@ -503,6 +506,7 @@ public class GenerateReportAction
|
||||
{
|
||||
Serializable serializable = getValueForColumn(variableInterpreter, column);
|
||||
pivotRow.setValue(column.getName(), serializable);
|
||||
thisRowValues.put(column.getName(), serializable);
|
||||
}
|
||||
}
|
||||
|
||||
@ -583,14 +587,17 @@ public class GenerateReportAction
|
||||
*******************************************************************************/
|
||||
private Serializable getValueForColumn(QMetaDataVariableInterpreter variableInterpreter, QReportField column) throws QFormulaException
|
||||
{
|
||||
String formula = column.getFormula();
|
||||
Serializable serializable = variableInterpreter.interpretForObject(formula);
|
||||
String formula = column.getFormula();
|
||||
Serializable result;
|
||||
if(formula.startsWith("=") && formula.length() > 1)
|
||||
{
|
||||
// serializable = interpretFormula(variableInterpreter, formula);
|
||||
serializable = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1));
|
||||
result = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1));
|
||||
}
|
||||
return serializable;
|
||||
else
|
||||
{
|
||||
result = variableInterpreter.interpretForObject(formula, null);
|
||||
}
|
||||
return (result);
|
||||
}
|
||||
|
||||
|
||||
|
@ -558,17 +558,17 @@ public class QInstanceEnricher
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// create an identity section for the id and any fields in the record label //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
QAppSection defaultSection = new QAppSection(app.getName(), app.getLabel(), new QIcon("badge"), new ArrayList<>(), new ArrayList<>());
|
||||
QAppSection defaultSection = new QAppSection(app.getName(), app.getLabel(), new QIcon("badge"), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
|
||||
|
||||
boolean foundNonAppChild = false;
|
||||
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
|
||||
{
|
||||
for(QAppChildMetaData child : app.getChildren())
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// only tables and processes are allowed to be in sections at this time, apps //
|
||||
// might be children but not in sections so keep track if we find any non-app //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// only tables, processes, and reports are allowed to be in sections at this time, apps //
|
||||
// might be children but not in sections so keep track if we find any non-app //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(child.getClass().equals(QTableMetaData.class))
|
||||
{
|
||||
defaultSection.getTables().add(child.getName());
|
||||
@ -579,6 +579,11 @@ public class QInstanceEnricher
|
||||
defaultSection.getProcesses().add(child.getName());
|
||||
foundNonAppChild = true;
|
||||
}
|
||||
else if(child.getClass().equals(QReportMetaData.class))
|
||||
{
|
||||
defaultSection.getReports().add(child.getName());
|
||||
foundNonAppChild = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -510,7 +510,8 @@ public class QInstanceValidator
|
||||
assertCondition(StringUtils.hasContent(section.getLabel()), "Missing a label for a section in app " + app.getLabel() + ".");
|
||||
boolean hasTables = CollectionUtils.nullSafeHasContents(section.getTables());
|
||||
boolean hasProcesses = CollectionUtils.nullSafeHasContents(section.getProcesses());
|
||||
if(assertCondition(hasTables || hasProcesses, "App " + app.getName() + " section " + section.getName() + " does not have any children."))
|
||||
boolean hasReports = CollectionUtils.nullSafeHasContents(section.getReports());
|
||||
if(assertCondition(hasTables || hasProcesses || hasReports, "App " + app.getName() + " section " + section.getName() + " does not have any children."))
|
||||
{
|
||||
if(hasTables)
|
||||
{
|
||||
@ -532,6 +533,16 @@ public class QInstanceValidator
|
||||
childNamesInSections.add(processName);
|
||||
}
|
||||
}
|
||||
if(hasReports)
|
||||
{
|
||||
for(String reportName : section.getReports())
|
||||
{
|
||||
assertCondition(app.getChildren().stream().anyMatch(c -> c.getName().equals(reportName)), "App " + app.getName() + " section " + section.getName() + " specifies report " + reportName + ", which is not a child of this app.");
|
||||
assertCondition(!childNamesInSections.contains(reportName), "App " + app.getName() + " has report " + reportName + " listed more than once in its sections.");
|
||||
|
||||
childNamesInSections.add(reportName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,7 @@ public class QAppSection
|
||||
|
||||
private List<String> tables;
|
||||
private List<String> processes;
|
||||
private List<String> reports;
|
||||
|
||||
|
||||
|
||||
@ -51,13 +52,14 @@ public class QAppSection
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QAppSection(String name, String label, QIcon icon, List<String> tables, List<String> processes)
|
||||
public QAppSection(String name, String label, QIcon icon, List<String> tables, List<String> processes, List<String> reports)
|
||||
{
|
||||
this.name = name;
|
||||
this.label = label;
|
||||
this.icon = icon;
|
||||
this.tables = tables;
|
||||
this.processes = processes;
|
||||
this.reports = reports;
|
||||
}
|
||||
|
||||
|
||||
@ -198,6 +200,40 @@ public class QAppSection
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reports
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<String> getReports()
|
||||
{
|
||||
return reports;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reports
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setReports(List<String> reports)
|
||||
{
|
||||
this.reports = reports;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for reports
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QAppSection withReports(List<String> reports)
|
||||
{
|
||||
this.reports = reports;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for icon
|
||||
**
|
||||
|
@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -50,8 +51,9 @@ public class ExecuteReportStep implements BackendStep
|
||||
{
|
||||
try
|
||||
{
|
||||
String reportName = runBackendStepInput.getValueString("reportName");
|
||||
File tmpFile = File.createTempFile(reportName, ".xlsx", new File("/tmp/"));
|
||||
String reportName = runBackendStepInput.getValueString("reportName");
|
||||
QReportMetaData report = runBackendStepInput.getInstance().getReport(reportName);
|
||||
File tmpFile = File.createTempFile(reportName, ".xlsx", new File("/tmp/"));
|
||||
|
||||
runBackendStepInput.getAsyncJobCallback().updateStatus("Generating Report");
|
||||
|
||||
@ -71,7 +73,7 @@ public class ExecuteReportStep implements BackendStep
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmm").withZone(ZoneId.systemDefault());
|
||||
String datePart = formatter.format(Instant.now());
|
||||
|
||||
runBackendStepOutput.addValue("downloadFileName", reportName + "-" + datePart + ".xlsx");
|
||||
runBackendStepOutput.addValue("downloadFileName", report.getLabel() + " " + datePart + ".xlsx");
|
||||
runBackendStepOutput.addValue("serverFilePath", tmpFile.getCanonicalPath());
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user