anapssungen der einvoice

This commit is contained in:
Ralph Soika 2026-07-24 11:04:22 +02:00
parent f758c4a6ac
commit 3f15b3a11b
12 changed files with 281 additions and 622 deletions

View file

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

View file

@ -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"));

View file

@ -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:
*
* <pre>
* {@code
<ksef name="create">
<textblock>textblock-ref</textblock>
<template>filename</template>
<debug>true</debug>
</ksef>
}
* </pre>
*
* <p>
* 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.
*
* <p>
* 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<ItemCollection> 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:
* <ul>
* <li>taxType "1" (Poland): P_13_1 (domestic VAT)</li>
* <li>taxType "2" (EU): P_13_6_2 (intra-community delivery, 0%)</li>
* <li>taxType "3" (Non-EU): P_13_6_3 (export, 0%)</li>
* </ul>
*/
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<ItemCollection> 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<String> 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<ItemCollection> 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;
}
}

View file

@ -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.
* <p>
* 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<Object> 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
/**

View file

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:a="urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:10"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>FV/2025/7471</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20250210</udt:DateTimeString>
</ram:IssueDateTime>
<ram:IncludedNote>
<ram:Content>Payment Instructions:</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>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 beneficiarys bank will be unable to process the incoming payment.</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Alexander Global Logistics</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Numer NIP: PL9552521552</ram:Content>
</ram:IncludedNote>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>1</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>EX-GDY-2602-001</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:GrossPriceProductTradePrice>
<ram:ChargeAmount>14040.0</ram:ChargeAmount>
</ram:GrossPriceProductTradePrice>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>14040.0</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>Z</ram:CategoryCode>
<ram:RateApplicablePercent>0.0</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>14040.0</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>2</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>Domestic transport handling</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:GrossPriceProductTradePrice>
<ram:ChargeAmount>3960.0</ram:ChargeAmount>
</ram:GrossPriceProductTradePrice>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>3960.0</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>23.0</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>3960.0</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement>
<ram:BuyerReference>n/a</ram:BuyerReference>
<ram:SellerTradeParty>
<ram:Name>Alexander Global Logistics</ram:Name>
<ram:DefinedTradeContact>
<ram:PersonName>n/a</ram:PersonName>
<ram:TelephoneUniversalCommunication>
<ram:CompleteNumber>+49 421 566 46 0</ram:CompleteNumber>
</ram:TelephoneUniversalCommunication>
<ram:EmailURIUniversalCommunication>
<ram:URIID>info@alexander-logistics.com</ram:URIID>
</ram:EmailURIUniversalCommunication>
</ram:DefinedTradeContact>
<ram:PostalTradeAddress>
<ram:PostcodeCode>70-660</ram:PostcodeCode>
<ram:LineOne>Gdanska 36</ram:LineOne>
<ram:CityName>Szczecin</ram:CityName>
<ram:CountryID>PL</ram:CountryID>
</ram:PostalTradeAddress>
<ram:URIUniversalCommunication>
<ram:URIID schemeID="EM">info@alexander-logistics.com</ram:URIID>
</ram:URIUniversalCommunication>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">PL9552521552</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Test Sp. z o.o.</ram:Name>
<ram:PostalTradeAddress>
<ram:PostcodeCode>00-001</ram:PostcodeCode>
<ram:LineOne>ul. Testowa 1</ram:LineOne>
<ram:CityName>Warszawa</ram:CityName>
<ram:CountryID>PL</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">PL1234567890</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery>
<ram:ShipToTradeParty>
<ram:Name>Test Sp. z o.o.</ram:Name>
<ram:PostalTradeAddress>
<ram:PostcodeCode>00-001</ram:PostcodeCode>
<ram:LineOne>ul. Testowa 1</ram:LineOne>
<ram:CityName>Warszawa</ram:CityName>
<ram:CountryID>PL</ram:CountryID>
</ram:PostalTradeAddress>
</ram:ShipToTradeParty>
</ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>PLN</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>30</ram:TypeCode>
<ram:PayeePartyCreditorFinancialAccount>
<ram:IBANID>PL79116022020000000654306674</ram:IBANID>
</ram:PayeePartyCreditorFinancialAccount>
<ram:PayeeSpecifiedCreditorFinancialInstitution>
<ram:BICID>BIGBPLPWXXX</ram:BICID>
</ram:PayeeSpecifiedCreditorFinancialInstitution>
</ram:SpecifiedTradeSettlementPaymentMeans>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>0.00</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>14040.0</ram:BasisAmount>
<ram:CategoryCode>Z</ram:CategoryCode>
<ram:RateApplicablePercent>0.0</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>910.80</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>3960.0</ram:BasisAmount>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>23.0</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradePaymentTerms>
<ram:Description>Payment due by 10.03.2025</ram:Description>
<ram:DueDateDateTime>
<udt:DateTimeString format="102">20250310</udt:DateTimeString>
</ram:DueDateDateTime>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>18000.00</ram:LineTotalAmount>
<ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>
<ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>
<ram:TaxBasisTotalAmount>18000.00</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="PLN">910.80</ram:TaxTotalAmount>
<ram:GrandTotalAmount>18910.80</ram:GrandTotalAmount>
<ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
<ram:DuePayableAmount>18910.80</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>

View file

@ -6,7 +6,7 @@
<Naglowek>
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
<WariantFormularza>3</WariantFormularza>
<DataWytworzeniaFa>2026-05-28T19:18:53.815720274Z</DataWytworzeniaFa>
<DataWytworzeniaFa>2026-07-24T10:11:51.410517Z</DataWytworzeniaFa>
<SystemInfo>Imixs eInvoice</SystemInfo>
</Naglowek>
<Podmiot1>
@ -42,7 +42,7 @@
<P_1>2025-02-10</P_1>
<P_1M>Szczecin</P_1M>
<P_2>FV/2025/7470</P_2>
<P_6>2026-05-28</P_6>
<P_6>2026-07-24</P_6>
<P_13_9>4620.00</P_13_9>
<P_15>4620.00</P_15>
<KursWalutyZ>0.2326</KursWalutyZ>

View file

@ -6,7 +6,7 @@
<Naglowek>
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
<WariantFormularza>3</WariantFormularza>
<DataWytworzeniaFa>2026-05-28T19:18:53.906736962Z</DataWytworzeniaFa>
<DataWytworzeniaFa>2026-07-24T10:11:51.547969Z</DataWytworzeniaFa>
<SystemInfo>Imixs eInvoice</SystemInfo>
</Naglowek>
<Podmiot1>
@ -42,7 +42,7 @@
<P_1>2025-02-10</P_1>
<P_1M>Szczecin</P_1M>
<P_2>FV/2025/7471</P_2>
<P_6>2026-05-28</P_6>
<P_6>2026-07-24</P_6>
<P_13_1>3960.00</P_13_1>
<P_14_1>910.80</P_14_1>
<P_13_6_1>14040.00</P_13_6_1>

View file

@ -6,7 +6,7 @@
<Naglowek>
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
<WariantFormularza>3</WariantFormularza>
<DataWytworzeniaFa>2026-05-28T19:18:53.629147269Z</DataWytworzeniaFa>
<DataWytworzeniaFa>2026-07-24T10:11:51.000225Z</DataWytworzeniaFa>
<SystemInfo>Imixs eInvoice</SystemInfo>
</Naglowek>
<Podmiot1>

View file

@ -6,7 +6,7 @@
<Naglowek>
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
<WariantFormularza>3</WariantFormularza>
<DataWytworzeniaFa>2026-05-28T19:18:53.885083580Z</DataWytworzeniaFa>
<DataWytworzeniaFa>2026-07-24T10:11:51.517086Z</DataWytworzeniaFa>
<SystemInfo>Imixs eInvoice</SystemInfo>
</Naglowek>
<Podmiot1>
@ -42,7 +42,7 @@
<P_1>2025-02-10</P_1>
<P_1M>Szczecin</P_1M>
<P_2>FV/2025/001</P_2>
<P_6>2026-05-28</P_6>
<P_6>2026-07-24</P_6>
<P_13_9>10000.00</P_13_9>
<P_15>10000.00</P_15>
<Adnotacje>

View file

@ -6,7 +6,7 @@
<Naglowek>
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
<WariantFormularza>3</WariantFormularza>
<DataWytworzeniaFa>2026-07-23T14:06:32.328007Z</DataWytworzeniaFa>
<DataWytworzeniaFa>2026-07-24T10:11:51.468487Z</DataWytworzeniaFa>
<SystemInfo>Imixs eInvoice</SystemInfo>
</Naglowek>
<Podmiot1>
@ -42,7 +42,7 @@
<P_1>2025-02-10</P_1>
<P_1M>Szczecin</P_1M>
<P_2>FV/2025/001</P_2>
<P_6>2026-07-23</P_6>
<P_6>2026-07-24</P_6>
<P_13_8>10000.00</P_13_8>
<P_15>10000.00</P_15>
<Adnotacje>

View file

@ -6,7 +6,7 @@
<Naglowek>
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
<WariantFormularza>3</WariantFormularza>
<DataWytworzeniaFa>2026-07-23T15:09:14.896034Z</DataWytworzeniaFa>
<DataWytworzeniaFa>2026-07-24T10:11:51.644379Z</DataWytworzeniaFa>
<SystemInfo>Imixs eInvoice</SystemInfo>
</Naglowek>
<Podmiot1>
@ -42,7 +42,7 @@
<P_1>2025-02-10</P_1>
<P_1M>Szczecin</P_1M>
<P_2>FV/2025/001</P_2>
<P_6>2026-07-23</P_6>
<P_6>2026-07-24</P_6>
<P_13_1>10000.00</P_13_1>
<P_14_1>2300.00</P_14_1>
<P_15>12300.00</P_15>

View file

@ -6,7 +6,7 @@
<Naglowek>
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
<WariantFormularza>3</WariantFormularza>
<DataWytworzeniaFa>2026-05-28T19:18:53.925385306Z</DataWytworzeniaFa>
<DataWytworzeniaFa>2026-07-24T10:11:51.591043Z</DataWytworzeniaFa>
<SystemInfo>Imixs eInvoice</SystemInfo>
</Naglowek>
<Podmiot1>
@ -42,7 +42,7 @@
<P_1>2025-02-10</P_1>
<P_1M>Szczecin</P_1M>
<P_2>FV/2025/001</P_2>
<P_6>2026-05-28</P_6>
<P_6>2026-07-24</P_6>
<P_13_1>-900.00</P_13_1>
<P_14_1>-207.00</P_14_1>
<P_14_1W>-48.15</P_14_1W>
@ -75,10 +75,10 @@
<P_7>usługa spedycyjna / transport w relacji, PL63 - CZ43</P_7>
<P_8A>szt.</P_8A>
<P_8B>1.00</P_8B>
<P_9A>-900.00</P_9A>
<P_11>-900.00</P_11>
<P_11A>-1107.00</P_11A>
<P_11Vat>-207.00</P_11Vat>
<P_9A>900.00</P_9A>
<P_11>900.00</P_11>
<P_11A>1107.00</P_11A>
<P_11Vat>207.00</P_11Vat>
<P_12>23</P_12>
</FaWiersz>
<Platnosc>