diff --git a/office-alexander-logistics-app/src/main/java/org/imixs/workflow/office/dataview/DataViewController.java b/office-alexander-logistics-app/src/main/java/org/imixs/workflow/office/dataview/DataViewController.java index a0b44fa..14df9d3 100644 --- a/office-alexander-logistics-app/src/main/java/org/imixs/workflow/office/dataview/DataViewController.java +++ b/office-alexander-logistics-app/src/main/java/org/imixs/workflow/office/dataview/DataViewController.java @@ -23,22 +23,30 @@ *******************************************************************************/ package org.imixs.workflow.office.dataview; +import java.io.IOException; import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; +import org.imixs.archive.core.SnapshotService; +import org.imixs.workflow.FileData; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.engine.DocumentService; +import org.imixs.workflow.engine.WorkflowService; import org.imixs.workflow.exceptions.ModelException; +import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.QueryException; import org.imixs.workflow.faces.data.ViewController; import org.imixs.workflow.faces.data.ViewHandler; import org.imixs.workflow.office.forms.CustomFormController; import org.imixs.workflow.office.forms.CustomFormSection; +import com.alexanderlogistics.mahnlauf.InkassoExportAdapter; + import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.Conversation; import jakarta.enterprise.context.ConversationScoped; @@ -69,6 +77,10 @@ import jakarta.servlet.http.HttpServletRequest; public class DataViewController extends ViewController { private static final long serialVersionUID = 1L; + + public static final String ERROR_CONFIG = "CONFIG_ERROR"; + public static final int MAX_ROWS = 3000; + private List sections = null; private List viewItemDefinitions = null; protected ItemCollection dataDefinition = null; @@ -85,6 +97,12 @@ public class DataViewController extends ViewController { @Inject private DocumentService documentService; + @Inject + private WorkflowService workflowService; + + @Inject + SnapshotService snapshotService; + @Inject CustomFormController customFormController; @@ -111,9 +129,7 @@ public class DataViewController extends ViewController { * This method loads the custom form sections */ public void onLoad() { - String cacheid = null; - logger.info("> onload..."); // Important: start a new conversation beause of the usage of the // CustomFormController! @@ -134,7 +150,7 @@ public class DataViewController extends ViewController { dataDefinition = documentService.load(uniqueid); } - logger.info("> cacheid=" + cacheid); + logger.finest("> cacheid=" + cacheid); if (cacheid != null && !cacheid.isEmpty()) { filter = dataViewCache.get(cacheid); } else { @@ -145,13 +161,21 @@ public class DataViewController extends ViewController { // Init new Filter.... if (dataDefinition != null) { filter.setItemValue("txtWorkflowEditorCustomForm", dataDefinition.getItemValue("form")); + filter.setItemValue("name", dataDefinition.getItemValueString("name")); + filter.setItemValue("description", dataDefinition.getItemValueString("description")); viewItemDefinitions = DataViewDefinitionController .computeDataViewItemDefinitions(dataDefinition); customFormController.computeFieldDefinition(filter); sections = customFormController.getSections(); - logger.info(" < cached page index=" + filter.getItemValueInteger("pageIndex")); + // Update View Handler settings + String sortBy = dataDefinition.getItemValueString("sort.by"); + if (sortBy.isEmpty()) { + sortBy = "$modified"; + } + this.setSortBy(sortBy); + this.setSortReverse(dataDefinition.getItemValueBoolean("sort.reverse")); this.setPageIndex(filter.getItemValueInteger("pageIndex")); if (!filter.getItemValueString("query").isEmpty()) { query = filter.getItemValueString("query"); @@ -229,7 +253,7 @@ public class DataViewController extends ViewController { // Replace all occurrences in the query case-insensitive. query = query.replaceAll("(?i)\\{" + Pattern.quote(itemName) + "\\}", itemValue); } - logger.info("query=" + query); + logger.finest("query=" + query); filter.setItemValue("query", query); // Prefetch data to update total count and page count @@ -246,29 +270,6 @@ public class DataViewController extends ViewController { return query; } - // /** - // * Returns the current workItem. If no workitem is defined the method - // * Instantiates a empty ItemCollection. - // * - // * @return - current workItem or null if not set - // */ - // public ItemCollection getData() { - // // do initialize an empty workItem here if null - // if (data == null) { - // reset(); - // } - // return data; - // } - - // /** - // * Set the current workItem - // * - // * @param workitem - new reference or null to clear the current workItem. - // */ - // public void setData(ItemCollection document) { - // this.data = document; - // } - /** * This method navigates back in the page index and caches the current page * index @@ -285,7 +286,6 @@ public class DataViewController extends ViewController { public void forward() { viewHandler.forward(this); filter.setItemValue("pageIndex", this.getPageIndex()); - logger.info("-> forward to page " + this.getPageIndex()); } /** @@ -297,9 +297,97 @@ public class DataViewController extends ViewController { ((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()) .getSession().getMaxInactiveInterval() * 1000); conversation.begin(); - logger.log(Level.INFO, "......start new conversation, id={0}", + logger.log(Level.FINEST, "......start new conversation, id={0}", conversation.getId()); + } } + /** + * Exports data into a excel template processed by apache-poi + * + * @throws PluginException + * @throws QueryException + */ + public String export() throws PluginException, QueryException { + + // build query and prepare dataset + run(); + + SimpleDateFormat dateformat = new SimpleDateFormat("yyyyMMddHHmm"); + String targetFileName = dataDefinition.getItemValueString("poi.targetFilename"); + if (targetFileName.isEmpty()) { + throw new PluginException(InkassoExportAdapter.class.getSimpleName(), ERROR_CONFIG, + "Missing Excel Export definition - check configuration!"); + } + logger.info("start export : " + targetFileName + "..."); + logger.fine(query); + + // load template + FileData fileData = loadTemplate(); + + if (fileData == null) { + // we did not found the template! + throw new PluginException(InkassoExportAdapter.class.getSimpleName(), ERROR_CONFIG, + "Missing Excel Export Template - check DataView definition!"); + } + targetFileName = targetFileName + "_" + dateformat.format(new Date()) + ".xlsx"; + try { + // test if query exceeds max count + int totalCount = documentService.count(query); + if (totalCount > MAX_ROWS) { + throw new PluginException(InkassoExportAdapter.class.getSimpleName(), ERROR_CONFIG, + "Data can not be exported into Excel because dataset exceeds " + MAX_ROWS + " rows!"); + } + String sortBy = dataDefinition.getItemValueString("sort.by"); + if (sortBy.isEmpty()) { + sortBy = "$modified"; // default + } + List invoices = documentService.find(query, MAX_ROWS, 0, sortBy, + dataDefinition.getItemValueBoolean("sort.reverse")); + if (invoices.size() > 0) { + String referenceCell = dataDefinition.getItemValueString("poi.referenceCell"); + DataViewPOIHelper.insertDataRows(invoices, referenceCell, viewItemDefinitions, fileData); + } + + // create a temp event + ItemCollection event = new ItemCollection().setItemValue("txtActivityResult", + dataDefinition.getItemValue("poi.update")); + ItemCollection poiConfig = workflowService.evalWorkflowResult(event, "poi-update", dataDefinition, + false); + DataViewPOIHelper.poiUpdate(filter, fileData, poiConfig, workflowService); + + fileData.setName(targetFileName); + + // See: + // https://stackoverflow.com/questions/9391838/how-to-provide-a-file-download-from-a-jsf-backing-bean + DataViewPOIHelper.downloadExcelFile(fileData); + } catch (IOException | QueryException e) { + throw new PluginException(InkassoExportAdapter.class.getSimpleName(), ERROR_CONFIG, + "Failed to generate Excel Export: " + e.getMessage()); + } + + // return "/pages/admin/excel_export_rechnungsausgang.jsf?faces-redirect=true"; + return ""; + } + + /** + * This method returns the first excel poi template from the Data Definition + * + * @param name in attribute txtname + * + * + */ + private FileData loadTemplate() { + + // first filename + List fileDataList = dataDefinition.getFileData(); + if (fileDataList != null && fileDataList.size() > 0) { + String fileName = fileDataList.get(0).getName(); + return snapshotService.getWorkItemFile(dataDefinition.getUniqueID(), fileName); + } + // no file data available! + return null; + } + } diff --git a/office-alexander-logistics-app/src/main/java/org/imixs/workflow/office/dataview/DataViewPOIHelper.java b/office-alexander-logistics-app/src/main/java/org/imixs/workflow/office/dataview/DataViewPOIHelper.java new file mode 100644 index 0000000..ad3c09b --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/org/imixs/workflow/office/dataview/DataViewPOIHelper.java @@ -0,0 +1,356 @@ +/******************************************************************************* + * Imixs Workflow Technology + * Copyright (C) 2003, 2008 Imixs Software Solutions GmbH, + * http://www.imixs.com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You can receive a copy of the GNU General Public + * License at http://www.gnu.org/licenses/gpl.html + * + * Contributors: + * Imixs Software Solutions GmbH - initial API and implementation + * Ralph Soika + * + *******************************************************************************/ +package org.imixs.workflow.office.dataview; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.poi.ss.usermodel.CellCopyPolicy; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.util.CellReference; +import org.apache.poi.xssf.usermodel.XSSFCell; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.imixs.workflow.FileData; +import org.imixs.workflow.ItemCollection; +import org.imixs.workflow.engine.WorkflowService; +import org.imixs.workflow.exceptions.PluginException; +import org.imixs.workflow.util.XMLParser; + +import jakarta.faces.context.ExternalContext; +import jakarta.faces.context.FacesContext; + +/** + * The DataViewPOIHelper provides methods to update a excel template with the + * help of the Apache POI framework. + * + * + * @author rsoika + * @version 1.0 + */ + +public class DataViewPOIHelper { + private static Logger logger = Logger.getLogger(DataViewPOIHelper.class.getName()); + public static final String ERROR_CONFIG = "CONFIG_ERROR"; + + /** + * Helper method to initialize a file download + * + * @throws IOException + */ + public static void downloadExcelFile(FileData fileData) throws IOException { + + FacesContext facesContext = FacesContext.getCurrentInstance(); + ExternalContext externalContext = facesContext.getExternalContext(); + + externalContext.responseReset(); + externalContext.setResponseContentType("application/vnd.ms-excel"); + externalContext.setResponseContentLength(fileData.getContent().length); + externalContext.setResponseHeader("Content-Disposition", "attachment; filename=\"" + fileData.getName() + "\""); + + OutputStream output = externalContext.getResponseOutputStream(); + + // Now you can write the InputStream of the file to the above OutputStream the + // usual way. + output.write(fileData.getContent()); + + facesContext.responseComplete(); // Important! Otherwise Faces will attempt to render the response which + // obviously will fail since it's already written with a file and closed. + } + + /** + * This helper method inserts a row for each invoice + * + * @throws PluginException + */ + public static void insertDataRows(List dataset, String referenceCell, + List viewItemDefinitions, + FileData fileData) throws PluginException { + // load XSSFWorkbook + try (InputStream imputStream = new ByteArrayInputStream(fileData.getContent())) { + XSSFWorkbook doc = new XSSFWorkbook(imputStream); + // NOTE: we only take the first sheet ! + XSSFSheet sheet = doc.getSheetAt(0); + + CellReference cr = new CellReference(referenceCell); + XSSFRow referenceRow = sheet.getRow(cr.getRow()); + int referenceRowPos = referenceRow.getRowNum() + 1; + int rowPos = referenceRowPos; + // int lastRow = sheet.getLastRowNum(); + int lastRow = 999; + logger.finest("Last rownum=" + lastRow); + sheet.shiftRows(rowPos, lastRow, dataset.size(), true, true); + + for (ItemCollection workitem : dataset) { + logger.finest("......copy row..."); + + // now create a new line.. + XSSFRow row = sheet.createRow(rowPos); + row.copyRowFrom(referenceRow, new CellCopyPolicy()); + // insert values + int cellNum = 0; + for (ItemCollection itemDef : viewItemDefinitions) { + String type = itemDef.getItemValueString("item.type"); + String name = itemDef.getItemValueString("item.name"); + switch (type) { + case "xs:double": + row.getCell(cellNum).setCellValue(workitem.getItemValueDouble(name)); + break; + case "xs:float": + row.getCell(cellNum).setCellValue(workitem.getItemValueFloat(name)); + break; + case "xs:int": + row.getCell(cellNum).setCellValue(workitem.getItemValueInteger(name)); + break; + case "xs:date": + row.getCell(cellNum).setCellValue(workitem.getItemValueDate(name)); + break; + + default: + row.getCell(cellNum).setCellValue(workitem.getItemValueString(name)); + } + cellNum++; + } + + rowPos++; + } + // delete reference row + sheet.shiftRows(referenceRowPos, lastRow + dataset.size(), -1, true, true); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + // write back the file + doc.write(byteArrayOutputStream); + doc.close(); + byte[] newContent = byteArrayOutputStream.toByteArray(); + fileData.setContent(newContent); + + } catch (IOException e) { + throw new PluginException(DataViewPOIHelper.class.getSimpleName(), ERROR_CONFIG, + "failed to update excel export: " + e.getMessage()); + } + } + + /** + * This helper method applies the POI update defintiions a row for each invoice + * + * @throws PluginException + */ + public static void poiUpdate(ItemCollection dataDefinition, FileData fileData, + ItemCollection poiConfig, WorkflowService workflowService) throws PluginException { + + // update $modified for Now function + dataDefinition.setItemValue("$modified", new Date()); + + if (poiConfig == null || !poiConfig.hasItem("findreplace")) { + // no config found + return; + } + List replaceDevList = poiConfig.getItemValue("findreplace"); + String eval = poiConfig.getItemValueString("eval"); + + // load XSSFWorkbook + try (InputStream imputStream = new ByteArrayInputStream(fileData.getContent())) { + XSSFWorkbook workbook = new XSSFWorkbook(imputStream); + // NOTE: we only take the first sheet ! + XSSFSheet sheet = workbook.getSheetAt(0); + + updateXSSFWorkbook(workbook, dataDefinition, replaceDevList, workflowService); + + // Update Eval list + if (eval != null && !eval.isEmpty()) { + // iterate over all cells to be evaluated + String[] cellPositions = eval.split(";"); + for (String cellPos : cellPositions) { + evalXSSFSheet(workbook, sheet, cellPos); + } + logger.fine("formula evaluation completed"); + } + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + // write back the file + workbook.write(byteArrayOutputStream); + workbook.close(); + byte[] newContent = byteArrayOutputStream.toByteArray(); + fileData.setContent(newContent); + + } catch (IOException e) { + throw new PluginException(DataViewPOIHelper.class.getSimpleName(), ERROR_CONFIG, + "failed to update excel export: " + e.getMessage()); + } + } + + /** + * Returns a Cell by name or an optional absolute cell postion + *

+ * Examples for refs are 'A1', 'AB3', 'MyCell' where 'MyCell' is a named cell. + *

+ * + */ + public static XSSFCell getCellByRef(XSSFWorkbook doc, XSSFSheet sheet, String cellReference) { + XSSFCell cell = null; + + // first we test if the cellName is a named cell + Name aNamedCell = doc.getName(cellReference); + if (aNamedCell != null) { + // yes its a named cell so we need to get the referrer Formula + logger.finest("...resolving named cell = " + aNamedCell.getNameName()); + cellReference = aNamedCell.getRefersToFormula(); + // now we can find the cell by its ref + } + + CellReference cr = new CellReference(cellReference); + XSSFRow row = sheet.getRow(cr.getRow()); + if (row == null) { + logger.severe("Unable to resolve cell ref '" + cellReference + "'!"); + return null; + } + cell = row.getCell(cr.getCol()); + return cell; + } + + /** + * This method updates the XSSFWorkbook document. The method can be overwritten + * by subclasses to add additional logic + * + * @param workbook + * @param workitem + * @param replaceDevList + * @throws PluginException + */ + public static void updateXSSFWorkbook(XSSFWorkbook workbook, ItemCollection workitem, List replaceDevList, + WorkflowService workflowService) + throws PluginException { + + logger.fine("XSSFWorkbook loaded"); + // NOTE: we only take the first sheet ! + XSSFSheet sheet = workbook.getSheetAt(0); + + for (String entityDev : replaceDevList) { + ItemCollection entityData = XMLParser.parseItemStructure(entityDev); + + if (entityData != null) { + String find = entityData.getItemValueString("find"); + String replace = entityData.getItemValueString("replace"); + replace = workflowService.adaptText(replace, workitem); + // optional itename + String itemname = entityData.getItemValueString("itemname"); + + // replace with item value? + if (!itemname.isEmpty()) { + List valueList = workitem.getItemValue(itemname); + if (valueList.size() > 0) { + // provide the first value only + replaceXSSFSheetItemValue(workbook, sheet, find, valueList.get(0)); + } + } else { + replaceXSSFSheetStringValue(workbook, sheet, find, replace); + } + + } + } + } + + /** + * Helper method replaces a given cell of a XSSFSheet with a typed item value + * + * @throws PluginException + */ + public static void replaceXSSFSheetItemValue(XSSFWorkbook doc, XSSFSheet sheet, String find, Object itemValue) + throws PluginException { + logger.finest("update cell " + find); + XSSFCell cell = getCellByRef(doc, sheet, find); + if (cell == null) { + logger.warning("Cell " + find + " not found."); + return; + } + if (itemValue instanceof Date) { + cell.setCellValue((Date) itemValue); + } else if (itemValue instanceof Double) { + cell.setCellValue((Double) itemValue); + } else { + // default to text + cell.setCellValue(itemValue.toString()); + } + } + + /** + * Helper method replaces a given cell of a XSSFSheet with a string value + * + * @throws PluginException + */ + private static void replaceXSSFSheetStringValue(XSSFWorkbook doc, XSSFSheet sheet, String find, String replace) + throws PluginException { + logger.finest("update cell " + find); + XSSFCell cell = getCellByRef(doc, sheet, find); + if (cell == null) { + logger.warning("Cell " + find + " not found."); + return; + } + try { + // we try to set first as float value if possible + float f = Float.parseFloat(replace); + cell.setCellValue(f); + } catch (NumberFormatException e) { + // set value as string + cell.setCellValue(replace); + } + } + + /** + * Evaluates a given list of cells in a given XSWorkbook + * + * @param doc + * @param sheet + * @param cell + * @throws PluginException + */ + public static void evalXSSFSheet(XSSFWorkbook doc, XSSFSheet sheet, String cell) throws PluginException { + FormulaEvaluator evaluator = doc.getCreationHelper().createFormulaEvaluator(); + XSSFCell c = getCellByRef(doc, sheet, cell); + if (c == null) { + logger.warning("Cell " + cell + " not found."); + return; + } + if (c.getCellType() == CellType.FORMULA) { + logger.finest("...eval cell " + cell); + try { + CellType evalResult = evaluator.evaluateFormulaCell(c); + if (evalResult == CellType.ERROR) { + logger.warning("...unable to evaluate cell " + cell); + } + } catch (Exception poie) { + logger.warning("...failed to evaluate cell " + cell + " : " + poie.getMessage()); + } + } + } +} diff --git a/office-alexander-logistics-app/src/main/webapp/pages/admin/dataViewDefinition.xhtml b/office-alexander-logistics-app/src/main/webapp/pages/admin/dataViewDefinition.xhtml index 8e003be..71c0333 100644 --- a/office-alexander-logistics-app/src/main/webapp/pages/admin/dataViewDefinition.xhtml +++ b/office-alexander-logistics-app/src/main/webapp/pages/admin/dataViewDefinition.xhtml @@ -10,10 +10,12 @@ + + - +

@@ -55,11 +57,8 @@

- Name: * - - + Name: *
-
@@ -88,6 +87,28 @@
+
+
+
+ Sort By: +
+
+ +
+
+
+
+ Reverse Order: +
+
+ + + +
+
+
@@ -102,7 +123,8 @@ values. Each item of the attribute list can define an optional label, xs datatype and format.
- Convert - xml datatypes: xs:string (default), xs:decimal, xs:date, + Convert - xml datatypes: xs:string (default), xs:int, xs:double, xs:float, + xs:date, xs:dateTime, xs:anyURI @@ -212,7 +234,7 @@
-
+

Template Test View

-
-
-
- POI Update: -
-
- -
-
+

+ + The 'Target Name' defines the name of the file download. The 'Reference Cell + marks the row to insert the dataset. POI update Definitions are optional. -

+

+
+
+ Target Name: +
+
+ +
+
+
+
+ Reference Cell: +
+
+ +
+
+
+
+
+
+ POI Definition: +
+
+ +
+
+ + +
+
+ Source File : +
+
-
+ +
+ + +
@@ -277,6 +333,7 @@ } + /*]]>*/ diff --git a/office-alexander-logistics-app/src/main/webapp/pages/dataviews/data.xhtml b/office-alexander-logistics-app/src/main/webapp/pages/dataviews/data.xhtml index 7ede73d..be34bf4 100644 --- a/office-alexander-logistics-app/src/main/webapp/pages/dataviews/data.xhtml +++ b/office-alexander-logistics-app/src/main/webapp/pages/dataviews/data.xhtml @@ -9,14 +9,39 @@ + + + + - - - - +

@@ -50,14 +75,16 @@ -
+
- + + +
@@ -133,10 +160,17 @@ #{columnDef.item['item.label']} + pattern="#{message.datePatternShort}" /> + + + #{columnDef.item['item.label']} + + + @@ -146,30 +180,11 @@
+
- - - - - diff --git a/templates/ausgangsrechnungen-export_template.xlsx b/templates/dataviews/offene_rechnungen_template.xlsx similarity index 66% rename from templates/ausgangsrechnungen-export_template.xlsx rename to templates/dataviews/offene_rechnungen_template.xlsx index bb61fd3..e4c0c00 100644 Binary files a/templates/ausgangsrechnungen-export_template.xlsx and b/templates/dataviews/offene_rechnungen_template.xlsx differ