Started e-invoice interface
This commit is contained in:
parent
1f2d3b4cd5
commit
1897377eb3
5 changed files with 3852 additions and 1 deletions
|
|
@ -1,6 +1,12 @@
|
|||
# Versionen
|
||||
|
||||
## 1.3.4 (Development)
|
||||
## 1.3.5 (Development)
|
||||
|
||||
- Verbesserter Cargosoft Export (Eingangsrechnungen)
|
||||
- Polen KSeF Schnittstelle
|
||||
- E-Rechnungs Adapter
|
||||
|
||||
## 1.3.4
|
||||
|
||||
- Finalisierung Business Partner Interface
|
||||
|- Neue Plugin Logik (Aktualisierung der BP Nummer und Aktivierung von BP Objekten falls diese archiviert waren)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,556 @@
|
|||
package com.alexanderlogistics.einvoice;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
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;
|
||||
import org.imixs.einvoice.TradeLineItem;
|
||||
import org.imixs.einvoice.TradeParty;
|
||||
import org.imixs.workflow.FileData;
|
||||
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 org.imixs.workflow.exceptions.QueryException;
|
||||
import org.imixs.workflow.util.XMLParser;
|
||||
|
||||
import com.alexanderlogistics.InvoiceUtil;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
/**
|
||||
* The EInvoiceConverterAdapter converts a Cargsoft outbound invoice into an
|
||||
* e-invoice and embeds the xml file into the pdf file to produce a Zugferd
|
||||
* Invoice.
|
||||
*
|
||||
* The adapter fetches the .pdf file matching the name of
|
||||
* 'cargosoft.import.filename'. If no such an item exits, the adapter takes the
|
||||
* first PDF file from the list of attachments.
|
||||
*
|
||||
* This adapter can only be run on Invoice data provided by Cargosoft.
|
||||
*
|
||||
* The adapter extracts a template XML from the text block, then inserts invoice
|
||||
* information into the XML file. In the end,
|
||||
* it inserts the XML file into the PDF.
|
||||
*
|
||||
* The adapter can be configured by the model:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
<e-invoice name="create">
|
||||
<textblock>textblock-ref</textblock>
|
||||
<template>filename</template>
|
||||
<debug>true</debug>
|
||||
</e-invoice>
|
||||
}
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* Wenn Debug = true gibt der Adapter alle einzelschritte auf der console aus
|
||||
* und erzeugt zusätzlich die factur-x xml und txt dateien.
|
||||
*
|
||||
*
|
||||
*
|
||||
* @version 1.0
|
||||
* @author rsoika
|
||||
*/
|
||||
public class EInvoiceConverterAdapter 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(EInvoiceConverterAdapter.class.getName());
|
||||
|
||||
public static SimpleDateFormat dateFormatter = new SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN);
|
||||
public static NumberFormat numberFormat = NumberFormat.getInstance(Locale.GERMANY);
|
||||
boolean debug = false;
|
||||
|
||||
@Inject
|
||||
WorkflowService workflowService;
|
||||
|
||||
@Inject
|
||||
DocumentService documentService;
|
||||
|
||||
@Inject
|
||||
SnapshotService snapshotService;
|
||||
|
||||
/**
|
||||
* This method
|
||||
*
|
||||
* @throws PluginException
|
||||
*/
|
||||
@Override
|
||||
public ItemCollection execute(ItemCollection workitem, ItemCollection event)
|
||||
throws AdapterException, PluginException {
|
||||
|
||||
logger.info("├── 🔜 Convert Invoice to E-Invoice...");
|
||||
// read configuration....
|
||||
ItemCollection eInvoiceConfig = workflowService.evalWorkflowResult(event, "e-invoice",
|
||||
workitem,
|
||||
false);
|
||||
if (eInvoiceConfig == null || !eInvoiceConfig.hasItem("create")) {
|
||||
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(), 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);
|
||||
|
||||
logger.info("│ ├── Source File: " + pdfFileData.getName());
|
||||
|
||||
} catch (PluginException e) {
|
||||
throw new AdapterException(e);
|
||||
// throw new AdapterException(
|
||||
// EInvoiceConverterAdapter.class.getSimpleName(), e.getErrorCode(),
|
||||
// e.getMessage());
|
||||
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// try to load filedata....
|
||||
pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), cargosoftFileName);
|
||||
|
||||
}
|
||||
|
||||
if (pdfFileData == null) {
|
||||
// take first pdf file available
|
||||
List<String> 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(EInvoiceConverterAdapter.class.getSimpleName(),
|
||||
DOCUMENT_ERROR,
|
||||
"DOCUMENT_ERROR: no pdf file found!");
|
||||
}
|
||||
return pdfFileData;
|
||||
|
||||
}
|
||||
|
||||
private FileData embeddXML(FileData pdfFileData, FileData xmlFileData) throws PluginException {
|
||||
try {
|
||||
PDDocument document = PDDocument.load(pdfFileData.getContent());
|
||||
byte[] xmlBytes = xmlFileData.getContent();
|
||||
|
||||
// Erstelle einen InputStream aus dem XML Content
|
||||
InputStream inputStream = new ByteArrayInputStream(xmlBytes);
|
||||
// Erstelle eine Datei-Spezifikation für die XML
|
||||
PDComplexFileSpecification fileSpec = new PDComplexFileSpecification();
|
||||
fileSpec.setFile(xmlFileData.getName());
|
||||
// Erstelle einen PDEmbeddedFile mit dem InputStream
|
||||
PDEmbeddedFile embeddedFile = new PDEmbeddedFile(document, inputStream);
|
||||
embeddedFile.setSubtype("application/xml");
|
||||
embeddedFile.setSize(xmlBytes.length);
|
||||
// Schließe den InputStream
|
||||
inputStream.close();
|
||||
// Setze den PDEmbeddedFile
|
||||
fileSpec.setEmbeddedFile(embeddedFile);
|
||||
|
||||
// Erstelle einen Map-Eintrag für die eingebettete Datei
|
||||
Map<String, PDComplexFileSpecification> filesMap = new HashMap<>();
|
||||
filesMap.put(xmlFileData.getName(), fileSpec);
|
||||
|
||||
// Erstelle einen Eingebettete-Dateien-Namensbaum und füge die Datei hinzu
|
||||
PDEmbeddedFilesNameTreeNode efTree = new PDEmbeddedFilesNameTreeNode();
|
||||
efTree.setNames(filesMap);
|
||||
|
||||
// Erstelle ein Namens-Dictionary und füge den Baum hinzu
|
||||
PDDocumentNameDictionary names = new PDDocumentNameDictionary(document.getDocumentCatalog());
|
||||
names.setEmbeddedFiles(efTree);
|
||||
document.getDocumentCatalog().setNames(names);
|
||||
|
||||
// Speichere das PDF-Dokument
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
byte[] eInvoiceData = baos.toByteArray();
|
||||
document.close();
|
||||
baos.close();
|
||||
|
||||
FileData eInvoiceFileData = new FileData(pdfFileData.getName(), eInvoiceData, pdfFileData.getContentType(),
|
||||
pdfFileData.getAttributes());
|
||||
return eInvoiceFileData;
|
||||
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(), CONFIG_ERROR,
|
||||
"Failed to generate e-invoice: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* THis method updates an e-invoice template with the data stored in the
|
||||
* workitem.
|
||||
*
|
||||
* First the method loads an EInvoiceModel based on the provided XML Template
|
||||
* and than updates the e-invoice data based on the items stored in the given
|
||||
* workitem.
|
||||
*
|
||||
* @param workitem
|
||||
* @throws PluginException
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException {
|
||||
|
||||
try {
|
||||
EInvoiceModel model = EInvoiceModelFactory.read(new ByteArrayInputStream(fileDataXMLTemplate.getContent()));
|
||||
|
||||
model.setId(workitem.getItemValueString("invoice.number"));
|
||||
|
||||
// Update Rechnungssummen...
|
||||
if (workitem.getItemValueDouble("invoice.vat.rate") > 0) {
|
||||
// wir haben eine Steuer!
|
||||
model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.amount.total")
|
||||
- workitem.getItemValueDouble("invoice.amount.net")));
|
||||
model.setTaxRate(workitem.getItemValueDouble("invoice.vat.rate"));
|
||||
}
|
||||
|
||||
// // Update Invoice Items
|
||||
// List<ItemCollection> invoiceItems = explodeChildList(workitem);
|
||||
// double lineTotalAmount = 0.00;
|
||||
// for (ItemCollection invoiceItem : invoiceItems) {
|
||||
// TradeLineItem tradeLineItem = buildTradeLineItem(invoiceItem);
|
||||
// model.setTradeLineItem(tradeLineItem);
|
||||
// lineTotalAmount = lineTotalAmount + tradeLineItem.getTotal();
|
||||
// }
|
||||
|
||||
// // Summenbildung
|
||||
// model.setNetTotalAmount(workitem.getItemValueDouble("invoice.amount.net"));
|
||||
// model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.amount.total"));
|
||||
// model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date"));
|
||||
|
||||
// // Update Addresses
|
||||
// TradeParty billingAddress =
|
||||
// parseAddress(workitem.getItemValue("dbtr.address"), "buyer");
|
||||
// model.setTradeParty(billingAddress);
|
||||
|
||||
// TradeParty shippingAddress =
|
||||
// parseAddress(workitem.getItemValue("dbtr.address.shipping"), "ship_to");
|
||||
// model.setTradeParty(shippingAddress);
|
||||
|
||||
// model.setOrderReferenceId(workitem.getItemValueString("order.number"));
|
||||
|
||||
// // finally update the template file
|
||||
// fileDataXMLTemplate.setContent(model.getContent());
|
||||
|
||||
} catch (FileNotFoundException | EInvoiceFormatException e) {
|
||||
throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, e.getMessage(), e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of address lines into a TradeParty object using best-guess
|
||||
* approach
|
||||
*/
|
||||
/**
|
||||
* Erstellt ein TradeParty Objekt aus einer Liste von Adresszeilen.
|
||||
*
|
||||
* @param addressLines Liste der Adresszeilen
|
||||
* @param type Der Typ der TradeParty
|
||||
* @return TradeParty
|
||||
*/
|
||||
public static TradeParty parseAddress(List<String> addressLines, String type) {
|
||||
if (addressLines == null || addressLines.isEmpty()) {
|
||||
throw new IllegalArgumentException("Address lines cannot be null or empty");
|
||||
}
|
||||
|
||||
TradeParty tradeParty = new TradeParty(type);
|
||||
|
||||
// Name ist immer die erste Zeile
|
||||
tradeParty.setName(addressLines.get(0).trim());
|
||||
|
||||
// Letzte Zeile analysieren
|
||||
String lastLine = addressLines.get(addressLines.size() - 1).trim();
|
||||
|
||||
// Prüfen ob die letzte Zeile ein PLZ + Stadt Format hat
|
||||
if (hasPostcodeAndCity(lastLine)) {
|
||||
tradeParty.setCountryId("DE");
|
||||
parsePostcodeAndCity(lastLine, tradeParty);
|
||||
|
||||
// Straßenadresse aus den Zeilen zwischen Name und PLZ/Stadt
|
||||
buildStreetAddress(addressLines.subList(1, addressLines.size() - 1), tradeParty);
|
||||
} else {
|
||||
// Wenn die letzte Zeile kein PLZ + Stadt Format hat, dann ist es ein Land
|
||||
String countryCode = resolveISOCountryCodes(lastLine.toUpperCase());
|
||||
tradeParty.setCountryId(countryCode);
|
||||
// Stadt und PLZ sind in der vorletzten Zeile
|
||||
if (addressLines.size() > 2) {
|
||||
parsePostcodeAndCity(addressLines.get(addressLines.size() - 2), tradeParty);
|
||||
// Straßenadresse aus den Zeilen zwischen Name und PLZ/Stadt/Land
|
||||
buildStreetAddress(addressLines.subList(1, addressLines.size() - 2), tradeParty);
|
||||
}
|
||||
}
|
||||
|
||||
return tradeParty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfemethode zum übersetzen von ISO Country codes
|
||||
*
|
||||
* @param country
|
||||
* @return
|
||||
*/
|
||||
private static String resolveISOCountryCodes(String country) {
|
||||
String countryCode = country.toUpperCase().substring(0, 2);
|
||||
|
||||
// Bulgarien -> BG
|
||||
countryCode = countryCode.replace("BU", "BG");
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die Straßenadresse aus den gegebenen Zeilen
|
||||
*/
|
||||
private static void buildStreetAddress(List<String> streetLines, TradeParty tradeParty) {
|
||||
if (streetLines.isEmpty())
|
||||
return;
|
||||
|
||||
StringBuilder streetAddress = new StringBuilder();
|
||||
for (int i = 0; i < streetLines.size(); i++) {
|
||||
if (i > 0) {
|
||||
streetAddress.append(", ");
|
||||
}
|
||||
streetAddress.append(streetLines.get(i).trim());
|
||||
}
|
||||
tradeParty.setStreetAddress(streetAddress.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine Zeile PLZ und Stadt enthält.
|
||||
* Eine PLZ wird als 4-5 stellige Zahl am Anfang der Zeile erkannt.
|
||||
*/
|
||||
private static boolean hasPostcodeAndCity(String line) {
|
||||
String[] parts = line.trim().split("\\s+", 2);
|
||||
if (parts.length < 2)
|
||||
return false;
|
||||
|
||||
String potentialPostcode = parts[0];
|
||||
return potentialPostcode.matches("\\d{4,5}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Analysiert die PLZ und Stadt aus einer Zeile.
|
||||
*/
|
||||
private static void parsePostcodeAndCity(String line, TradeParty tradeParty) {
|
||||
String[] parts = line.trim().split("\\s+", 2);
|
||||
if (parts.length >= 2) {
|
||||
tradeParty.setPostcodeCode(parts[0].trim());
|
||||
tradeParty.setCityName(parts[1].trim());
|
||||
} else {
|
||||
tradeParty.setCityName(line.trim());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the data list of an invoice line and returns a TradeLineItem object.
|
||||
* The order of the list items must be exactly!
|
||||
*/
|
||||
private TradeLineItem buildTradeLineItem(ItemCollection orderItem) {
|
||||
if (orderItem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TradeLineItem tradeLineItem = new TradeLineItem(orderItem.getItemValueString("id"));
|
||||
|
||||
tradeLineItem.setName(orderItem.getItemValueString("name"));
|
||||
tradeLineItem.setQuantity(orderItem.getItemValueDouble("quantity"));
|
||||
tradeLineItem.setNetPrice(orderItem.getItemValueDouble("price"));
|
||||
tradeLineItem.setTotal(orderItem.getItemValueDouble("total"));
|
||||
tradeLineItem.setTaxRate(orderItem.getItemValueDouble("vat"));
|
||||
|
||||
// if (orderItem.hasItem("orderid")) {
|
||||
// tradeLineItem.setOrderReferenceId(orderItem.getItemValueString("orderid"));
|
||||
// }
|
||||
return tradeLineItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method to set a item value by a regex
|
||||
*
|
||||
* @param text
|
||||
* @param regex
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
private void setItemValueByRegex(ItemCollection workitem, String itemName, String content, String regex,
|
||||
Class itemType) {
|
||||
|
||||
String value = getValueByRegex(content, regex);
|
||||
logger.info(itemName + "=" + value);
|
||||
if (value.isEmpty()) {
|
||||
return; // no value found
|
||||
}
|
||||
|
||||
// Parse Double?
|
||||
if (itemType == Double.class) {
|
||||
if (value.contains(",")) {
|
||||
value = value.replace(".", "");
|
||||
}
|
||||
value = value.replace(",", ".");
|
||||
workitem.setItemValue(itemName, Double.parseDouble(value));
|
||||
} else
|
||||
// Parse Integer
|
||||
if (itemType == Integer.class) {
|
||||
workitem.setItemValue(itemName, Integer.parseInt(value));
|
||||
} else
|
||||
// Parse Date
|
||||
if (itemType == Date.class) {
|
||||
try {
|
||||
workitem.setItemValue(itemName, dateFormatter.parse(value));
|
||||
} catch (ParseException e) {
|
||||
logger.warning("Date value " + value + " not parsable: " + e.getMessage());
|
||||
workitem.setItemValue(itemName, value);
|
||||
}
|
||||
} else {
|
||||
workitem.setItemValue(itemName, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method to extract a text fragment by regex
|
||||
*
|
||||
* @param text
|
||||
* @param regex
|
||||
* @return
|
||||
*/
|
||||
private String getValueByRegex(String text, String regex) {
|
||||
String result = "";
|
||||
Pattern datumPattern = Pattern.compile(regex);
|
||||
Matcher datumMatcher = datumPattern.matcher(text);
|
||||
if (datumMatcher.find()) {
|
||||
result = datumMatcher.group(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(EInvoiceConverterAdapter.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(EInvoiceConverterAdapter.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<ItemCollection> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,548 @@
|
|||
package com.alexanderlogistics.einvoice;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
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;
|
||||
import org.imixs.einvoice.TradeLineItem;
|
||||
import org.imixs.einvoice.TradeParty;
|
||||
import org.imixs.workflow.FileData;
|
||||
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 org.imixs.workflow.exceptions.QueryException;
|
||||
import org.imixs.workflow.util.XMLParser;
|
||||
|
||||
import com.alexanderlogistics.InvoiceUtil;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
/**
|
||||
* The KSeFAdapter converts a Cargsoft outbound invoice into an KSeF XML
|
||||
* e-invoice and sends the xml file to the polish KSeF API
|
||||
*
|
||||
* The adapter can be configured by the model:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
<e-invoice name="ksef">
|
||||
<textblock>textblock-ref</textblock>
|
||||
<template>filename</template>
|
||||
<debug>true</debug>
|
||||
</e-invoice>
|
||||
}
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* Wenn Debug = true gibt der Adapter alle einzelschritte auf der console aus
|
||||
* und erzeugt zusätzlich die factur-x xml und txt dateien.
|
||||
*
|
||||
*
|
||||
*
|
||||
* @version 1.0
|
||||
* @author rsoika
|
||||
*/
|
||||
public class KSeFAdapter 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(EInvoiceConverterAdapter.class.getName());
|
||||
|
||||
public static SimpleDateFormat dateFormatter = new SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN);
|
||||
public static NumberFormat numberFormat = NumberFormat.getInstance(Locale.GERMANY);
|
||||
boolean debug = false;
|
||||
|
||||
@Inject
|
||||
WorkflowService workflowService;
|
||||
|
||||
@Inject
|
||||
DocumentService documentService;
|
||||
|
||||
@Inject
|
||||
SnapshotService snapshotService;
|
||||
|
||||
/**
|
||||
* This method
|
||||
*
|
||||
* @throws PluginException
|
||||
*/
|
||||
@Override
|
||||
public ItemCollection execute(ItemCollection workitem, ItemCollection event)
|
||||
throws AdapterException, PluginException {
|
||||
|
||||
logger.info("├── 🔜 Convert Invoice to KSeF...");
|
||||
// read configuration....
|
||||
ItemCollection eInvoiceConfig = workflowService.evalWorkflowResult(event, "e-invoice",
|
||||
workitem,
|
||||
false);
|
||||
if (eInvoiceConfig == null || !eInvoiceConfig.hasItem("ksef")) {
|
||||
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(), CONFIG_ERROR,
|
||||
"missing e-invoice/ksef configuration in model event - please check model configuration");
|
||||
}
|
||||
ItemCollection ksefDefinition = XMLParser.parseItemStructure(eInvoiceConfig.getItemValueString("ksef"));
|
||||
try {
|
||||
|
||||
// Load the e-invoice template....
|
||||
FileData xmlFileData = loadXMLTemplate(workitem, ksefDefinition);
|
||||
FileData pdfFileData = loadPDF(workitem);
|
||||
|
||||
logger.info("│ ├── Source File: " + pdfFileData.getName());
|
||||
|
||||
if (true)
|
||||
throw new AdapterException(KSeFAdapter.class.getSimpleName(), CONFIG_ERROR, "NOT YET IMPLEMENTED");
|
||||
} catch (PluginException e) {
|
||||
throw new AdapterException(e);
|
||||
// throw new AdapterException(
|
||||
// EInvoiceConverterAdapter.class.getSimpleName(), e.getErrorCode(),
|
||||
// e.getMessage());
|
||||
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// try to load filedata....
|
||||
pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), cargosoftFileName);
|
||||
|
||||
}
|
||||
|
||||
if (pdfFileData == null) {
|
||||
// take first pdf file available
|
||||
List<String> 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(EInvoiceConverterAdapter.class.getSimpleName(),
|
||||
DOCUMENT_ERROR,
|
||||
"DOCUMENT_ERROR: no pdf file found!");
|
||||
}
|
||||
return pdfFileData;
|
||||
|
||||
}
|
||||
|
||||
private FileData embeddXML(FileData pdfFileData, FileData xmlFileData) throws PluginException {
|
||||
try {
|
||||
PDDocument document = PDDocument.load(pdfFileData.getContent());
|
||||
byte[] xmlBytes = xmlFileData.getContent();
|
||||
|
||||
// Erstelle einen InputStream aus dem XML Content
|
||||
InputStream inputStream = new ByteArrayInputStream(xmlBytes);
|
||||
// Erstelle eine Datei-Spezifikation für die XML
|
||||
PDComplexFileSpecification fileSpec = new PDComplexFileSpecification();
|
||||
fileSpec.setFile(xmlFileData.getName());
|
||||
// Erstelle einen PDEmbeddedFile mit dem InputStream
|
||||
PDEmbeddedFile embeddedFile = new PDEmbeddedFile(document, inputStream);
|
||||
embeddedFile.setSubtype("application/xml");
|
||||
embeddedFile.setSize(xmlBytes.length);
|
||||
// Schließe den InputStream
|
||||
inputStream.close();
|
||||
// Setze den PDEmbeddedFile
|
||||
fileSpec.setEmbeddedFile(embeddedFile);
|
||||
|
||||
// Erstelle einen Map-Eintrag für die eingebettete Datei
|
||||
Map<String, PDComplexFileSpecification> filesMap = new HashMap<>();
|
||||
filesMap.put(xmlFileData.getName(), fileSpec);
|
||||
|
||||
// Erstelle einen Eingebettete-Dateien-Namensbaum und füge die Datei hinzu
|
||||
PDEmbeddedFilesNameTreeNode efTree = new PDEmbeddedFilesNameTreeNode();
|
||||
efTree.setNames(filesMap);
|
||||
|
||||
// Erstelle ein Namens-Dictionary und füge den Baum hinzu
|
||||
PDDocumentNameDictionary names = new PDDocumentNameDictionary(document.getDocumentCatalog());
|
||||
names.setEmbeddedFiles(efTree);
|
||||
document.getDocumentCatalog().setNames(names);
|
||||
|
||||
// Speichere das PDF-Dokument
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
byte[] eInvoiceData = baos.toByteArray();
|
||||
document.close();
|
||||
baos.close();
|
||||
|
||||
FileData eInvoiceFileData = new FileData(pdfFileData.getName(), eInvoiceData, pdfFileData.getContentType(),
|
||||
pdfFileData.getAttributes());
|
||||
return eInvoiceFileData;
|
||||
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
throw new PluginException(EInvoiceConverterAdapter.class.getSimpleName(), CONFIG_ERROR,
|
||||
"Failed to generate e-invoice: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* THis method updates an e-invoice template with the data stored in the
|
||||
* workitem.
|
||||
*
|
||||
* First the method loads an EInvoiceModel based on the provided XML Template
|
||||
* and than updates the e-invoice data based on the items stored in the given
|
||||
* workitem.
|
||||
*
|
||||
* @param workitem
|
||||
* @throws PluginException
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException {
|
||||
|
||||
try {
|
||||
EInvoiceModel model = EInvoiceModelFactory.read(new ByteArrayInputStream(fileDataXMLTemplate.getContent()));
|
||||
|
||||
model.setId(workitem.getItemValueString("invoice.number"));
|
||||
|
||||
// Update Rechnungssummen...
|
||||
if (workitem.getItemValueDouble("invoice.vat.rate") > 0) {
|
||||
// wir haben eine Steuer!
|
||||
model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.amount.total")
|
||||
- workitem.getItemValueDouble("invoice.amount.net")));
|
||||
model.setTaxRate(workitem.getItemValueDouble("invoice.vat.rate"));
|
||||
}
|
||||
|
||||
// // Update Invoice Items
|
||||
// List<ItemCollection> invoiceItems = explodeChildList(workitem);
|
||||
// double lineTotalAmount = 0.00;
|
||||
// for (ItemCollection invoiceItem : invoiceItems) {
|
||||
// TradeLineItem tradeLineItem = buildTradeLineItem(invoiceItem);
|
||||
// model.setTradeLineItem(tradeLineItem);
|
||||
// lineTotalAmount = lineTotalAmount + tradeLineItem.getTotal();
|
||||
// }
|
||||
|
||||
// // Summenbildung
|
||||
// model.setNetTotalAmount(workitem.getItemValueDouble("invoice.amount.net"));
|
||||
// model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.amount.total"));
|
||||
// model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date"));
|
||||
|
||||
// // Update Addresses
|
||||
// TradeParty billingAddress =
|
||||
// parseAddress(workitem.getItemValue("dbtr.address"), "buyer");
|
||||
// model.setTradeParty(billingAddress);
|
||||
|
||||
// TradeParty shippingAddress =
|
||||
// parseAddress(workitem.getItemValue("dbtr.address.shipping"), "ship_to");
|
||||
// model.setTradeParty(shippingAddress);
|
||||
|
||||
// model.setOrderReferenceId(workitem.getItemValueString("order.number"));
|
||||
|
||||
// // finally update the template file
|
||||
// fileDataXMLTemplate.setContent(model.getContent());
|
||||
|
||||
} catch (FileNotFoundException | EInvoiceFormatException e) {
|
||||
throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, e.getMessage(), e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of address lines into a TradeParty object using best-guess
|
||||
* approach
|
||||
*/
|
||||
/**
|
||||
* Erstellt ein TradeParty Objekt aus einer Liste von Adresszeilen.
|
||||
*
|
||||
* @param addressLines Liste der Adresszeilen
|
||||
* @param type Der Typ der TradeParty
|
||||
* @return TradeParty
|
||||
*/
|
||||
public static TradeParty parseAddress(List<String> addressLines, String type) {
|
||||
if (addressLines == null || addressLines.isEmpty()) {
|
||||
throw new IllegalArgumentException("Address lines cannot be null or empty");
|
||||
}
|
||||
|
||||
TradeParty tradeParty = new TradeParty(type);
|
||||
|
||||
// Name ist immer die erste Zeile
|
||||
tradeParty.setName(addressLines.get(0).trim());
|
||||
|
||||
// Letzte Zeile analysieren
|
||||
String lastLine = addressLines.get(addressLines.size() - 1).trim();
|
||||
|
||||
// Prüfen ob die letzte Zeile ein PLZ + Stadt Format hat
|
||||
if (hasPostcodeAndCity(lastLine)) {
|
||||
tradeParty.setCountryId("DE");
|
||||
parsePostcodeAndCity(lastLine, tradeParty);
|
||||
|
||||
// Straßenadresse aus den Zeilen zwischen Name und PLZ/Stadt
|
||||
buildStreetAddress(addressLines.subList(1, addressLines.size() - 1), tradeParty);
|
||||
} else {
|
||||
// Wenn die letzte Zeile kein PLZ + Stadt Format hat, dann ist es ein Land
|
||||
String countryCode = resolveISOCountryCodes(lastLine.toUpperCase());
|
||||
tradeParty.setCountryId(countryCode);
|
||||
// Stadt und PLZ sind in der vorletzten Zeile
|
||||
if (addressLines.size() > 2) {
|
||||
parsePostcodeAndCity(addressLines.get(addressLines.size() - 2), tradeParty);
|
||||
// Straßenadresse aus den Zeilen zwischen Name und PLZ/Stadt/Land
|
||||
buildStreetAddress(addressLines.subList(1, addressLines.size() - 2), tradeParty);
|
||||
}
|
||||
}
|
||||
|
||||
return tradeParty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfemethode zum übersetzen von ISO Country codes
|
||||
*
|
||||
* @param country
|
||||
* @return
|
||||
*/
|
||||
private static String resolveISOCountryCodes(String country) {
|
||||
String countryCode = country.toUpperCase().substring(0, 2);
|
||||
|
||||
// Bulgarien -> BG
|
||||
countryCode = countryCode.replace("BU", "BG");
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die Straßenadresse aus den gegebenen Zeilen
|
||||
*/
|
||||
private static void buildStreetAddress(List<String> streetLines, TradeParty tradeParty) {
|
||||
if (streetLines.isEmpty())
|
||||
return;
|
||||
|
||||
StringBuilder streetAddress = new StringBuilder();
|
||||
for (int i = 0; i < streetLines.size(); i++) {
|
||||
if (i > 0) {
|
||||
streetAddress.append(", ");
|
||||
}
|
||||
streetAddress.append(streetLines.get(i).trim());
|
||||
}
|
||||
tradeParty.setStreetAddress(streetAddress.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine Zeile PLZ und Stadt enthält.
|
||||
* Eine PLZ wird als 4-5 stellige Zahl am Anfang der Zeile erkannt.
|
||||
*/
|
||||
private static boolean hasPostcodeAndCity(String line) {
|
||||
String[] parts = line.trim().split("\\s+", 2);
|
||||
if (parts.length < 2)
|
||||
return false;
|
||||
|
||||
String potentialPostcode = parts[0];
|
||||
return potentialPostcode.matches("\\d{4,5}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Analysiert die PLZ und Stadt aus einer Zeile.
|
||||
*/
|
||||
private static void parsePostcodeAndCity(String line, TradeParty tradeParty) {
|
||||
String[] parts = line.trim().split("\\s+", 2);
|
||||
if (parts.length >= 2) {
|
||||
tradeParty.setPostcodeCode(parts[0].trim());
|
||||
tradeParty.setCityName(parts[1].trim());
|
||||
} else {
|
||||
tradeParty.setCityName(line.trim());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the data list of an invoice line and returns a TradeLineItem object.
|
||||
* The order of the list items must be exactly!
|
||||
*/
|
||||
private TradeLineItem buildTradeLineItem(ItemCollection orderItem) {
|
||||
if (orderItem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TradeLineItem tradeLineItem = new TradeLineItem(orderItem.getItemValueString("id"));
|
||||
|
||||
tradeLineItem.setName(orderItem.getItemValueString("name"));
|
||||
tradeLineItem.setQuantity(orderItem.getItemValueDouble("quantity"));
|
||||
tradeLineItem.setNetPrice(orderItem.getItemValueDouble("price"));
|
||||
tradeLineItem.setTotal(orderItem.getItemValueDouble("total"));
|
||||
tradeLineItem.setTaxRate(orderItem.getItemValueDouble("vat"));
|
||||
|
||||
// if (orderItem.hasItem("orderid")) {
|
||||
// tradeLineItem.setOrderReferenceId(orderItem.getItemValueString("orderid"));
|
||||
// }
|
||||
return tradeLineItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method to set a item value by a regex
|
||||
*
|
||||
* @param text
|
||||
* @param regex
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
private void setItemValueByRegex(ItemCollection workitem, String itemName, String content, String regex,
|
||||
Class itemType) {
|
||||
|
||||
String value = getValueByRegex(content, regex);
|
||||
logger.info(itemName + "=" + value);
|
||||
if (value.isEmpty()) {
|
||||
return; // no value found
|
||||
}
|
||||
|
||||
// Parse Double?
|
||||
if (itemType == Double.class) {
|
||||
if (value.contains(",")) {
|
||||
value = value.replace(".", "");
|
||||
}
|
||||
value = value.replace(",", ".");
|
||||
workitem.setItemValue(itemName, Double.parseDouble(value));
|
||||
} else
|
||||
// Parse Integer
|
||||
if (itemType == Integer.class) {
|
||||
workitem.setItemValue(itemName, Integer.parseInt(value));
|
||||
} else
|
||||
// Parse Date
|
||||
if (itemType == Date.class) {
|
||||
try {
|
||||
workitem.setItemValue(itemName, dateFormatter.parse(value));
|
||||
} catch (ParseException e) {
|
||||
logger.warning("Date value " + value + " not parsable: " + e.getMessage());
|
||||
workitem.setItemValue(itemName, value);
|
||||
}
|
||||
} else {
|
||||
workitem.setItemValue(itemName, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method to extract a text fragment by regex
|
||||
*
|
||||
* @param text
|
||||
* @param regex
|
||||
* @return
|
||||
*/
|
||||
private String getValueByRegex(String text, String regex) {
|
||||
String result = "";
|
||||
Pattern datumPattern = Pattern.compile(regex);
|
||||
Matcher datumMatcher = datumPattern.matcher(text);
|
||||
if (datumMatcher.find()) {
|
||||
result = datumMatcher.group(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(EInvoiceConverterAdapter.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(EInvoiceConverterAdapter.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<ItemCollection> 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;
|
||||
}
|
||||
|
||||
}
|
||||
113
workflow/pl/e-invoice/factur-x.xml
Normal file
113
workflow/pl/e-invoice/factur-x.xml
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<rsm:CrossIndustryInvoice xmlns:a='urn:un:unece:uncefact:data:standard:QualifiedDataType:100'
|
||||
xmlns:rsm='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'
|
||||
xmlns:qdt='urn:un:unece:uncefact:data:standard:QualifiedDataType:10'
|
||||
xmlns:ram='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'
|
||||
xmlns:xs='http://www.w3.org/2001/XMLSchema'
|
||||
xmlns:udt='urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'>
|
||||
<rsm:ExchangedDocumentContext>
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID></ram:ID>
|
||||
<ram:TypeCode>380</ram:TypeCode>
|
||||
<ram:IssueDateTime>
|
||||
<udt:DateTimeString format="102"></udt:DateTimeString>
|
||||
</ram:IssueDateTime>
|
||||
<ram:IncludedNote>
|
||||
<ram:Content>Wir berechnen heute unter Zugrundelegung unserer allgemeinen
|
||||
Geschäftsbedingungen, die Ihnen bereits
|
||||
übersendet wurden oder die Sie unter www.mueller-ahlhorn.com nachlesen können, wie
|
||||
folgt:</ram:Content>
|
||||
</ram:IncludedNote>
|
||||
<ram:IncludedNote>
|
||||
<ram:Content>Bitte zahlen Sie ab sofort alle Rechnungen auf unser Konto bei der
|
||||
Landessparkasse zu Oldenburg, BIC
|
||||
SLZODE22XXX, IBAN DE04 2805 0100 0092 2828 70. Etwaig anfallende Bankspesen gehen zu
|
||||
Ihren Lasten.</ram:Content>
|
||||
</ram:IncludedNote>
|
||||
<ram:IncludedNote>
|
||||
<ram:Content>Dr. Dietrich Müller GmbH</ram:Content>
|
||||
</ram:IncludedNote>
|
||||
<ram:IncludedNote>
|
||||
<ram:Content>Geschäftsführer: Dr. Michael Müller </ram:Content>
|
||||
</ram:IncludedNote>
|
||||
<ram:IncludedNote>
|
||||
<ram:Content>Amtsgericht Oldenburg HRB 209026</ram:Content>
|
||||
</ram:IncludedNote>
|
||||
<ram:IncludedNote>
|
||||
<ram:Content>info@mueller-ahlhorn.com</ram:Content>
|
||||
</ram:IncludedNote>
|
||||
</rsm:ExchangedDocument>
|
||||
<rsm:SupplyChainTradeTransaction>
|
||||
|
||||
<ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:BuyerReference />
|
||||
<ram:SellerTradeParty>
|
||||
<ram:Name>Dr. Dietrich Müller GmbH</ram:Name>
|
||||
<ram:DefinedTradeContact>
|
||||
<ram:PersonName>Müller, Michael</ram:PersonName>
|
||||
<ram:TelephoneUniversalCommunication>
|
||||
<ram:CompleteNumber>+4944359710261</ram:CompleteNumber>
|
||||
</ram:TelephoneUniversalCommunication>
|
||||
<ram:EmailURIUniversalCommunication>
|
||||
<ram:URIID>info@mueller-ahlhorn.com</ram:URIID>
|
||||
</ram:EmailURIUniversalCommunication>
|
||||
</ram:DefinedTradeContact>
|
||||
<ram:PostalTradeAddress>
|
||||
<ram:PostcodeCode>26197</ram:PostcodeCode>
|
||||
<ram:LineOne>Zeppelinring 18</ram:LineOne>
|
||||
<ram:CityName>Ahlhorn</ram:CityName>
|
||||
<ram:CountryID>DE</ram:CountryID>
|
||||
</ram:PostalTradeAddress>
|
||||
<ram:URIUniversalCommunication>
|
||||
<ram:URIID schemeID="EM">info@mueller-ahlhorn.com</ram:URIID>
|
||||
</ram:URIUniversalCommunication>
|
||||
<ram:SpecifiedTaxRegistration>
|
||||
<ram:ID schemeID="VA">DE295969093</ram:ID>
|
||||
</ram:SpecifiedTaxRegistration>
|
||||
</ram:SellerTradeParty>
|
||||
<ram:BuyerTradeParty>
|
||||
</ram:BuyerTradeParty>
|
||||
</ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:ApplicableHeaderTradeDelivery>
|
||||
|
||||
</ram:ApplicableHeaderTradeDelivery>
|
||||
<ram:ApplicableHeaderTradeSettlement>
|
||||
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
|
||||
<ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:TypeCode>30</ram:TypeCode>
|
||||
<ram:PayeePartyCreditorFinancialAccount>
|
||||
<ram:IBANID>DE04 2805 0100 0092 2828 70</ram:IBANID>
|
||||
</ram:PayeePartyCreditorFinancialAccount>
|
||||
<ram:PayeeSpecifiedCreditorFinancialInstitution>
|
||||
<ram:BICID>SLZODE22XXX</ram:BICID>
|
||||
</ram:PayeeSpecifiedCreditorFinancialInstitution>
|
||||
</ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:CalculatedAmount>0.00</ram:CalculatedAmount>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:BasisAmount>0.00</ram:BasisAmount>
|
||||
<ram:CategoryCode>Z</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>0.00</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradePaymentTerms>
|
||||
<ram:Description>Wir berechnen heute unter Zugrundelegung unserer allgemeinen
|
||||
Geschäftsbedingungen, die Ihnen bereits
|
||||
übersendet wurden oder die Sie unter www.mueller-ahlhorn.com nachlesen können.</ram:Description>
|
||||
</ram:SpecifiedTradePaymentTerms>
|
||||
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
<ram:LineTotalAmount>0.00</ram:LineTotalAmount>
|
||||
<ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>
|
||||
<ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>
|
||||
<ram:TaxBasisTotalAmount>0.00</ram:TaxBasisTotalAmount>
|
||||
<ram:TaxTotalAmount currencyID="EUR">0.00</ram:TaxTotalAmount>
|
||||
<ram:GrandTotalAmount>0.00</ram:GrandTotalAmount>
|
||||
<ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
|
||||
<ram:DuePayableAmount>0.00</ram:DuePayableAmount>
|
||||
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
</ram:ApplicableHeaderTradeSettlement>
|
||||
</rsm:SupplyChainTradeTransaction>
|
||||
</rsm:CrossIndustryInvoice>
|
||||
2628
workflow/pl/rechnungsausgang-pl-1.1.0.bpmn
Normal file
2628
workflow/pl/rechnungsausgang-pl-1.1.0.bpmn
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue