From 6f76b367e79a6bea94d9cf43ab97fd95289ad46b Mon Sep 17 00:00:00 2001 From: Ralph Soika Date: Tue, 28 Apr 2026 16:44:10 +0200 Subject: [PATCH] update --- .../einvoice/KSeFAdapter.java | 143 ++++++++++++++++-- .../xml/CargosoftXMLInvoiceImportService.java | 16 +- workflow/usa/rechnungseingang-usa-1.0.1.bpmn | 68 ++++----- 3 files changed, 173 insertions(+), 54 deletions(-) diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapter.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapter.java index 57e7794..cccf00b 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapter.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapter.java @@ -8,14 +8,21 @@ import java.text.NumberFormat; 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 javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; import org.imixs.archive.core.SnapshotService; import org.imixs.einvoice.EInvoiceFormatException; @@ -35,11 +42,14 @@ import org.imixs.workflow.exceptions.QueryException; import org.imixs.workflow.util.XMLParser; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; 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; @@ -162,6 +172,13 @@ public class KSeFAdapter implements SignalAdapter { if (workitem.getItemValueDate("invoice.performancedate") == null) { syncPerformanceDate(workitem); } + + // CargoSoft VAT code per child item - resync from Cargosoft XML if any + // child item is missing it (legacy data created before the import was + // extended to capture cargosoft.vat.code) + if (isAnyChildMissingVatCode(workitem)) { + syncCargosoftVatCodes(workitem); + } ((EInvoiceModelKSeF) model) .setPerformanceDateTime(workitem.getItemValueLocalDate("invoice.performancedate")); @@ -273,6 +290,120 @@ public class KSeFAdapter implements SignalAdapter { } } + /** + * Returns {@code true} if at least one entry of {@code _childitems} does + * not yet have the item {@code cargosoft.vat.code} populated. This can + * happen for legacy workitems that were imported before the Cargosoft + * import was extended to capture the VAT code per row. + */ + private boolean isAnyChildMissingVatCode(ItemCollection workitem) { + List childItems = InvoiceUtil.explodeChildList(workitem, "_childitems"); + if (childItems == null || childItems.isEmpty()) { + return false; + } + for (ItemCollection child : childItems) { + if (child.getItemValueString("cargosoft.vat.code").isBlank()) { + return true; + } + } + return false; + } + + /** + * Resync of the {@code cargosoft.vat.code} item on each child of + * {@code _childitems} from the original Cargosoft import XML. + *

+ * Strategy: parse all {@code } elements of the source XML in + * document order, skipping rows whose {@code BillingCode} is + * {@code "WÄHRUNG"} or {@code "KURS"} (special non-position rows). The + * remaining rows are paired with the {@code _childitems} entries by their + * one-based position number ({@code numpos}). Each child item without a + * VAT code receives the value extracted from its matching XML row. + *

+ * Rows where the VAT code cannot be extracted are silently skipped here; + * the {@link com.alexanderlogistics.einvoice.KSeFInvoiceLineBuilder} will + * later fail with a {@link PluginException} for that specific position, + * which is the desired behaviour. + */ + private void syncCargosoftVatCodes(ItemCollection workitem) throws PluginException { + + try { + logger.info("----Resync cargosoft.vat.code...."); + + FileData cargoXML = snapshotService.getWorkItemFile(workitem.getUniqueID(), + workitem.getItemValueString("cargosoft.import.filename")); + if (cargoXML == null) { + logger.warning("│ ├── ⚠️ Cargosoft source XML not found - cannot resync vat codes"); + return; + } + + InputStream inputStream = new ByteArrayInputStream(cargoXML.getContent()); + InputSource inputSource = new InputSource(inputStream); + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document doc = documentBuilder.parse(inputSource); + + // Build a map: numpos (1-based position number) -> vatCode + // by iterating the InvoiceRow elements in document order, skipping + // the special rows for currency / exchange-rate metadata. + XPath xPath = XPathFactory.newInstance().newXPath(); + XPathExpression billingCodeExpr = xPath.compile("BillingCode/Codes/Code"); + XPathExpression vatCodeExpr = xPath.compile( + "InvoiceAmount/VATInformation/VAT/Codes/Code[@Type='cs']/text()"); + + Map vatCodeByPos = new HashMap<>(); + NodeList rowList = doc.getElementsByTagName("InvoiceRow"); + int posCounter = 0; + for (int i = 0; i < rowList.getLength(); i++) { + Node rowNode = rowList.item(i); + if (rowNode.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + String billingCode = (String) billingCodeExpr.evaluate(rowNode, XPathConstants.STRING); + if ("WÄHRUNG".equals(billingCode) || "KURS".equals(billingCode)) { + continue; + } + posCounter++; + + String vatCode = (String) vatCodeExpr.evaluate(rowNode, XPathConstants.STRING); + if (vatCode != null && !vatCode.isBlank()) { + vatCodeByPos.put(posCounter, vatCode); + } + } + + // Patch each child item that is still missing the vat code, + // matching by numpos. Re-implode the list at the end. + List childItems = InvoiceUtil.explodeChildList(workitem, "_childitems"); + boolean changed = false; + for (ItemCollection child : childItems) { + if (!child.getItemValueString("cargosoft.vat.code").isBlank()) { + continue; + } + int numpos; + try { + numpos = Integer.parseInt(child.getItemValueString("numpos")); + } catch (NumberFormatException nfe) { + logger.warning("│ ├── ⚠️ child item without numpos - cannot resync vat code"); + continue; + } + String vatCode = vatCodeByPos.get(numpos); + if (vatCode != null) { + child.setItemValue("cargosoft.vat.code", vatCode); + changed = true; + logger.info("│ ├── resynced numpos=" + numpos + " cargosoft.vat.code=" + vatCode); + } else { + logger.warning("│ ├── ⚠️ no matching InvoiceRow for numpos=" + numpos); + } + } + if (changed) { + InvoiceUtil.implodeChildList(workitem, childItems); + } + + } catch (ParserConfigurationException | SAXException | IOException | XPathExpressionException e) { + throw new PluginException(CargosoftXMLInvoiceImportService.class.getName(), "XML_ERROR", + e.getMessage()); + } + } + /** * Returns {@code true} if the workitem represents a correction invoice. */ @@ -301,13 +432,7 @@ public class KSeFAdapter implements SignalAdapter { "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. - */ + // Migration: resync partner.vat from legacy cargosoftkreditor object if missing if (businessPartner.getItemValueString("partner.vat").isEmpty()) { logger.info("│ ├── BusinessPartner " + partnerID + " does not provide vat-id - try to migrate...."); String dNumber = businessPartner.getItemValueString("dbtr.number"); @@ -329,8 +454,6 @@ public class KSeFAdapter implements SignalAdapter { } 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")); @@ -338,10 +461,8 @@ public class KSeFAdapter implements SignalAdapter { tradeParty.setStreetAddress(businessPartner.getItemValueString("partner.address")); if ("buyer".equals(type)) { - // if we do NOT have a partner.vat we can not upload the invoice! String partnerVAT = businessPartner.getItemValueString("partner.vat"); if (partnerVAT.isBlank()) { - // Just a warning logger.warning("│ ├── ⚠️ Business Partner ID '" + partnerID + "' does not contain a VAT ID !"); } workitem.setItemValue("partner.vat", partnerVAT); diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/xml/CargosoftXMLInvoiceImportService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/xml/CargosoftXMLInvoiceImportService.java index fbecb63..6ae1d73 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/xml/CargosoftXMLInvoiceImportService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/xml/CargosoftXMLInvoiceImportService.java @@ -618,8 +618,11 @@ public class CargosoftXMLInvoiceImportService { XPathExpression netAmountValueExpr = xPath.compile("InvoiceAmount/NetAmount/Amount/Value/text()"); XPathExpression netAmountExchangeRateExpr = xPath .compile("InvoiceAmount/NetAmount/Amount/ExchangeRate/text()"); - XPathExpression vatInformationExpr = xPath - .compile("InvoiceAmount/VATInformation/VAT/VATRate/text()"); + + XPathExpression vatInformationExpr = xPath.compile("InvoiceAmount/VATInformation/VAT/VATRate/text()"); + XPathExpression vatCodeTypeExpr = xPath + .compile("InvoiceAmount/VATInformation/VAT/Codes/Code[@Type='cs']/text()"); + XPathExpression netActivityTypeExpr = xPath.compile("ActivityType/Codes/Code[@Type='cs']/text()"); NodeList rowList = doc.getElementsByTagName("InvoiceRow"); @@ -649,7 +652,14 @@ public class CargosoftXMLInvoiceImportService { String vatRateText = (String) vatInformationExpr.evaluate(rowNode, XPathConstants.STRING); childItemCol.setItemValue("datev.vatrate", Double.valueOf(vatRateText)); } catch (Exception e) { - // no op + logger.warning("Unable to pase cs vat rate: " + e.getMessage()); + } + // get VAT Code + try { + String vatCodeText = (String) vatCodeTypeExpr.evaluate(rowNode, XPathConstants.STRING); + childItemCol.setItemValue("cargosoft.vat.code", vatCodeText); + } catch (Exception e) { + logger.warning("Unable to pase cs vat code: " + e.getMessage()); } // speichere die Position in invoice.positions zur suche nach dem diff --git a/workflow/usa/rechnungseingang-usa-1.0.1.bpmn b/workflow/usa/rechnungseingang-usa-1.0.1.bpmn index 0a92204..3c39fa7 100644 --- a/workflow/usa/rechnungseingang-usa-1.0.1.bpmn +++ b/workflow/usa/rechnungseingang-usa-1.0.1.bpmn @@ -901,14 +901,14 @@ if ( b>0 && (parseFloat(a)!=parseFloat(b)) ) { false - + @@ -1954,14 +1954,14 @@ result.isValid=true; false - + @@ -3359,14 +3359,14 @@ Betrag: _amount (Brutto € _amount_brutto false - + @@ -3654,14 +3654,14 @@ result.isValid=true; false - + @@ -3827,14 +3827,14 @@ result.isValid=true; false - + @@ -4265,14 +4265,14 @@ result.isValid=true; false - + @@ -5597,15 +5597,15 @@ Export erfolgt über eine asynchrone Verarbeitung im Modell 'invoice-export']]>< - + - + - + - + @@ -5744,9 +5744,9 @@ Export erfolgt über eine asynchrone Verarbeitung im Modell 'invoice-export']]>< - + - + @@ -6172,28 +6172,22 @@ Export erfolgt über eine asynchrone Verarbeitung im Modell 'invoice-export']]>< - - - - + + - - + + - - - - - - - + + + @@ -6254,8 +6248,8 @@ Export erfolgt über eine asynchrone Verarbeitung im Modell 'invoice-export']]>< - - + + @@ -6339,13 +6333,7 @@ Export erfolgt über eine asynchrone Verarbeitung im Modell 'invoice-export']]>< - - - - - - - +