diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java index 97ef610..98ee6da 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java @@ -7,7 +7,6 @@ import java.io.IOException; import java.io.InputStream; import java.text.NumberFormat; import java.text.SimpleDateFormat; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -21,7 +20,6 @@ import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; -import org.imixs.archive.core.SnapshotService; import org.imixs.einvoice.EInvoiceFormatException; import org.imixs.einvoice.EInvoiceModel; import org.imixs.einvoice.EInvoiceModelFactory; @@ -34,11 +32,11 @@ import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.engine.WorkflowService; import org.imixs.workflow.exceptions.AdapterException; import org.imixs.workflow.exceptions.PluginException; -import org.imixs.workflow.exceptions.QueryException; import org.imixs.workflow.util.XMLParser; import com.alexanderlogistics.BusinessPartnerService; import com.alexanderlogistics.InvoiceUtil; +import com.alexanderlogistics.ksef.api.KSeFAPIService; import jakarta.inject.Inject; @@ -80,9 +78,6 @@ import jakarta.inject.Inject; */ public class EInvoiceAdapter implements SignalAdapter { - final String TYPE_TEXTBLOCK = "textblock"; - public static final String DOCUMENT_ERROR = "DOCUMENT_ERROR"; - public static final String CONFIG_ERROR = "CONFIG_ERROR"; public static final String LINE_ITEMS_PROPERTY = "invoice.items"; private static Logger logger = Logger.getLogger(EInvoiceAdapter.class.getName()); @@ -97,10 +92,10 @@ public class EInvoiceAdapter implements SignalAdapter { DocumentService documentService; @Inject - SnapshotService snapshotService; + BusinessPartnerService businessPartnerService; @Inject - BusinessPartnerService businessPartnerService; + KSeFAPIService kSeFAPIService; /** * This method @@ -117,14 +112,14 @@ public class EInvoiceAdapter implements SignalAdapter { workitem, false); if (eInvoiceConfig == null || !eInvoiceConfig.hasItem("create")) { - throw new PluginException(EInvoiceAdapter.class.getSimpleName(), CONFIG_ERROR, + throw new PluginException(EInvoiceAdapter.class.getSimpleName(), KSeFAPIService.CONFIG_ERROR, "missing e-invoice configuration in model event - please check model configuration"); } ItemCollection createDefinition = XMLParser.parseItemStructure(eInvoiceConfig.getItemValueString("CREATE")); try { // Load the e-invoice template.... - FileData xmlFileData = loadXMLTemplate(workitem, createDefinition); - FileData pdfFileData = loadPDF(workitem); + FileData xmlFileData = kSeFAPIService.loadXMLTemplate(workitem, createDefinition); + FileData pdfFileData = kSeFAPIService.loadPDF(workitem); logger.info("│ ├── Source File: " + pdfFileData.getName()); @@ -148,47 +143,50 @@ public class EInvoiceAdapter implements SignalAdapter { return workitem; } - /** - * This helper method tries to find the pdf file matching the item - * 'cargosoft.import.filename'. If not found the method returns the first .pdf - * file form the list of attachments. The method returns null if no pdf file - * exits - * - * @param workitem - * @return the pdf FileData object - * @throws PluginException if not pdf file was found - */ - private FileData loadPDF(ItemCollection workitem) throws PluginException { - FileData pdfFileData = null; - String cargosoftFileName = workitem.getItemValueString("cargosoft.import.filename"); - if (!cargosoftFileName.isEmpty()) { - cargosoftFileName = cargosoftFileName.replace(".xml", ".pdf"); + // /** + // * This helper method tries to find the pdf file matching the item + // * 'cargosoft.import.filename'. If not found the method returns the first .pdf + // * file form the list of attachments. The method returns null if no pdf file + // * exits + // * + // * @param workitem + // * @return the pdf FileData object + // * @throws PluginException if not pdf file was found + // */ + // private FileData loadPDF(ItemCollection workitem) throws PluginException { + // FileData pdfFileData = null; + // String cargosoftFileName = + // workitem.getItemValueString("cargosoft.import.filename"); + // if (!cargosoftFileName.isEmpty()) { + // cargosoftFileName = cargosoftFileName.replace(".xml", ".pdf"); - // try to load filedata.... - pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), cargosoftFileName); + // // try to load filedata.... + // pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), + // cargosoftFileName); - } + // } - if (pdfFileData == null) { - // take first pdf file available - List fileNames = workitem.getFileNames(); - for (String filename : fileNames) { + // if (pdfFileData == null) { + // // take first pdf file available + // List fileNames = workitem.getFileNames(); + // for (String filename : fileNames) { - if (filename.toLowerCase().endsWith(".pdf")) { - pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), filename); - break; - } - } - } + // if (filename.toLowerCase().endsWith(".pdf")) { + // pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), + // filename); + // break; + // } + // } + // } - if (pdfFileData == null) { - throw new PluginException(EInvoiceAdapter.class.getSimpleName(), - DOCUMENT_ERROR, - "DOCUMENT_ERROR: no pdf file found!"); - } - return pdfFileData; + // if (pdfFileData == null) { + // throw new PluginException(EInvoiceAdapter.class.getSimpleName(), + // DOCUMENT_ERROR, + // "DOCUMENT_ERROR: no pdf file found!"); + // } + // return pdfFileData; - } + // } private FileData embeddXML(FileData pdfFileData, FileData xmlFileData) throws PluginException { try { @@ -236,7 +234,7 @@ public class EInvoiceAdapter implements SignalAdapter { } catch (IOException e) { // TODO Auto-generated catch block - throw new PluginException(EInvoiceAdapter.class.getSimpleName(), CONFIG_ERROR, + throw new PluginException(EInvoiceAdapter.class.getSimpleName(), KSeFAPIService.CONFIG_ERROR, "Failed to generate e-invoice: " + e.getMessage()); } } @@ -298,7 +296,7 @@ public class EInvoiceAdapter implements SignalAdapter { fileDataXMLTemplate.setContent(model.getContent()); } catch (FileNotFoundException | EInvoiceFormatException | TransformerException e) { - throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, e.getMessage(), e); + throw new PluginException(this.getClass().getName(), KSeFAPIService.DOCUMENT_ERROR, e.getMessage(), e); } } @@ -312,12 +310,12 @@ public class EInvoiceAdapter implements SignalAdapter { */ public TradeParty buildAddress(String partnerID, String type) throws PluginException { if (partnerID == null || partnerID.isEmpty()) { - throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, "Missing partnerID"); + throw new PluginException(this.getClass().getName(), KSeFAPIService.DOCUMENT_ERROR, "Missing partnerID"); } ItemCollection businessPartner = businessPartnerService.getBusinessPartnerByID(partnerID); if (businessPartner == null) { - throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, + throw new PluginException(this.getClass().getName(), KSeFAPIService.DOCUMENT_ERROR, "Business Partner ID '" + partnerID + "' does not exist"); } TradeParty tradeParty = new TradeParty(type); @@ -352,80 +350,4 @@ public class EInvoiceAdapter implements SignalAdapter { return tradeLineItem; } - /** - * This method loads a text-block for a specified ref and appends the named - * fileData object of this document. - * - * @param document - * @throws PluginException - */ - - private FileData loadXMLTemplate(ItemCollection workitem, ItemCollection config) - throws PluginException { - - String textblock = config.getItemValueString("textblock"); - String template = config.getItemValueString("template"); - String sourceName = config.getItemValueString("source"); - - try { - debug = Boolean.parseBoolean(config.getItemValueString("debug")); - } catch (Exception e) { - } - String targetName = "factur-x.xml"; - - // adapt text.... - sourceName = workflowService.adaptText(sourceName, workitem); - - if ((template == null || template.isEmpty()) || (textblock == null || textblock.isEmpty())) { - throw new PluginException(EInvoiceAdapter.class.getSimpleName(), - CONFIG_ERROR, - "invalid e-invoice configuration in model event - textblock/template reference not defined!"); - } - - // load the text block - FileData fileData = loadTextBlockFileData(textblock, template); - - // do we found the document? - if (fileData == null) { - throw new PluginException(EInvoiceAdapter.class.getSimpleName(), - CONFIG_ERROR, - "invalid e-invoice configuration in model event - textblock/template: " + textblock + "/" + template - + " not found!"); - } - fileData.setName(targetName); - - return fileData; - } - - /** - * This method returns a text-block ItemCollection for a specified name. - * - * @param name in attribute txtname - * - * - */ - public FileData loadTextBlockFileData(String name, String fileName) { - ItemCollection textBlockItemCollection = null; - - // load text-block by name.... - String sQuery = "(type:\"" + TYPE_TEXTBLOCK + "\" AND txtname:\"" + name + "\")"; - Collection col; - try { - // find the textblock... - col = documentService.find(sQuery, 1, 0); - if (col.size() > 0) { - textBlockItemCollection = col.iterator().next(); - // fetch the fileData... - return snapshotService.getWorkItemFile(textBlockItemCollection.getUniqueID(), fileName); - - } else { - logger.warning("Missing text-block : '" + name + "'"); - } - } catch (QueryException e) { - logger.warning("getTextBlock - invalid query: " + e.getMessage()); - } - - return null; - } - } \ No newline at end of file diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/QRTest.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/QRTest.java new file mode 100644 index 0000000..17644fe --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/QRTest.java @@ -0,0 +1,51 @@ +package com.alexanderlogistics.einvoice; + +import org.imixs.archive.core.SnapshotService; +import org.imixs.workflow.ItemCollection; +import org.imixs.workflow.SignalAdapter; +import org.imixs.workflow.engine.DocumentService; +import org.imixs.workflow.engine.WorkflowService; +import org.imixs.workflow.exceptions.AdapterException; +import org.imixs.workflow.exceptions.PluginException; + +import com.alexanderlogistics.BusinessPartnerService; +import com.alexanderlogistics.ksef.api.KSeFAPIService; + +import jakarta.inject.Inject; + +/** + * + * @version 1.0 + * @author rsoika + */ +public class QRTest implements SignalAdapter { + + @Inject + WorkflowService workflowService; + + @Inject + DocumentService documentService; + + @Inject + SnapshotService snapshotService; + + @Inject + BusinessPartnerService businessPartnerService; + + @Inject + KSeFAPIService kSeFAPIService; + + /** + * This method + * + * @throws PluginException + */ + @Override + public ItemCollection execute(ItemCollection workitem, ItemCollection event) + throws AdapterException, PluginException { + + kSeFAPIService.embedQRCode(workitem); + return workitem; + } + +} \ No newline at end of file diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java index 17108e4..7a20008 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java @@ -12,6 +12,7 @@ import java.security.NoSuchAlgorithmException; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Base64; +import java.util.Collection; import java.util.List; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -23,11 +24,18 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; 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.PluginException; +import org.imixs.workflow.exceptions.QueryException; import com.google.zxing.BarcodeFormat; import com.google.zxing.WriterException; @@ -64,6 +72,9 @@ public class KSeFAPIService { private static Logger logger = Logger.getLogger(KSeFAPIService.class.getName()); public static final String ERROR_API = "API_ERROR"; + public static final String DOCUMENT_ERROR = "DOCUMENT_ERROR"; + public static final String CONFIG_ERROR = "CONFIG_ERROR"; + final String TYPE_TEXTBLOCK = "textblock"; @Inject KSeFAuthManager kseFAuthManager; @@ -71,6 +82,9 @@ public class KSeFAPIService { @Inject DocumentService documentService; + @Inject + WorkflowService workflowService; + @Inject SnapshotService snapshotService; @@ -249,6 +263,9 @@ public class KSeFAPIService { // store QR Code generateQRCode(workitem); + + // print QR Code to pdf + embedQRCode(workitem); } } catch (IOException | NoSuchAlgorithmException | InterruptedException e) { @@ -453,6 +470,88 @@ public class KSeFAPIService { } } + /** + * This helper method prints the QR Code on the AGL Invoice document + * + * @param workitem + * @throws PluginException + */ + public void embedQRCode(ItemCollection workitem) { + + try { + FileData pdfFileData = this.loadPDF(workitem); + + String ksefRefNumber = workitem.getItemValueString("ksef.referenceNumber"); + FileData qrCodeFileData = workitem.getFileData(ksefRefNumber + ".png"); + if (qrCodeFileData.getContent().length == 0) { + qrCodeFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), ksefRefNumber + ".png"); + } + + // now we have the QR Code (qrCodeFileData) and the PDF (pdfFileData) + PDDocument document = PDDocument.load(pdfFileData.getContent()); + + logger.info("pdf file loaded, embed qrcode..."); + // place thee PNG on the first page 10x10 cm from left upper corner + // .... + PDPage page = document.getPage(0); + PDImageXObject pdImage = PDImageXObject.createFromByteArray(document, qrCodeFileData.getContent(), + pdfFileData.getName()); + // PDPageContentStream contentStream = new PDPageContentStream(document, page); + + PDPageContentStream contentStream = new PDPageContentStream( + document, + page, + PDPageContentStream.AppendMode.APPEND, // Append statt Overwrite + true, // compress + true // resetContext - wichtig für konsistente Grafik-Zustände + ); + + // Mit Größenangabe (Breite und Höhe in Points): + // QR-Code zeichnen + // Seitenhöhe auslesen + float pageHeight = page.getMediaBox().getHeight(); + // Position von OBEN berechnen (z.B. 50 Points vom oberen Rand) + float marginFromTop = 230; + float imageWidth = 50; + float imageHeight = 50; + float imageX = 350; + float imageY = pageHeight - marginFromTop - imageHeight; // Von oben berechnet! + + contentStream.drawImage(pdImage, imageX, imageY, imageWidth, imageHeight); + + // Text darunter schreiben + String text = "KSeF-No.: " + workitem.getItemValueString("ksef.number"); + float fontSize = 7; + PDType1Font font = PDType1Font.COURIER; + float textX = imageX + 5; + float textY = imageY - 5; // 10 Points unter dem Bild + contentStream.beginText(); + contentStream.setFont(font, fontSize); + contentStream.newLineAtOffset(textX, textY); + contentStream.showText(text); + contentStream.endText(); + + contentStream.close(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + byte[] eInvoiceData = baos.toByteArray(); + document.close(); + baos.close(); + + pdfFileData.setContent(eInvoiceData); + + // finally update the pdf file + workitem.removeFile(pdfFileData.getName()); + // set new pdf file... + workitem.addFileData(pdfFileData); + + } catch (IOException | PluginException e) { + logger.warning("Failed to embed QR Code on PDF file: " + e.getMessage()); + } + + } + /** * Encrypt invoice XML with the session encryption keys * @@ -505,4 +604,117 @@ public class KSeFAPIService { return Base64.getEncoder().encodeToString(hashBytes); } + /** + * This helper method tries to find the pdf file matching the item + * 'cargosoft.import.filename'. If not found the method returns the first .pdf + * file form the list of attachments. The method returns null if no pdf file + * exits + * + * @param workitem + * @return the pdf FileData object + * @throws PluginException if not pdf file was found + */ + public FileData loadPDF(ItemCollection workitem) throws PluginException { + FileData pdfFileData = null; + String cargosoftFileName = workitem.getItemValueString("cargosoft.import.filename"); + if (!cargosoftFileName.isEmpty()) { + cargosoftFileName = cargosoftFileName.replace(".xml", ".pdf"); + + // try to load filedata.... + pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), cargosoftFileName); + + } + + if (pdfFileData == null) { + // take first pdf file available + List fileNames = workitem.getFileNames(); + for (String filename : fileNames) { + + if (filename.toLowerCase().endsWith(".pdf")) { + pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), filename); + break; + } + } + } + + if (pdfFileData == null) { + throw new PluginException(KSeFAPIService.class.getSimpleName(), + DOCUMENT_ERROR, + "DOCUMENT_ERROR: no pdf file found!"); + } + return pdfFileData; + + } + + /** + * This method returns a text-block ItemCollection for a specified name. + * + * @param name in attribute txtname + * + * + */ + public FileData loadTextBlockFileData(String name, String fileName) { + ItemCollection textBlockItemCollection = null; + + // load text-block by name.... + String sQuery = "(type:\"" + TYPE_TEXTBLOCK + "\" AND txtname:\"" + name + "\")"; + Collection col; + try { + // find the textblock... + col = documentService.find(sQuery, 1, 0); + if (col.size() > 0) { + textBlockItemCollection = col.iterator().next(); + // fetch the fileData... + return snapshotService.getWorkItemFile(textBlockItemCollection.getUniqueID(), fileName); + + } else { + logger.warning("Missing text-block : '" + name + "'"); + } + } catch (QueryException e) { + logger.warning("getTextBlock - invalid query: " + e.getMessage()); + } + + return null; + } + + /** + * This method loads a text-block for a specified ref and appends the named + * fileData object of this document. + * + * @param document + * @throws PluginException + */ + + public FileData loadXMLTemplate(ItemCollection workitem, ItemCollection config) + throws PluginException { + + String textblock = config.getItemValueString("textblock"); + String template = config.getItemValueString("template"); + String sourceName = config.getItemValueString("source"); + + String targetName = "factur-x.xml"; + + // adapt text.... + sourceName = workflowService.adaptText(sourceName, workitem); + + if ((template == null || template.isEmpty()) || (textblock == null || textblock.isEmpty())) { + throw new PluginException(KSeFAPIService.class.getSimpleName(), + KSeFAPIService.CONFIG_ERROR, + "invalid e-invoice configuration in model event - textblock/template reference not defined!"); + } + + // load the text block + FileData fileData = this.loadTextBlockFileData(textblock, template); + + // do we found the document? + if (fileData == null) { + throw new PluginException(KSeFAPIService.class.getSimpleName(), + KSeFAPIService.CONFIG_ERROR, + "invalid e-invoice configuration in model event - textblock/template: " + textblock + "/" + template + + " not found!"); + } + fileData.setName(targetName); + + return fileData; + } } diff --git a/workflow/pl/rechnungsausgang-pl-1.1.0.bpmn b/workflow/pl/rechnungsausgang-pl-1.1.0.bpmn index 77bdc0f..b584d4a 100644 --- a/workflow/pl/rechnungsausgang-pl-1.1.0.bpmn +++ b/workflow/pl/rechnungsausgang-pl-1.1.0.bpmn @@ -173,6 +173,7 @@ th { font-weight: bold;} ExclusiveGateway_3 dataObject_V0KVQg event_w96sOg + event_xxUDcA @@ -340,6 +341,19 @@ Bei Fragen wenden Sie sich bitte an info@imixs.com + + + + + + + + + + + + + ]]> @@ -496,6 +510,7 @@ Bei Fragen wenden Sie sich bitte an info@imixs.com $workflowgroup - $workflowstatus]]> SequenceFlow_27 sequenceFlow_EZX5kQ + sequenceFlow_seshBg SequenceFlow_18 SequenceFlow_34 sequenceFlow_zhcE7Q @@ -2428,7 +2443,7 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht - + sequenceFlow_bRxzKQ @@ -2571,6 +2586,79 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + sequenceFlow_seshBg + + + + @@ -2583,6 +2671,7 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht + @@ -2966,6 +3055,12 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht + + + + + + @@ -3338,6 +3433,12 @@ Wiedervorlage wird um 7 bzw. 5 Tage erhöht + + + + + +