office-alexander-logistics/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapter.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;
}
}