package com.alexanderlogistics.einvoice; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.logging.Logger; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; 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.EInvoiceModelKSeF; 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.Document; import org.w3c.dom.Element; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.alexanderlogistics.BusinessPartnerService; import com.alexanderlogistics.InvoiceService; import com.alexanderlogistics.InvoiceUtil; import com.alexanderlogistics.xml.CargosoftXMLInvoiceImportService; 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
filename
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 KSeFAdapterDeprecated 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 API_ERROR = "API_ERROR";
public static final String BUSINESSPARTNER_ERROR = "BUSINESSPARTNER_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;
@Inject
InvoiceService invoiceService;
/**
* 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
*/
public 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"));
// Set Performance Date
if (workitem.getItemValueDate("invoice.performancedate") == null) {
// hilfs code um das invoice.performancedate nachträglich zu parsen
syncPerformanceDate(workitem);
}
((EInvoiceModelKSeF) model)
.setPerformanceDateTime(workitem.getItemValueLocalDate("invoice.performancedate"));
// Update Addresses
TradeParty billingAddress = buildAddress(workitem, "buyer", model);
model.setTradeParty(billingAddress);
// set Tax Type based on vatID - as a result taxType is 1=Poland 2=EU 3=Other
((EInvoiceModelKSeF) model).setTaxType(workitem.getItemValueString("partner.vat"));
workitem.setItemValue("invoice.tax.type", ((EInvoiceModelKSeF) model).getTaxType());
// Update Invoice Type (RodzajFaktury) -> VAT | KOR
Element elementFa = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Fa");
if (isKorrekturRechnung(workitem)) {
((EInvoiceModelKSeF) model).setRodzajFaktury("KOR");
logger.info("│ ├── invoice type=KOR");
// Set correction data (DaneFaKorygowanej) - required for KOR invoices
Element daneFaKorygowanej = model.findOrCreateChildNodeAfter(
elementFa, EInvoiceNS.KSEF, "DaneFaKorygowanej", "RodzajFaktury");
Date correctionInvoiceDate = workitem.getItemValueDate("invoice.date");
// try to lookup original invoice by invoice.CorrectionInvoiceNumber
ItemCollection correctionInvoice = invoiceService
.findOutboundInvoiceByNumber(workitem.getItemValueString("invoice.CorrectionInvoiceNumber"));
if (correctionInvoice != null) {
correctionInvoiceDate = correctionInvoice.getItemValueDate("invoice.date");
logger.info("│ ├── found original invoice, date: " + correctionInvoiceDate);
} else {
logger.info("│ ├── ⚠️ original invoice not found, using current date as fallback");
}
// Original invoice date (DataWystFaKorygowanej) - required
if (correctionInvoiceDate != null) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
String sCorrectionDate = formatter.format(correctionInvoiceDate);
model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF,
"DataWystFaKorygowanej", sCorrectionDate);
}
// Original invoice number (NrFaKorygowanej) - required
model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF,
"NrFaKorygowanej", workitem.getItemValueString("invoice.CorrectionInvoiceNumber"));
// NrKSeFN = 1 means the original invoice was issued outside KSeF
model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF, "NrKSeFN", "1");
} else {
// normale rechnung
((EInvoiceModelKSeF) model).setRodzajFaktury("VAT");
logger.info("│ ├── invoice type=VAT");
}
// set currency
model.updateElementValue(elementFa, EInvoiceNS.KSEF, "KodWaluty",
workitem.getItemValueString("invoice.currency"));
// Update Invoice Items
// Here we compute the pos number independent if the _childitems hold a posnum.
// This is because a new imported cargosoft invoice does not provide numPos
// items in the child list. So it is important to generate the pos numbers here
// one by one
List
*
*/
model.setNetTotalAmount(workitem.getItemValueDouble("invoice.total.net"));
// ?? has no function
model.setTaxRate(workitem.getItemValueDouble("invoice.total.tax"));
// Tax
/*
* P_14_1
*
* This call also generate P_14_1W bei fremdwährung
*/
model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.total")
- workitem.getItemValueDouble("invoice.total.net")));
/*
* If we have Foreign Currency we need to set P_14_1W in case we are tax type 1
*/
if (!"PLN".equals(workitem.getItemValueString("invoice.currency"))) {
if ("1".equals(((EInvoiceModelKSeF) model).getTaxType())) {
// compute rate
double rate = workitem.getItemValueDouble("invoice.rate");
logger.info("│ ├── rate=" + rate);
double totalTax = InvoiceUtil.round(workitem.getItemValueDouble("invoice.total.tax"));
logger.info("│ ├── total.tax=" + totalTax);
BigDecimal value = BigDecimal.valueOf(totalTax)
.divide(BigDecimal.valueOf(rate), 10, RoundingMode.HALF_UP);
// P_14_1W must come directly after P_14_1
logger.info("│ ├── P_14_1W=" + value);
Element element = model.findOrCreateChildNodeAfter(elementFa, EInvoiceNS.KSEF, "P_14_1W", "P_14_1");
element.setTextContent(value.setScale(2, RoundingMode.HALF_UP).toPlainString());
}
}
// Brutto
/*
* P_15
*/
model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.total"));
/*
* KursWalutyZ
*
* Für Rechnungen in Fremdwährung obligatorisch. Anzuwenden ist der NBP-
* Durchschnittskurs des Tages vor Entstehen der Steuerpflicht (Art. 31a
* UStG-PL).
* Dezimalformat mit Punkt.
*/
String currency = workitem.getItemValueString("invoice.currency");
if (!currency.isBlank() && !"PLN".equals(currency)) {
Element p15 = model.findChildNode(elementFa, EInvoiceNS.KSEF, "P_15");
if (p15 != null) {
Double rate = workitem.getItemValueDouble("invoice.rate");
logger.info("│ ├── set KursWalutyZ = " + rate);
Element element = model.findOrCreateChildNodeAfter(elementFa, EInvoiceNS.KSEF, "KursWalutyZ",
"P_15");
element.setTextContent(rate.toString());
}
}
// finally set the due date at the end of the XML tree
model.setDueDateTime(workitem.getItemValueLocalDate("invoice.duedate"));
/*
* 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);
}
}
/**
* Dies ist eine Hilfsmethode die nachträglich das invoice.performancedate aus
* dem Cargosoft XML ausliest
*
* @param workitem
* @throws PluginException
*/
private void syncPerformanceDate(ItemCollection workitem) throws PluginException {
if (!workitem.hasItem("invoice.performancedate")) {
try {
logger.info("----Resync cargosoft performancedate....");
DocumentBuilder documentBuilder;
// hole die XML Datei
FileData cargoXML = snapshotService.getWorkItemFile(workitem.getUniqueID(),
workitem.getItemValueString("cargosoft.import.filename"));
if (cargoXML != null) {
InputStream inputStream = new ByteArrayInputStream(cargoXML.getContent());
InputSource inputSource = new InputSource(inputStream);
documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = documentBuilder.parse(inputSource);
// Performance Date
CargosoftXMLInvoiceImportService.readXMLValue(doc,
"/Invoices/Invoice/InvoiceHeader/PerformanceDate",
workitem, "invoice.performancedate",
Date.class);
// falls keines gefunden wurde nehmen wir das invoice date
if (workitem.getItemValueDate("invoice.performancedate") == null) {
workitem.setItemValue("invoice.performancedate", workitem.getItemValueDate("invoice.date"));
}
}
} catch (ParserConfigurationException | SAXException | IOException e) {
throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(), "XML_ERROR",
e.getMessage());
}
}
}
/**
* 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(ItemCollection workitem, String type, EInvoiceModel model) throws PluginException {
String partnerID = workitem.getItemValueString("partner.id");
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