This commit is contained in:
Ralph Soika 2026-04-28 16:44:10 +02:00
parent edacbbdd6d
commit 6f76b367e7
3 changed files with 173 additions and 54 deletions

View file

@ -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<ItemCollection> 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.
* <p>
* Strategy: parse all {@code <InvoiceRow>} 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.
* <p>
* 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<Integer, String> 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<ItemCollection> 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);

View file

@ -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

View file

@ -901,14 +901,14 @@ if ( b>0 && (parseFloat(a)!=parseFloat(b)) ) {
<imixs:item name="keyupdateacl" type="xs:boolean">
<imixs:value>false</imixs:value>
</imixs:item>
<imixs:item name="txtbusinessrule" type="CDATA">
<imixs:item name="txtbusinessrule" type="xs:string">
<imixs:value><![CDATA[var result={};
var refField="txtcomment";
result.isValid=true;
if ( ( workitem.get(refField) == null || ''==workitem.get(refField)[0]) ) {
result.isValid=false;
result.errorMessage='Bitte geben Sie einen Kommentar ein.';
result.errorMessage='Please enter a comment.';
}]]></imixs:value>
</imixs:item>
</bpmn2:extensionElements>
@ -1954,14 +1954,14 @@ result.isValid=true;
<imixs:item name="keyupdateacl" type="xs:boolean">
<imixs:value>false</imixs:value>
</imixs:item>
<imixs:item name="txtbusinessrule" type="CDATA">
<imixs:item name="txtbusinessrule" type="xs:string">
<imixs:value><![CDATA[var result={};
var refField="txtcomment";
result.isValid=true;
if ( ( workitem.get(refField) == null || ''==workitem.get(refField)[0]) ) {
result.isValid=false;
result.errorMessage='Bitte geben Sie einen Kommentar ein.';
result.errorMessage='Please enter a comment.';
}]]></imixs:value>
</imixs:item>
</bpmn2:extensionElements>
@ -3359,14 +3359,14 @@ Betrag: <itemvalue>_amount</itemvalue> (Brutto € <itemvalue>_amount_brutto</it
<imixs:item name="keyupdateacl" type="xs:boolean">
<imixs:value>false</imixs:value>
</imixs:item>
<imixs:item name="txtbusinessrule" type="CDATA">
<imixs:item name="txtbusinessrule" type="xs:string">
<imixs:value><![CDATA[var result={};
var refField="txtcomment";
result.isValid=true;
if ( ( workitem.get(refField) == null || ''==workitem.get(refField)[0]) ) {
result.isValid=false;
result.errorMessage='Bitte geben Sie einen Kommentar ein.';
result.errorMessage='Please enter a comment.';
}]]></imixs:value>
</imixs:item>
</bpmn2:extensionElements>
@ -3654,14 +3654,14 @@ result.isValid=true;
<imixs:item name="keyupdateacl" type="xs:boolean">
<imixs:value>false</imixs:value>
</imixs:item>
<imixs:item name="txtbusinessrule" type="CDATA">
<imixs:item name="txtbusinessrule" type="xs:string">
<imixs:value><![CDATA[var result={};
var refField="txtcomment";
result.isValid=true;
if ( ( workitem.get(refField) == null || ''==workitem.get(refField)[0]) ) {
result.isValid=false;
result.errorMessage='Bitte geben Sie einen Kommentar ein.';
result.errorMessage='Please enter a comment.';
}]]></imixs:value>
</imixs:item>
</bpmn2:extensionElements>
@ -3827,14 +3827,14 @@ result.isValid=true;
<imixs:item name="keyupdateacl" type="xs:boolean">
<imixs:value>false</imixs:value>
</imixs:item>
<imixs:item name="txtbusinessrule" type="CDATA">
<imixs:item name="txtbusinessrule" type="xs:string">
<imixs:value><![CDATA[var result={};
var refField="txtcomment";
result.isValid=true;
if ( ( workitem.get(refField) == null || ''==workitem.get(refField)[0]) ) {
result.isValid=false;
result.errorMessage='Bitte geben Sie einen Kommentar ein.';
result.errorMessage='Please enter a comment.';
}]]></imixs:value>
</imixs:item>
</bpmn2:extensionElements>
@ -4265,14 +4265,14 @@ result.isValid=true;
<imixs:item name="keyupdateacl" type="xs:boolean">
<imixs:value>false</imixs:value>
</imixs:item>
<imixs:item name="txtbusinessrule" type="CDATA">
<imixs:item name="txtbusinessrule" type="xs:string">
<imixs:value><![CDATA[var result={};
var refField="txtcomment";
result.isValid=true;
if ( ( workitem.get(refField) == null || ''==workitem.get(refField)[0]) ) {
result.isValid=false;
result.errorMessage='Bitte geben Sie einen Kommentar ein.';
result.errorMessage='Please enter a comment.';
}]]></imixs:value>
</imixs:item>
</bpmn2:extensionElements>
@ -5597,15 +5597,15 @@ Export erfolgt über eine asynchrone Verarbeitung im Modell 'invoice-export']]><
<dc:Bounds height="50.0" width="110.0" x="2130.0" y="1600.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="IntermediateCatchEvent_5" id="BPMNShape_IntermediateCatchEvent_10">
<dc:Bounds height="36.0" width="36.0" x="2317.0" y="1677.0"/>
<dc:Bounds height="36.0" width="36.0" x="2327.0" y="1607.0"/>
<bpmndi:BPMNLabel id="BPMNLabel_51" labelStyle="BPMNLabelStyle_1">
<dc:Bounds height="20.0" width="100.0" x="2286.5" y="1719.0"/>
<dc:Bounds height="20.0" width="100.0" x="2296.5" y="1649.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="IntermediateCatchEvent_34" id="BPMNShape_IntermediateCatchEvent_39">
<dc:Bounds height="36.0" width="36.0" x="2177.0" y="1767.0"/>
<dc:Bounds height="36.0" width="36.0" x="2167.0" y="1707.0"/>
<bpmndi:BPMNLabel id="BPMNLabel_171" labelStyle="BPMNLabelStyle_1">
<dc:Bounds height="20.0" width="100.0" x="2147.0" y="1809.0"/>
<dc:Bounds height="20.0" width="100.0" x="2137.0" y="1749.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="IntermediateCatchEvent_35" id="BPMNShape_IntermediateCatchEvent_40">
@ -5744,9 +5744,9 @@ Export erfolgt über eine asynchrone Verarbeitung im Modell 'invoice-export']]><
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="IntermediateCatchEvent_59" id="BPMNShape_IntermediateCatchEvent_64">
<dc:Bounds height="36.0" width="36.0" x="2177.0" y="1507.0"/>
<dc:Bounds height="36.0" width="36.0" x="2167.0" y="1507.0"/>
<bpmndi:BPMNLabel id="BPMNLabel_270">
<dc:Bounds height="20.0" width="100.0" x="2147.5" y="1547.0"/>
<dc:Bounds height="20.0" width="100.0" x="2137.5" y="1547.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="IntermediateCatchEvent_60" id="BPMNShape_IntermediateCatchEvent_65">
@ -6172,28 +6172,22 @@ Export erfolgt über eine asynchrone Verarbeitung im Modell 'invoice-export']]><
<bpmndi:BPMNLabel id="BPMNLabel_169"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="SequenceFlow_57" id="BPMNEdge_SequenceFlow_58" sourceElement="BPMNShape_Task_17" targetElement="BPMNShape_IntermediateCatchEvent_10">
<di:waypoint x="2240.0" y="1622.0"/>
<di:waypoint x="2284.0" y="1622.0"/>
<di:waypoint x="2284.0" y="1695.0"/>
<di:waypoint x="2317.0" y="1695.0"/>
<di:waypoint x="2240.0" y="1625.0"/>
<di:waypoint x="2327.0" y="1625.0"/>
<bpmndi:BPMNLabel id="BPMNLabel_129"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="SequenceFlow_58" id="BPMNEdge_SequenceFlow_59" sourceElement="BPMNShape_IntermediateCatchEvent_10" targetElement="BPMNShape_Task_11">
<di:waypoint x="2335.0" y="1713.0"/>
<di:waypoint x="2335.0" y="1807.0"/>
<di:waypoint x="2345.0" y="1643.0"/>
<di:waypoint x="2345.0" y="1807.0"/>
<di:waypoint x="2415.0" y="1807.0"/>
<di:waypoint x="2415.0" y="1815.0"/>
<di:waypoint x="2420.0" y="1815.0"/>
<bpmndi:BPMNLabel id="BPMNLabel_170"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="SequenceFlow_79" id="BPMNEdge_SequenceFlow_80" sourceElement="BPMNShape_IntermediateCatchEvent_39" targetElement="BPMNShape_Task_17">
<di:waypoint x="2195.0" y="1767.0"/>
<di:waypoint x="2195.0" y="1704.0"/>
<di:waypoint x="2174.0" y="1704.0"/>
<di:waypoint x="2174.0" y="1568.0"/>
<di:waypoint x="2185.0" y="1568.0"/>
<di:waypoint x="2185.0" y="1598.0"/>
<di:waypoint x="2185.0" y="1600.0"/>
<di:waypoint x="2185.0" y="1707.0"/>
<di:waypoint x="2185.0" y="1679.0"/>
<di:waypoint x="2185.0" y="1650.0"/>
<bpmndi:BPMNLabel id="BPMNLabel_172"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="SequenceFlow_80" id="BPMNEdge_SequenceFlow_81" sourceElement="BPMNShape_IntermediateCatchEvent_40" targetElement="BPMNShape_IntermediateThrowEvent_1">
@ -6254,8 +6248,8 @@ Export erfolgt über eine asynchrone Verarbeitung im Modell 'invoice-export']]><
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="SequenceFlow_16" id="BPMNEdge_SequenceFlow_17" sourceElement="BPMNShape_EventBasedGateway_3" targetElement="BPMNShape_IntermediateCatchEvent_45">
<di:waypoint x="813.0" y="318.0"/>
<di:waypoint x="813.0" y="349.0"/>
<di:waypoint x="908.0" y="349.0"/>
<di:waypoint x="813.0" y="355.0"/>
<di:waypoint x="907.0" y="355.0"/>
<bpmndi:BPMNLabel id="BPMNLabel_183"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="SequenceFlow_85" id="BPMNEdge_SequenceFlow_86" sourceElement="BPMNShape_Task_16" targetElement="BPMNShape_EventBasedGateway_3">
@ -6339,13 +6333,7 @@ Export erfolgt über eine asynchrone Verarbeitung im Modell 'invoice-export']]><
<bpmndi:BPMNLabel id="BPMNLabel_269"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="SequenceFlow_124" id="BPMNEdge_SequenceFlow_125" sourceElement="BPMNShape_IntermediateCatchEvent_64" targetElement="BPMNShape_Task_17">
<di:waypoint x="2195.0" y="1543.0"/>
<di:waypoint x="2195.0" y="1544.0"/>
<di:waypoint x="2195.0" y="1567.0"/>
<di:waypoint x="2174.0" y="1567.0"/>
<di:waypoint x="2174.0" y="1518.0"/>
<di:waypoint x="2185.0" y="1518.0"/>
<di:waypoint x="2185.0" y="1598.0"/>
<di:waypoint x="2185.0" y="1543.0"/>
<di:waypoint x="2185.0" y="1600.0"/>
<bpmndi:BPMNLabel id="BPMNLabel_271"/>
</bpmndi:BPMNEdge>