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: * *
 * {@code
        
            textblock-ref
            
            true
         

   }
 * 
* *

* 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. * *

* 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 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 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 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; } }