382 lines
No EOL
15 KiB
Java
382 lines
No EOL
15 KiB
Java
package com.alexanderlogistics.einvoice;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.text.NumberFormat;
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.Collection;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.logging.Logger;
|
|
|
|
import javax.xml.transform.TransformerException;
|
|
|
|
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.EInvoiceNS;
|
|
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 org.w3c.dom.Element;
|
|
|
|
import com.alexanderlogistics.BusinessPartnerService;
|
|
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
|
|
<ksef name="create">
|
|
<textblock>textblock-ref</textblock>
|
|
<template>filename</template>
|
|
<debug>true</debug>
|
|
</ksef>
|
|
|
|
}
|
|
* </pre>
|
|
*
|
|
* <p>
|
|
* NIP: The adapter needs the NIP (vat-id). If we do not find the partner.vat in
|
|
* the business object, we do a lookup on the D-Cargosoft object and try the vat
|
|
* id from there. This is because the partner.vat is a field which was not
|
|
* defined before.
|
|
*
|
|
* <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(EInvoiceAdapter.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;
|
|
|
|
@Inject
|
|
BusinessPartnerService businessPartnerService;
|
|
|
|
/**
|
|
* 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, "ksef",
|
|
workitem,
|
|
false);
|
|
if (eInvoiceConfig == null || !eInvoiceConfig.hasItem("CREATE")) {
|
|
throw new PluginException(EInvoiceAdapter.class.getSimpleName(), CONFIG_ERROR,
|
|
"missing e-invoice/ksef configuration in model event - please check model configuration");
|
|
}
|
|
ItemCollection ksefCreateDefinition = XMLParser.parseItemStructure(eInvoiceConfig.getItemValueString("CREATE"));
|
|
try {
|
|
|
|
// Load the e-invoice template....
|
|
FileData xmlFileData = loadXMLTemplate(workitem, ksefCreateDefinition);
|
|
|
|
updateEInvoice(xmlFileData, workitem);
|
|
|
|
// append XML document
|
|
logger.info("│ ├── attach KSeF e-invoice...");
|
|
workitem.addFileData(xmlFileData);
|
|
|
|
} catch (PluginException e) {
|
|
throw new AdapterException(e);
|
|
}
|
|
|
|
return workitem;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
private void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException {
|
|
|
|
try {
|
|
EInvoiceModel model = EInvoiceModelFactory.read(new ByteArrayInputStream(fileDataXMLTemplate.getContent()));
|
|
|
|
model.setId(workitem.getItemValueString("invoice.number"));
|
|
|
|
// date
|
|
model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date"));
|
|
model.setDueDateTime(workitem.getItemValueLocalDate("invoice.duedate"));
|
|
|
|
// Update Addresses
|
|
TradeParty billingAddress = buildAddress(workitem.getItemValueString("partner.id"), "buyer", model);
|
|
model.setTradeParty(billingAddress);
|
|
|
|
// Update Invoice Type (RodzajFaktury) -> VAT | KOR
|
|
Element elementFa = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Fa");
|
|
|
|
if (isKorrekturRechnung(workitem)) {
|
|
model.updateElementValue(elementFa, EInvoiceNS.KSEF, "RodzajFaktury", "KOR");
|
|
logger.info("│ ├── invoice type=KOR");
|
|
} else {
|
|
// normale rechnung
|
|
model.updateElementValue(elementFa, EInvoiceNS.KSEF, "RodzajFaktury", "VAT");
|
|
logger.info("│ ├── invoice type=VAT");
|
|
}
|
|
// Update Rechnungssummen...
|
|
if (workitem.getItemValueDouble("invoice.total.tax") > 0) {
|
|
// wir haben eine Steuer!
|
|
model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.total")
|
|
- workitem.getItemValueDouble("invoice.total.net")));
|
|
model.setTaxRate(workitem.getItemValueDouble("invoice.total.tax"));
|
|
}
|
|
|
|
// set currency
|
|
model.updateElementValue(elementFa, EInvoiceNS.KSEF, "KodWaluty",
|
|
workitem.getItemValueString("invoice.currency"));
|
|
|
|
// Update Invoice Items
|
|
List<ItemCollection> invoiceItems = InvoiceUtil.explodeChildList(workitem, "_childitems");
|
|
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.total"));
|
|
model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.total"));
|
|
|
|
// finally update the template file
|
|
fileDataXMLTemplate.setContent(model.getContent());
|
|
|
|
} catch (FileNotFoundException | EInvoiceFormatException | TransformerException e) {
|
|
throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, e.getMessage(), e);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Gibt True zurück wenn es eine korrektur rechnung ist
|
|
*
|
|
* InvoiceHeader/Correction=true
|
|
* InvoiceHeader/CorrectionInvoiceNumber !empty
|
|
*
|
|
*
|
|
* @param workitem
|
|
* @return
|
|
*/
|
|
private boolean isKorrekturRechnung(ItemCollection workitem) {
|
|
return ("true".equals(workitem.getItemValueString("invoice.correction"))
|
|
&& !workitem.getItemValueString("invoice.CorrectionInvoiceNumber").isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Erstellt ein TradeParty Objekt aus einer Liste von Adresszeilen.
|
|
*
|
|
* @param addressLines Liste der Adresszeilen
|
|
* @param type Der Typ der TradeParty
|
|
* @return TradeParty
|
|
*/
|
|
public TradeParty buildAddress(String partnerID, String type, EInvoiceModel model) throws PluginException {
|
|
if (partnerID == null || partnerID.isEmpty()) {
|
|
throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, "Missing partnerID");
|
|
}
|
|
|
|
ItemCollection businessPartner = businessPartnerService.getBusinessPartnerByID(partnerID);
|
|
if (businessPartner == null) {
|
|
throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR,
|
|
"Business Partner ID '" + partnerID + "' does not exist");
|
|
}
|
|
|
|
/**
|
|
* Migration:
|
|
* If the business partner does not yet have the item "partner.vat" we try here
|
|
* to resync this information from the cargosoftkreditor object. This is needed
|
|
* because the partner.vat was not initially defined by the synch processor and
|
|
* so many partner objects do not yet provide this information.
|
|
*/
|
|
if (businessPartner.getItemValueString("partner.vat").isEmpty()) {
|
|
logger.info("│ ├── BusinessPartner " + partnerID + " does not provide vat-id - try to migrate....");
|
|
String dNumber = businessPartner.getItemValueString("dbtr.number");
|
|
if (!dNumber.isEmpty()) {
|
|
try {
|
|
String query = "(type:cargosoftkreditor) AND (name:" + dNumber + ")";
|
|
List<ItemCollection> result;
|
|
result = documentService.find(query, 1, 0);
|
|
if (result != null && result.size() > 0) {
|
|
ItemCollection creditorData = result.get(0);
|
|
String vatID = creditorData.getItemValueString("_vendor_vat_registration_id");
|
|
logger.info("│ ├── synchronize VAT Registrytion ID: " + vatID);
|
|
// update business partner
|
|
businessPartner.setItemValue("partner.vat", vatID);
|
|
documentService.saveByNewTransaction(businessPartner);
|
|
}
|
|
} catch (Exception e) {
|
|
// should not happen!
|
|
logger.info("│ ├── ⚠️ Failed to sync BusinessPartner : " + e.getMessage());
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
TradeParty tradeParty = new TradeParty(type);
|
|
|
|
// Name ist immer die erste Zeile
|
|
tradeParty.setName(businessPartner.getItemValueString("partner.name"));
|
|
tradeParty.setCountryId(businessPartner.getItemValueString("partner.country"));
|
|
tradeParty.setCityName(businessPartner.getItemValueString("partner.city"));
|
|
tradeParty.setPostcodeCode(businessPartner.getItemValueString("partner.zip"));
|
|
tradeParty.setStreetAddress(businessPartner.getItemValueString("partner.address"));
|
|
|
|
if ("buyer".equals(type)) {
|
|
tradeParty.setVatNumber(businessPartner.getItemValueString("partner.vat"));
|
|
// Update or create NrKlienta element directly under Podmiot2
|
|
Element podmiot2 = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Podmiot2");
|
|
model.updateElementValue(podmiot2, EInvoiceNS.KSEF, "NrKlienta",
|
|
businessPartner.getItemValueString("dbtr.number"));
|
|
|
|
}
|
|
|
|
return tradeParty;
|
|
}
|
|
|
|
/**
|
|
* 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("numpos"));
|
|
|
|
tradeLineItem.setName(orderItem.getItemValueString("datev.text"));
|
|
tradeLineItem.setQuantity(1);
|
|
tradeLineItem.setNetPrice(orderItem.getItemValueDouble("datev.umsatz"));
|
|
tradeLineItem.setTotal(orderItem.getItemValueDouble("datev.umsatz"));
|
|
// tradeLineItem.setTaxRate(orderItem.getItemValueDouble("vat"));
|
|
|
|
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 = "ksef.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<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;
|
|
}
|
|
|
|
} |