From 3f15b3a11b192e06575f09e6aaf1649796542157 Mon Sep 17 00:00:00 2001 From: Ralph Soika Date: Fri, 24 Jul 2026 11:04:22 +0200 Subject: [PATCH] anapssungen der einvoice --- .../com/alexanderlogistics/InvoiceUtil.java | 6 + .../einvoice/EInvoiceAdapter.java | 10 +- .../einvoice/KSeFAdapterDeprecated.java | 600 ------------------ .../einvoice/AGLEInvoiceAdapterTest.java | 65 ++ .../einvoice/output/invoice-mixed-rates.xml | 188 ++++++ .../ksef/output/invoice-eu-service-np.xml | 4 +- .../ksef/output/invoice-mixed-rates.xml | 4 +- .../invoice-multiple-zero-rate-codes.xml | 2 +- .../test/resources/ksef/output/invoice-np.xml | 4 +- .../resources/ksef/output/invoice-npt.xml | 4 +- .../resources/ksef/output/invoice-simple.xml | 4 +- .../ksef/output/test-korrekturrechnung.xml | 12 +- 12 files changed, 281 insertions(+), 622 deletions(-) delete mode 100644 office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapterDeprecated.java create mode 100644 office-alexander-logistics-app/src/test/resources/einvoice/output/invoice-mixed-rates.xml diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java index cd1a0fc..dc3a26a 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java @@ -38,6 +38,12 @@ public class InvoiceUtil { return bd.doubleValue(); } + public static BigDecimal roundBigDecimal(double value) { + BigDecimal bd = BigDecimal.valueOf(value); + bd = bd.setScale(2, RoundingMode.HALF_UP); + return bd; + } + /** * Rounds a Flat Value * diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java index 94b4374..a8c20aa 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java @@ -224,9 +224,9 @@ public class EInvoiceAdapter implements SignalAdapter { // Update Rechnungssummen... if (workitem.getItemValueDouble("invoice.total.tax") > 0) { // wir haben eine Steuer! - model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.total") + model.setTaxTotalAmount(InvoiceUtil.roundBigDecimal(workitem.getItemValueDouble("invoice.total") - workitem.getItemValueDouble("invoice.total.net"))); - model.setTaxRate(workitem.getItemValueDouble("invoice.total.tax")); + model.setTaxRate(InvoiceUtil.roundBigDecimal(workitem.getItemValueDouble("invoice.total.tax"))); } // Update Invoice Items @@ -234,13 +234,13 @@ public class EInvoiceAdapter implements SignalAdapter { double lineTotalAmount = 0.00; for (ItemCollection invoiceItem : invoiceItems) { TradeLineItem tradeLineItem = buildTradeLineItem(invoiceItem); - model.setTradeLineItem(tradeLineItem); + model.addTradeLineItem(tradeLineItem); lineTotalAmount = lineTotalAmount + tradeLineItem.getTotal(); } // // Summenbildung - model.setNetTotalAmount(workitem.getItemValueDouble("invoice.total.net")); - model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.total")); + model.setNetTotalAmount(InvoiceUtil.roundBigDecimal(workitem.getItemValueDouble("invoice.total.net"))); + model.setGrandTotalAmount(InvoiceUtil.roundBigDecimal(workitem.getItemValueDouble("invoice.total"))); // date model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date")); diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapterDeprecated.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapterDeprecated.java deleted file mode 100644 index 68cdcd7..0000000 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapterDeprecated.java +++ /dev/null @@ -1,600 +0,0 @@ -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
-            
-            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 invoiceItems = InvoiceUtil.explodeChildList(workitem, "_childitems"); - double lineTotalAmount = 0.00; - int pos = 1; - for (ItemCollection invoiceItem : invoiceItems) { - TradeLineItem tradeLineItem = buildTradeLineItem(invoiceItem, pos); - model.setTradeLineItem(tradeLineItem); - lineTotalAmount = lineTotalAmount + tradeLineItem.getTotal(); - pos++; - } - - // Summenbildung - /* - * Field mapping: - *

- */ - 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 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)) { - // 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 !"); - // throw new PluginException(this.getClass().getName(), BUSINESSPARTNER_ERROR, - // "Business Partner ID '" + partnerID + "' does not contain a VAT ID !"); - } - workitem.setItemValue("partner.vat", partnerVAT); - tradeParty.setVatNumber(partnerVAT); - // 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! - * - * # 16.04.2026 - * Laut Herrn Grzegorz Grzelczyk dürfen in den Rechnungspositionen nur netto - * werte eingetragen sein. Siehe buildTradeLineItem() - * - * Des weiteren soll anstatt datev.text (pos nummer) der original billing text - * ausgewiesen werden - */ - private TradeLineItem buildTradeLineItem(ItemCollection orderItem, int pos) { - if (orderItem == null) { - return null; - } - - // TradeLineItem tradeLineItem = new - // TradeLineItem(orderItem.getItemValueString("numpos")); - // We do not trust the item 'numPos' here because it can be empty for new - // invoices! - TradeLineItem tradeLineItem = new TradeLineItem(pos + ""); - - // Herrn Grzegorz Grzelczyk möchte hier BillingText - // tradeLineItem.setName(orderItem.getItemValueString("datev.text")); - // tradeLineItem.setName(orderItem.getItemValueString("billingtext")); - // Herr Grzelczyk will das alle billing texte aus der liste, verkettet - // ausgegeben werden. - List billingTexts = orderItem.getItemValue("billingtext"); - String billingTextName = billingTexts == null - ? "" - : String.join(", ", billingTexts); - final int MAX_LENGTH = 512; - if (billingTextName.length() > MAX_LENGTH) { - billingTextName = billingTextName.substring(0, MAX_LENGTH); - } - tradeLineItem.setName(billingTextName); - - tradeLineItem.setQuantity(1); - - double vat = orderItem.getItemValueDouble("datev.vatrate"); // 23.00 - double netto = orderItem.getItemValueDouble("datev.umsatz"); // 970.00 - - // # 16.04.2026 - // Der total bezieht sich laut Herrn Grzegorz Grzelczyk ausschließlich auf den - // Nettowert x Menge. - // Der Brutto Wert ist hier falsch! Da die Menge immer 1 ist ist das beide male - // der selbe wert - // WRONG: tradeLineItem.setTotal(brutto); - - // double brutto = netto * (1 + (vat / 100)); - // double steuer = brutto - netto; - tradeLineItem.setTaxRate(vat); - tradeLineItem.setNetPrice(netto); - tradeLineItem.setTotal(netto); // menge x netto - - 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; - } - -} \ No newline at end of file diff --git a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/einvoice/AGLEInvoiceAdapterTest.java b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/einvoice/AGLEInvoiceAdapterTest.java index 5591e89..56d62f7 100644 --- a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/einvoice/AGLEInvoiceAdapterTest.java +++ b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/einvoice/AGLEInvoiceAdapterTest.java @@ -139,6 +139,71 @@ public class AGLEInvoiceAdapterTest { writeOutputToResources(xmlTemplate, "einvoice-simple.xml"); } + /** + * Mixed tax rates: 0% (Art. 83) + 23% on the same invoice in PLN. + *

+ * This is the case from the tax advisor mapping document (invoice 7471) + * where the previous implementation incorrectly aggregated all amounts + * into a single field. Expected output: P_13_1+P_14_1 (for the 23% + * position) AND P_13_6_1 (for the 0% position) populated separately. + */ + @Test + @DisplayName("Test Mixed Tax Rates (PLN, 0% KR + 23%)") + public void testMixedTaxRates() throws Exception { + logger.info("==> Test: Mixed Tax Rates"); + + ItemCollection workitem = new ItemCollection(); + workitem.setItemValue("invoice.number", "FV/2025/7471"); + workitem.setItemValue("invoice.date", LocalDate.of(2025, 2, 10)); + workitem.setItemValue("invoice.duedate", LocalDate.of(2025, 3, 10)); + workitem.setItemValue("invoice.currency", "PLN"); + // workitem.setItemValue("invoice.total.net", 18000.00); + // workitem.setItemValue("invoice.total.tax", 23.0); + workitem.setItemValue("invoice.total", 18000.00); + + workitem.setItemValue("invoice.correction", "false"); + workitem.setItemValue("invoice.CorrectionInvoiceNumber", ""); + workitem.setItemValue("partner.id", "BP-001"); + workitem.setItemValue("partner.vat", "PL1234567890"); + workitem.setItemValue("invoice.performancedate", new Date()); + + List childItems = new ArrayList<>(); + + // Position 1: 0% domestic (Art. 83 sec. 1 no. 19 - forwarding for export) + ItemCollection lineItem1 = new ItemCollection() + .setItemValue("numpos", "1") + .setItemValue("datev.text", "EX-GDY-2602-001") + .setItemValue("billingtext", "Forwarding service for export shipment") + .setItemValue("datev.umsatz", 14040.00) + .setItemValue("datev.vatrate", 0.0) + .setItemValue("cargosoft.vat.code", "0%E"); + childItems.add(lineItem1.getAllItems()); + + // Position 2: 23% domestic taxable + ItemCollection lineItem2 = new ItemCollection() + .setItemValue("numpos", "2") + .setItemValue("datev.text", "Domestic transport handling") + .setItemValue("billingtext", "Krajowa obsługa transportu") + .setItemValue("datev.umsatz", 3960.00) + .setItemValue("datev.vatrate", 23.0) + .setItemValue("cargosoft.vat.code", "23"); + childItems.add(lineItem2.getAllItems()); + + workitem.setItemValue("_childitems", childItems); + + when(businessPartnerService.getBusinessPartnerByID("BP-001")) + .thenReturn(businessPartner); + + FileData xmlTemplate = loadTemplateFromResources(TEMPLATE_FILE); + + adapter.updateEInvoice(xmlTemplate, workitem); + + assertNotNull(xmlTemplate.getContent(), "XML content should not be null after update"); + assertTrue(xmlTemplate.getContent().length > 0, "XML content should not be empty"); + + writeOutputToResources(xmlTemplate, "invoice-mixed-rates.xml"); + } + // ── Helper methods ────────────────────────────────────────────── /** diff --git a/office-alexander-logistics-app/src/test/resources/einvoice/output/invoice-mixed-rates.xml b/office-alexander-logistics-app/src/test/resources/einvoice/output/invoice-mixed-rates.xml new file mode 100644 index 0000000..e357e10 --- /dev/null +++ b/office-alexander-logistics-app/src/test/resources/einvoice/output/invoice-mixed-rates.xml @@ -0,0 +1,188 @@ + + + + + urn:cen.eu:en16931:2017 + + + + FV/2025/7471 + 380 + + 20250210 + + + Payment Instructions: + + + Please ensure that the payment reference includes the invoice number, BL or + AWB number, container or shipment number and place of loading and discharge. + The full invoice amount must be transferred without any deductions and all bank charges + must be covered by the sender. + Otherwise, the beneficiary’s bank will be unable to process the incoming payment. + + + Alexander Global Logistics + + + Numer NIP: PL9552521552 + + + + + + 1 + + + EX-GDY-2602-001 + + + + 14040.0 + + + 14040.0 + + + + 1.0 + + + + VAT + Z + 0.0 + + + 14040.0 + + + + + + 2 + + + Domestic transport handling + + + + 3960.0 + + + 3960.0 + + + + 1.0 + + + + VAT + S + 23.0 + + + 3960.0 + + + + + n/a + + Alexander Global Logistics + + n/a + + +49 421 566 46 0 + + + info@alexander-logistics.com + + + + 70-660 + Gdanska 36 + Szczecin + PL + + + info@alexander-logistics.com + + + PL9552521552 + + + + Test Sp. z o.o. + + 00-001 + ul. Testowa 1 + Warszawa + PL + + + PL1234567890 + + + + + + Test Sp. z o.o. + + 00-001 + ul. Testowa 1 + Warszawa + PL + + + + + PLN + + 30 + + PL79116022020000000654306674 + + + BIGBPLPWXXX + + + + 0.00 + VAT + 14040.0 + Z + 0.0 + + + 910.80 + VAT + 3960.0 + S + 23.0 + + + Payment due by 10.03.2025 + + 20250310 + + + + 18000.00 + 0.00 + 0.00 + 18000.00 + 910.80 + 18910.80 + 0.00 + 18910.80 + + + + diff --git a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-eu-service-np.xml b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-eu-service-np.xml index e4e188d..87c201b 100644 --- a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-eu-service-np.xml +++ b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-eu-service-np.xml @@ -6,7 +6,7 @@ FA 3 - 2026-05-28T19:18:53.815720274Z + 2026-07-24T10:11:51.410517Z Imixs eInvoice @@ -42,7 +42,7 @@ 2025-02-10 Szczecin FV/2025/7470 - 2026-05-28 + 2026-07-24 4620.00 4620.00 0.2326 diff --git a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-mixed-rates.xml b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-mixed-rates.xml index b0f0aca..93c9064 100644 --- a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-mixed-rates.xml +++ b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-mixed-rates.xml @@ -6,7 +6,7 @@ FA 3 - 2026-05-28T19:18:53.906736962Z + 2026-07-24T10:11:51.547969Z Imixs eInvoice @@ -42,7 +42,7 @@ 2025-02-10 Szczecin FV/2025/7471 - 2026-05-28 + 2026-07-24 3960.00 910.80 14040.00 diff --git a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-multiple-zero-rate-codes.xml b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-multiple-zero-rate-codes.xml index 2d90179..50f57ad 100644 --- a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-multiple-zero-rate-codes.xml +++ b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-multiple-zero-rate-codes.xml @@ -6,7 +6,7 @@ FA 3 - 2026-05-28T19:18:53.629147269Z + 2026-07-24T10:11:51.000225Z Imixs eInvoice diff --git a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-np.xml b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-np.xml index bd9cd9a..0a24a79 100644 --- a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-np.xml +++ b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-np.xml @@ -6,7 +6,7 @@ FA 3 - 2026-05-28T19:18:53.885083580Z + 2026-07-24T10:11:51.517086Z Imixs eInvoice @@ -42,7 +42,7 @@ 2025-02-10 Szczecin FV/2025/001 - 2026-05-28 + 2026-07-24 10000.00 10000.00 diff --git a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-npt.xml b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-npt.xml index 7414f47..b46b139 100644 --- a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-npt.xml +++ b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-npt.xml @@ -6,7 +6,7 @@ FA 3 - 2026-07-23T14:06:32.328007Z + 2026-07-24T10:11:51.468487Z Imixs eInvoice @@ -42,7 +42,7 @@ 2025-02-10 Szczecin FV/2025/001 - 2026-07-23 + 2026-07-24 10000.00 10000.00 diff --git a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-simple.xml b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-simple.xml index 1dc8f47..6be9ba5 100644 --- a/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-simple.xml +++ b/office-alexander-logistics-app/src/test/resources/ksef/output/invoice-simple.xml @@ -6,7 +6,7 @@ FA 3 - 2026-07-23T15:09:14.896034Z + 2026-07-24T10:11:51.644379Z Imixs eInvoice @@ -42,7 +42,7 @@ 2025-02-10 Szczecin FV/2025/001 - 2026-07-23 + 2026-07-24 10000.00 2300.00 12300.00 diff --git a/office-alexander-logistics-app/src/test/resources/ksef/output/test-korrekturrechnung.xml b/office-alexander-logistics-app/src/test/resources/ksef/output/test-korrekturrechnung.xml index 287b42a..3a82ed0 100644 --- a/office-alexander-logistics-app/src/test/resources/ksef/output/test-korrekturrechnung.xml +++ b/office-alexander-logistics-app/src/test/resources/ksef/output/test-korrekturrechnung.xml @@ -6,7 +6,7 @@ FA 3 - 2026-05-28T19:18:53.925385306Z + 2026-07-24T10:11:51.591043Z Imixs eInvoice @@ -42,7 +42,7 @@ 2025-02-10 Szczecin FV/2025/001 - 2026-05-28 + 2026-07-24 -900.00 -207.00 -48.15 @@ -75,10 +75,10 @@ usługa spedycyjna / transport w relacji, PL63 - CZ43 szt. 1.00 - -900.00 - -900.00 - -1107.00 - -207.00 + 900.00 + 900.00 + 1107.00 + 207.00 23