neue Imple KSEF Adapter
This commit is contained in:
parent
04acdb4ff3
commit
edacbbdd6d
9 changed files with 1772 additions and 356 deletions
|
|
@ -4,8 +4,6 @@ import java.io.ByteArrayInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
@ -25,7 +23,6 @@ import org.imixs.einvoice.EInvoiceModel;
|
||||||
import org.imixs.einvoice.EInvoiceModelFactory;
|
import org.imixs.einvoice.EInvoiceModelFactory;
|
||||||
import org.imixs.einvoice.EInvoiceModelKSeF;
|
import org.imixs.einvoice.EInvoiceModelKSeF;
|
||||||
import org.imixs.einvoice.EInvoiceNS;
|
import org.imixs.einvoice.EInvoiceNS;
|
||||||
import org.imixs.einvoice.TradeLineItem;
|
|
||||||
import org.imixs.einvoice.TradeParty;
|
import org.imixs.einvoice.TradeParty;
|
||||||
import org.imixs.workflow.FileData;
|
import org.imixs.workflow.FileData;
|
||||||
import org.imixs.workflow.ItemCollection;
|
import org.imixs.workflow.ItemCollection;
|
||||||
|
|
@ -43,41 +40,41 @@ import org.xml.sax.SAXException;
|
||||||
|
|
||||||
import com.alexanderlogistics.BusinessPartnerService;
|
import com.alexanderlogistics.BusinessPartnerService;
|
||||||
import com.alexanderlogistics.InvoiceService;
|
import com.alexanderlogistics.InvoiceService;
|
||||||
import com.alexanderlogistics.InvoiceUtil;
|
|
||||||
import com.alexanderlogistics.xml.CargosoftXMLInvoiceImportService;
|
import com.alexanderlogistics.xml.CargosoftXMLInvoiceImportService;
|
||||||
|
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The KSeFAdapter converts a Cargsoft outbound invoice into an KSeF XML
|
* The KSeFAdapter converts a Cargosoft outbound invoice into a KSeF FA(3) XML
|
||||||
* e-invoice and sends the xml file to the polish KSeF API
|
* e-invoice and sends the xml file to the polish KSeF API.
|
||||||
*
|
* <p>
|
||||||
* The adapter can be configured by the model:
|
* The adapter is configured via the BPMN model:
|
||||||
*
|
*
|
||||||
* <pre>
|
* <pre>
|
||||||
* {@code
|
* {@code
|
||||||
<ksef name="create">
|
* <ksef name="create">
|
||||||
<textblock>textblock-ref</textblock>
|
* <textblock>textblock-ref</textblock>
|
||||||
<template>filename</template>
|
* <template>filename</template>
|
||||||
<debug>true</debug>
|
* <debug>true</debug>
|
||||||
</ksef>
|
* </ksef>
|
||||||
|
* }
|
||||||
}
|
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* NIP: The adapter needs the NIP (vat-id). If we do not find the partner.vat in
|
* <b>Architecture:</b> The adapter handles invoice header data (parties,
|
||||||
* the business object, we do a lookup on the D-Cargosoft object and try the vat
|
* dates, invoice type, currency code, KOR/correction data). All logic
|
||||||
* id from there. This is because the partner.vat is a field which was not
|
* concerning invoice positions ({@code <FaWiersz>}) and summary fields
|
||||||
* defined before.
|
* ({@code P_13_x}, {@code P_14_x}, {@code P_14_xW}, {@code P_15},
|
||||||
*
|
* {@code KursWalutyZ}) is delegated to {@link KSeFInvoiceLineBuilder}, which
|
||||||
|
* uses the CargoSoft VAT code per position to determine the correct target
|
||||||
|
* field in the {@code <Fa>} block.
|
||||||
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Wenn Debug = true gibt der Adapter alle einzelschritte auf der console aus
|
* <b>NIP:</b> The adapter needs the NIP (vat-id). If we do not find the
|
||||||
* und erzeugt zusätzlich die factur-x xml und txt dateien.
|
* partner.vat in the business object, we do a lookup on the D-Cargosoft
|
||||||
*
|
* object and try the vat id from there.
|
||||||
*
|
*
|
||||||
*
|
* @version 2.0
|
||||||
* @version 1.0
|
|
||||||
* @author rsoika
|
* @author rsoika
|
||||||
*/
|
*/
|
||||||
public class KSeFAdapter implements SignalAdapter {
|
public class KSeFAdapter implements SignalAdapter {
|
||||||
|
|
@ -109,33 +106,27 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
@Inject
|
@Inject
|
||||||
InvoiceService invoiceService;
|
InvoiceService invoiceService;
|
||||||
|
|
||||||
/**
|
|
||||||
* This method
|
|
||||||
*
|
|
||||||
* @throws PluginException
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public ItemCollection execute(ItemCollection workitem, ItemCollection event)
|
public ItemCollection execute(ItemCollection workitem, ItemCollection event)
|
||||||
throws AdapterException, PluginException {
|
throws AdapterException, PluginException {
|
||||||
|
|
||||||
logger.info("├── 🔜 Convert Invoice to KSeF...");
|
logger.info("├── 🔜 Convert Invoice to KSeF...");
|
||||||
// read configuration....
|
|
||||||
ItemCollection eInvoiceConfig = workflowService.evalWorkflowResult(event, "ksef",
|
// Read configuration
|
||||||
workitem,
|
ItemCollection eInvoiceConfig = workflowService.evalWorkflowResult(event, "ksef", workitem, false);
|
||||||
false);
|
|
||||||
if (eInvoiceConfig == null || !eInvoiceConfig.hasItem("CREATE")) {
|
if (eInvoiceConfig == null || !eInvoiceConfig.hasItem("CREATE")) {
|
||||||
throw new PluginException(EInvoiceAdapter.class.getSimpleName(), CONFIG_ERROR,
|
throw new PluginException(EInvoiceAdapter.class.getSimpleName(), CONFIG_ERROR,
|
||||||
"missing e-invoice/ksef configuration in model event - please check model configuration");
|
"missing e-invoice/ksef configuration in model event - please check model configuration");
|
||||||
}
|
}
|
||||||
ItemCollection ksefCreateDefinition = XMLParser.parseItemStructure(eInvoiceConfig.getItemValueString("CREATE"));
|
ItemCollection ksefCreateDefinition = XMLParser.parseItemStructure(eInvoiceConfig.getItemValueString("CREATE"));
|
||||||
try {
|
|
||||||
|
|
||||||
// Load the e-invoice template....
|
try {
|
||||||
|
// Load the e-invoice template
|
||||||
FileData xmlFileData = loadXMLTemplate(workitem, ksefCreateDefinition);
|
FileData xmlFileData = loadXMLTemplate(workitem, ksefCreateDefinition);
|
||||||
|
|
||||||
updateEInvoice(xmlFileData, workitem);
|
updateEInvoice(xmlFileData, workitem);
|
||||||
|
|
||||||
// append XML document
|
// Append XML document
|
||||||
logger.info("│ ├── attach KSeF e-invoice...");
|
logger.info("│ ├── attach KSeF e-invoice...");
|
||||||
workitem.addFileData(xmlFileData);
|
workitem.addFileData(xmlFileData);
|
||||||
|
|
||||||
|
|
@ -147,15 +138,15 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method updates an e-invoice template with the data stored in the
|
* Updates the e-invoice template with the data stored in the workitem.
|
||||||
* workitem.
|
* <p>
|
||||||
*
|
* The method handles invoice header data (parties, invoice type, dates,
|
||||||
* First the method loads an EInvoiceModel based on the provided XML Template
|
* currency code, KOR data) and then delegates the entire line-item and
|
||||||
* and than updates the e-invoice data based on the items stored in the given
|
* summary construction to {@link KSeFInvoiceLineBuilder}.
|
||||||
* workitem.
|
*
|
||||||
*
|
* @param fileDataXMLTemplate the XML template to be filled
|
||||||
* @param workitem
|
* @param workitem the workitem holding the invoice data
|
||||||
* @throws PluginException
|
* @throws PluginException if the model cannot be parsed or written
|
||||||
*/
|
*/
|
||||||
public void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException {
|
public void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException {
|
||||||
|
|
||||||
|
|
@ -164,38 +155,37 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
|
|
||||||
model.setId(workitem.getItemValueString("invoice.number"));
|
model.setId(workitem.getItemValueString("invoice.number"));
|
||||||
|
|
||||||
// date
|
// Issue date
|
||||||
model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date"));
|
model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date"));
|
||||||
|
|
||||||
// Set Performance Date
|
// Performance date - resync from Cargosoft XML if missing
|
||||||
if (workitem.getItemValueDate("invoice.performancedate") == null) {
|
if (workitem.getItemValueDate("invoice.performancedate") == null) {
|
||||||
// hilfs code um das invoice.performancedate nachträglich zu parsen
|
|
||||||
syncPerformanceDate(workitem);
|
syncPerformanceDate(workitem);
|
||||||
}
|
}
|
||||||
((EInvoiceModelKSeF) model)
|
((EInvoiceModelKSeF) model)
|
||||||
.setPerformanceDateTime(workitem.getItemValueLocalDate("invoice.performancedate"));
|
.setPerformanceDateTime(workitem.getItemValueLocalDate("invoice.performancedate"));
|
||||||
|
|
||||||
// Update Addresses
|
// Buyer address
|
||||||
TradeParty billingAddress = buildAddress(workitem, "buyer", model);
|
TradeParty billingAddress = buildAddress(workitem, "buyer", model);
|
||||||
model.setTradeParty(billingAddress);
|
model.setTradeParty(billingAddress);
|
||||||
|
|
||||||
// set Tax Type based on vatID - as a result taxType is 1=Poland 2=EU 3=Other
|
// Tax type derived from NIP - 1=Poland, 2=EU, 3=Other
|
||||||
|
// Still used by setTradeParty (NIP vs. NrID decision via prefix)
|
||||||
((EInvoiceModelKSeF) model).setTaxType(workitem.getItemValueString("partner.vat"));
|
((EInvoiceModelKSeF) model).setTaxType(workitem.getItemValueString("partner.vat"));
|
||||||
workitem.setItemValue("invoice.tax.type", ((EInvoiceModelKSeF) model).getTaxType());
|
workitem.setItemValue("invoice.tax.type", ((EInvoiceModelKSeF) model).getTaxType());
|
||||||
|
|
||||||
// Update Invoice Type (RodzajFaktury) -> VAT | KOR
|
// Invoice type (RodzajFaktury) -> VAT or KOR
|
||||||
Element elementFa = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Fa");
|
Element elementFa = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Fa");
|
||||||
|
|
||||||
if (isKorrekturRechnung(workitem)) {
|
if (isKorrekturRechnung(workitem)) {
|
||||||
((EInvoiceModelKSeF) model).setRodzajFaktury("KOR");
|
((EInvoiceModelKSeF) model).setRodzajFaktury("KOR");
|
||||||
logger.info("│ ├── invoice type=KOR");
|
logger.info("│ ├── invoice type=KOR");
|
||||||
|
|
||||||
// Set correction data (DaneFaKorygowanej) - required for KOR invoices
|
// Correction data (DaneFaKorygowanej) - required for KOR invoices
|
||||||
Element daneFaKorygowanej = model.findOrCreateChildNodeAfter(
|
Element daneFaKorygowanej = model.findOrCreateChildNodeAfter(
|
||||||
elementFa, EInvoiceNS.KSEF, "DaneFaKorygowanej", "RodzajFaktury");
|
elementFa, EInvoiceNS.KSEF, "DaneFaKorygowanej", "RodzajFaktury");
|
||||||
|
|
||||||
Date correctionInvoiceDate = workitem.getItemValueDate("invoice.date");
|
Date correctionInvoiceDate = workitem.getItemValueDate("invoice.date");
|
||||||
// try to lookup original invoice by invoice.CorrectionInvoiceNumber
|
|
||||||
ItemCollection correctionInvoice = invoiceService
|
ItemCollection correctionInvoice = invoiceService
|
||||||
.findOutboundInvoiceByNumber(workitem.getItemValueString("invoice.CorrectionInvoiceNumber"));
|
.findOutboundInvoiceByNumber(workitem.getItemValueString("invoice.CorrectionInvoiceNumber"));
|
||||||
if (correctionInvoice != null) {
|
if (correctionInvoice != null) {
|
||||||
|
|
@ -204,7 +194,8 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
} else {
|
} else {
|
||||||
logger.info("│ ├── ⚠️ original invoice not found, using current date as fallback");
|
logger.info("│ ├── ⚠️ original invoice not found, using current date as fallback");
|
||||||
}
|
}
|
||||||
// Original invoice date (DataWystFaKorygowanej) - required
|
|
||||||
|
// Original invoice date - required
|
||||||
if (correctionInvoiceDate != null) {
|
if (correctionInvoiceDate != null) {
|
||||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
|
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
String sCorrectionDate = formatter.format(correctionInvoiceDate);
|
String sCorrectionDate = formatter.format(correctionInvoiceDate);
|
||||||
|
|
@ -212,7 +203,7 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
"DataWystFaKorygowanej", sCorrectionDate);
|
"DataWystFaKorygowanej", sCorrectionDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Original invoice number (NrFaKorygowanej) - required
|
// Original invoice number - required
|
||||||
model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF,
|
model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF,
|
||||||
"NrFaKorygowanej", workitem.getItemValueString("invoice.CorrectionInvoiceNumber"));
|
"NrFaKorygowanej", workitem.getItemValueString("invoice.CorrectionInvoiceNumber"));
|
||||||
|
|
||||||
|
|
@ -220,118 +211,35 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF, "NrKSeFN", "1");
|
model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF, "NrKSeFN", "1");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// normale rechnung
|
|
||||||
((EInvoiceModelKSeF) model).setRodzajFaktury("VAT");
|
((EInvoiceModelKSeF) model).setRodzajFaktury("VAT");
|
||||||
logger.info("│ ├── invoice type=VAT");
|
logger.info("│ ├── invoice type=VAT");
|
||||||
}
|
}
|
||||||
|
|
||||||
// set currency
|
// Currency code
|
||||||
model.updateElementValue(elementFa, EInvoiceNS.KSEF, "KodWaluty",
|
model.updateElementValue(elementFa, EInvoiceNS.KSEF, "KodWaluty",
|
||||||
workitem.getItemValueString("invoice.currency"));
|
workitem.getItemValueString("invoice.currency"));
|
||||||
|
|
||||||
// Update Invoice Items
|
// ================================================================
|
||||||
// Here we compute the pos number independent if the _childitems hold a posnum.
|
// Delegate: line items, summary fields, foreign currency fields.
|
||||||
// This is because a new imported cargosoft invoice does not provide numPos
|
// The builder uses the CargoSoft VAT code per position to write
|
||||||
// items in the child list. So it is important to generate the pos numbers here
|
// each amount into the correct P_13_x / P_14_x / P_14_xW field.
|
||||||
// one by one
|
// ================================================================
|
||||||
List<ItemCollection> invoiceItems = InvoiceUtil.explodeChildList(workitem, "_childitems");
|
new KSeFInvoiceLineBuilder().build((EInvoiceModelKSeF) model, workitem);
|
||||||
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
|
// Due date - written at the end of the XML tree
|
||||||
/*
|
|
||||||
* 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"));
|
model.setDueDateTime(workitem.getItemValueLocalDate("invoice.duedate"));
|
||||||
|
|
||||||
/*
|
// Persist the modified template
|
||||||
* finally update the template file
|
|
||||||
*/
|
|
||||||
fileDataXMLTemplate.setContent(model.getContent());
|
fileDataXMLTemplate.setContent(model.getContent());
|
||||||
|
|
||||||
} catch (FileNotFoundException | EInvoiceFormatException | TransformerException e) {
|
} catch (FileNotFoundException | EInvoiceFormatException | TransformerException e) {
|
||||||
throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, e.getMessage(), e);
|
throw new PluginException(this.getClass().getName(), DOCUMENT_ERROR, e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dies ist eine Hilfsmethode die nachträglich das invoice.performancedate aus
|
* Helper to resync the {@code invoice.performancedate} from the Cargosoft
|
||||||
* dem Cargosoft XML ausliest
|
* source XML if it is not yet set on the workitem.
|
||||||
*
|
|
||||||
* @param workitem
|
|
||||||
* @throws PluginException
|
|
||||||
*/
|
*/
|
||||||
private void syncPerformanceDate(ItemCollection workitem) throws PluginException {
|
private void syncPerformanceDate(ItemCollection workitem) throws PluginException {
|
||||||
|
|
||||||
|
|
@ -339,7 +247,6 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
try {
|
try {
|
||||||
logger.info("----Resync cargosoft performancedate....");
|
logger.info("----Resync cargosoft performancedate....");
|
||||||
DocumentBuilder documentBuilder;
|
DocumentBuilder documentBuilder;
|
||||||
// hole die XML Datei
|
|
||||||
FileData cargoXML = snapshotService.getWorkItemFile(workitem.getUniqueID(),
|
FileData cargoXML = snapshotService.getWorkItemFile(workitem.getUniqueID(),
|
||||||
workitem.getItemValueString("cargosoft.import.filename"));
|
workitem.getItemValueString("cargosoft.import.filename"));
|
||||||
if (cargoXML != null) {
|
if (cargoXML != null) {
|
||||||
|
|
@ -349,13 +256,12 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||||
Document doc = documentBuilder.parse(inputSource);
|
Document doc = documentBuilder.parse(inputSource);
|
||||||
|
|
||||||
// Performance Date
|
|
||||||
CargosoftXMLInvoiceImportService.readXMLValue(doc,
|
CargosoftXMLInvoiceImportService.readXMLValue(doc,
|
||||||
"/Invoices/Invoice/InvoiceHeader/PerformanceDate",
|
"/Invoices/Invoice/InvoiceHeader/PerformanceDate",
|
||||||
workitem, "invoice.performancedate",
|
workitem, "invoice.performancedate",
|
||||||
Date.class);
|
Date.class);
|
||||||
|
|
||||||
// falls keines gefunden wurde nehmen wir das invoice date
|
// Fallback to invoice date if no performance date was found
|
||||||
if (workitem.getItemValueDate("invoice.performancedate") == null) {
|
if (workitem.getItemValueDate("invoice.performancedate") == null) {
|
||||||
workitem.setItemValue("invoice.performancedate", workitem.getItemValueDate("invoice.date"));
|
workitem.setItemValue("invoice.performancedate", workitem.getItemValueDate("invoice.date"));
|
||||||
}
|
}
|
||||||
|
|
@ -368,14 +274,7 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt True zurück wenn es eine korrektur rechnung ist
|
* Returns {@code true} if the workitem represents a correction invoice.
|
||||||
*
|
|
||||||
* InvoiceHeader/Correction=true
|
|
||||||
* InvoiceHeader/CorrectionInvoiceNumber !empty
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param workitem
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
private boolean isKorrekturRechnung(ItemCollection workitem) {
|
private boolean isKorrekturRechnung(ItemCollection workitem) {
|
||||||
return ("true".equals(workitem.getItemValueString("invoice.correction"))
|
return ("true".equals(workitem.getItemValueString("invoice.correction"))
|
||||||
|
|
@ -383,11 +282,11 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt ein TradeParty Objekt aus einer Liste von Adresszeilen.
|
* Builds a TradeParty (buyer/seller) from the business partner data.
|
||||||
*
|
* <p>
|
||||||
* @param addressLines Liste der Adresszeilen
|
* If the business partner is missing the {@code partner.vat} item, a
|
||||||
* @param type Der Typ der TradeParty
|
* one-time migration step tries to read the VAT ID from the legacy
|
||||||
* @return TradeParty
|
* {@code cargosoftkreditor} object.
|
||||||
*/
|
*/
|
||||||
public TradeParty buildAddress(ItemCollection workitem, String type, EInvoiceModel model) throws PluginException {
|
public TradeParty buildAddress(ItemCollection workitem, String type, EInvoiceModel model) throws PluginException {
|
||||||
|
|
||||||
|
|
@ -415,20 +314,16 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
if (!dNumber.isEmpty()) {
|
if (!dNumber.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
String query = "(type:cargosoftkreditor) AND (name:" + dNumber + ")";
|
String query = "(type:cargosoftkreditor) AND (name:" + dNumber + ")";
|
||||||
List<ItemCollection> result;
|
List<ItemCollection> result = documentService.find(query, 1, 0);
|
||||||
result = documentService.find(query, 1, 0);
|
|
||||||
if (result != null && result.size() > 0) {
|
if (result != null && result.size() > 0) {
|
||||||
ItemCollection creditorData = result.get(0);
|
ItemCollection creditorData = result.get(0);
|
||||||
String vatID = creditorData.getItemValueString("_vendor_vat_registration_id");
|
String vatID = creditorData.getItemValueString("_vendor_vat_registration_id");
|
||||||
logger.info("│ ├── synchronize VAT Registrytion ID: " + vatID);
|
logger.info("│ ├── synchronize VAT Registration ID: " + vatID);
|
||||||
// update business partner
|
|
||||||
businessPartner.setItemValue("partner.vat", vatID);
|
businessPartner.setItemValue("partner.vat", vatID);
|
||||||
documentService.saveByNewTransaction(businessPartner);
|
documentService.saveByNewTransaction(businessPartner);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// should not happen!
|
|
||||||
logger.info("│ ├── ⚠️ Failed to sync BusinessPartner : " + e.getMessage());
|
logger.info("│ ├── ⚠️ Failed to sync BusinessPartner : " + e.getMessage());
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -448,87 +343,22 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
if (partnerVAT.isBlank()) {
|
if (partnerVAT.isBlank()) {
|
||||||
// Just a warning
|
// Just a warning
|
||||||
logger.warning("│ ├── ⚠️ Business Partner ID '" + partnerID + "' does not contain a VAT ID !");
|
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);
|
workitem.setItemValue("partner.vat", partnerVAT);
|
||||||
tradeParty.setVatNumber(partnerVAT);
|
tradeParty.setVatNumber(partnerVAT);
|
||||||
|
|
||||||
// Update or create NrKlienta element directly under Podmiot2
|
// Update or create NrKlienta element directly under Podmiot2
|
||||||
Element podmiot2 = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Podmiot2");
|
Element podmiot2 = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Podmiot2");
|
||||||
model.updateElementValue(podmiot2, EInvoiceNS.KSEF, "NrKlienta",
|
model.updateElementValue(podmiot2, EInvoiceNS.KSEF, "NrKlienta",
|
||||||
businessPartner.getItemValueString("dbtr.number"));
|
businessPartner.getItemValueString("dbtr.number"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tradeParty;
|
return tradeParty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the data list of an invoice line and returns a TradeLineItem object.
|
* Loads the e-invoice XML template referenced by the configuration.
|
||||||
* 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)
|
private FileData loadXMLTemplate(ItemCollection workitem, ItemCollection config)
|
||||||
throws PluginException {
|
throws PluginException {
|
||||||
|
|
||||||
|
|
@ -539,10 +369,10 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
try {
|
try {
|
||||||
debug = Boolean.parseBoolean(config.getItemValueString("debug"));
|
debug = Boolean.parseBoolean(config.getItemValueString("debug"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
// ignore - debug remains false
|
||||||
}
|
}
|
||||||
String targetName = "ksef.xml";
|
String targetName = "ksef.xml";
|
||||||
|
|
||||||
// adapt text....
|
|
||||||
sourceName = workflowService.adaptText(sourceName, workitem);
|
sourceName = workflowService.adaptText(sourceName, workitem);
|
||||||
|
|
||||||
if ((template == null || template.isEmpty()) || (textblock == null || textblock.isEmpty())) {
|
if ((template == null || template.isEmpty()) || (textblock == null || textblock.isEmpty())) {
|
||||||
|
|
@ -551,10 +381,8 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
"invalid e-invoice configuration in model event - textblock/template reference not defined!");
|
"invalid e-invoice configuration in model event - textblock/template reference not defined!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// load the text block
|
|
||||||
FileData fileData = loadTextBlockFileData(textblock, template);
|
FileData fileData = loadTextBlockFileData(textblock, template);
|
||||||
|
|
||||||
// do we found the document?
|
|
||||||
if (fileData == null) {
|
if (fileData == null) {
|
||||||
throw new PluginException(EInvoiceAdapter.class.getSimpleName(),
|
throw new PluginException(EInvoiceAdapter.class.getSimpleName(),
|
||||||
CONFIG_ERROR,
|
CONFIG_ERROR,
|
||||||
|
|
@ -567,26 +395,18 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method returns a text-block ItemCollection for a specified name.
|
* Returns a text-block FileData by name and filename.
|
||||||
*
|
|
||||||
* @param name in attribute txtname
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public FileData loadTextBlockFileData(String name, String fileName) {
|
public FileData loadTextBlockFileData(String name, String fileName) {
|
||||||
ItemCollection textBlockItemCollection = null;
|
ItemCollection textBlockItemCollection = null;
|
||||||
|
|
||||||
// load text-block by name....
|
|
||||||
String sQuery = "(type:\"" + TYPE_TEXTBLOCK + "\" AND txtname:\"" + name + "\")";
|
String sQuery = "(type:\"" + TYPE_TEXTBLOCK + "\" AND txtname:\"" + name + "\")";
|
||||||
Collection<ItemCollection> col;
|
Collection<ItemCollection> col;
|
||||||
try {
|
try {
|
||||||
// find the textblock...
|
|
||||||
col = documentService.find(sQuery, 1, 0);
|
col = documentService.find(sQuery, 1, 0);
|
||||||
if (col.size() > 0) {
|
if (col.size() > 0) {
|
||||||
textBlockItemCollection = col.iterator().next();
|
textBlockItemCollection = col.iterator().next();
|
||||||
// fetch the fileData...
|
|
||||||
return snapshotService.getWorkItemFile(textBlockItemCollection.getUniqueID(), fileName);
|
return snapshotService.getWorkItemFile(textBlockItemCollection.getUniqueID(), fileName);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
logger.warning("Missing text-block : '" + name + "'");
|
logger.warning("Missing text-block : '" + name + "'");
|
||||||
}
|
}
|
||||||
|
|
@ -596,5 +416,4 @@ public class KSeFAdapter implements SignalAdapter {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,600 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,362 @@
|
||||||
|
package com.alexanderlogistics.einvoice;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.imixs.einvoice.EInvoiceModelKSeF;
|
||||||
|
import org.imixs.einvoice.EInvoiceNS;
|
||||||
|
import org.imixs.workflow.ItemCollection;
|
||||||
|
import org.imixs.workflow.exceptions.PluginException;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
|
||||||
|
import com.alexanderlogistics.InvoiceUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the {@code <FaWiersz>} (line item) blocks and the
|
||||||
|
* {@code <Fa>}-level summary fields ({@code P_13_x}, {@code P_14_x},
|
||||||
|
* {@code P_14_xW}, {@code P_15}, {@code KursWalutyZ}) of a Polish KSeF FA(3)
|
||||||
|
* e-invoice.
|
||||||
|
* <p>
|
||||||
|
* This builder is the single entry point for everything related to invoice
|
||||||
|
* positions and totals. It replaces the previous logic in {@code KSeFAdapter}
|
||||||
|
* which used {@code model.setNetTotalAmount(...)},
|
||||||
|
* {@code model.setTaxTotalAmount(...)} and a per-position
|
||||||
|
* {@code TradeLineItem} round-trip - all of which made the wrong implicit
|
||||||
|
* assumption that one invoice has exactly one tax rate.
|
||||||
|
* <p>
|
||||||
|
* The builder runs in three sequential phases:
|
||||||
|
* <ol>
|
||||||
|
* <li><b>Line items:</b> for each position in {@code _childitems} a
|
||||||
|
* {@code <FaWiersz>} block is written. The CargoSoft VAT code drives the
|
||||||
|
* mapping into the correct {@code <P_12>} value and into the aggregation
|
||||||
|
* bucket.</li>
|
||||||
|
* <li><b>Summary fields:</b> aggregated net and VAT amounts per
|
||||||
|
* {@link TaxCode} are written into the corresponding
|
||||||
|
* {@code <P_13_x>}/{@code <P_14_x>} elements; {@code <P_15>} receives the
|
||||||
|
* gross total of all positions.</li>
|
||||||
|
* <li><b>Foreign currency:</b> if the invoice currency differs from PLN, a
|
||||||
|
* {@code <P_14_xW>} (VAT amount converted to PLN) is written for every
|
||||||
|
* populated VAT field, and {@code <KursWalutyZ>} is written after
|
||||||
|
* {@code <P_15>}.</li>
|
||||||
|
* </ol>
|
||||||
|
* <p>
|
||||||
|
* <b>Source of truth for amounts:</b> All amounts are computed from the
|
||||||
|
* position data ({@code datev.umsatz}, {@code datev.vatrate}). Aggregated
|
||||||
|
* workitem fields like {@code invoice.total.net} or {@code invoice.total}
|
||||||
|
* are intentionally NOT consulted here, because rounding the difference of
|
||||||
|
* two aggregates can drift away from the sum of positions.
|
||||||
|
*
|
||||||
|
* @author imixs.com
|
||||||
|
*/
|
||||||
|
public class KSeFInvoiceLineBuilder {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(KSeFInvoiceLineBuilder.class.getName());
|
||||||
|
|
||||||
|
/** Scale (decimal places) for all monetary amounts written into the XML. */
|
||||||
|
private static final int MONEY_SCALE = 2;
|
||||||
|
|
||||||
|
/** Standard rounding mode for monetary amounts. */
|
||||||
|
private static final RoundingMode MONEY_ROUNDING = RoundingMode.HALF_UP;
|
||||||
|
|
||||||
|
/** Hundred, used for VAT percentage calculations. */
|
||||||
|
private static final BigDecimal HUNDRED = new BigDecimal("100");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds all line items and summary fields of the given KSeF model from
|
||||||
|
* the data of the given workitem.
|
||||||
|
*
|
||||||
|
* @param model the KSeF model whose {@code <Fa>} block will be
|
||||||
|
* populated
|
||||||
|
* @param workitem the workitem holding the invoice header data
|
||||||
|
* ({@code invoice.currency}, {@code invoice.rate}) and
|
||||||
|
* the line items ({@code _childitems})
|
||||||
|
* @throws PluginException if a CargoSoft VAT code cannot be mapped or any
|
||||||
|
* required field is missing
|
||||||
|
*/
|
||||||
|
public void build(EInvoiceModelKSeF model, ItemCollection workitem) throws PluginException {
|
||||||
|
|
||||||
|
Element elementFa = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Fa");
|
||||||
|
|
||||||
|
List<ItemCollection> invoiceItems = InvoiceUtil.explodeChildList(workitem, "_childitems");
|
||||||
|
if (invoiceItems == null || invoiceItems.isEmpty()) {
|
||||||
|
logger.warning("│ ├── ⚠️ no invoice items found - nothing to build");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregation buckets: net and VAT amount per TaxCode
|
||||||
|
Map<TaxCode, NetVatPair> aggregates = new EnumMap<>(TaxCode.class);
|
||||||
|
|
||||||
|
// Phase 1: write each FaWiersz, fill aggregation buckets
|
||||||
|
BigDecimal grossTotal = buildLineItems(model, elementFa, invoiceItems, aggregates);
|
||||||
|
|
||||||
|
// Phase 2: write summary fields P_13_x, P_14_x and P_15
|
||||||
|
writeSummaryFields(model, elementFa, aggregates, grossTotal);
|
||||||
|
|
||||||
|
// Phase 3: write foreign currency fields (P_14_xW, KursWalutyZ)
|
||||||
|
String currency = workitem.getItemValueString("invoice.currency");
|
||||||
|
if (!"PLN".equalsIgnoreCase(currency) && !currency.isBlank()) {
|
||||||
|
BigDecimal exchangeRate = BigDecimal.valueOf(workitem.getItemValueDouble("invoice.rate"));
|
||||||
|
writeForeignCurrencyFields(model, elementFa, aggregates, exchangeRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Phase 1 - line items
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes one {@code <FaWiersz>} block per invoice line and fills the
|
||||||
|
* aggregation buckets. Returns the gross total of all positions.
|
||||||
|
*/
|
||||||
|
private BigDecimal buildLineItems(
|
||||||
|
EInvoiceModelKSeF model,
|
||||||
|
Element elementFa,
|
||||||
|
List<ItemCollection> invoiceItems,
|
||||||
|
Map<TaxCode, NetVatPair> aggregates) throws PluginException {
|
||||||
|
|
||||||
|
BigDecimal grossTotal = BigDecimal.ZERO;
|
||||||
|
int pos = 1;
|
||||||
|
|
||||||
|
for (ItemCollection invoiceItem : invoiceItems) {
|
||||||
|
|
||||||
|
// Resolve the CargoSoft VAT code into a TaxCode
|
||||||
|
String cargoCode = invoiceItem.getItemValueString("cargosoft.vat.code");
|
||||||
|
TaxCode taxCode = TaxCode.fromCargosoftCode(cargoCode);
|
||||||
|
|
||||||
|
// Read raw position data
|
||||||
|
BigDecimal netAmount = BigDecimal.valueOf(invoiceItem.getItemValueDouble("datev.umsatz"));
|
||||||
|
BigDecimal vatRate = BigDecimal.valueOf(invoiceItem.getItemValueDouble("datev.vatrate"));
|
||||||
|
|
||||||
|
// Compute VAT and gross amount of this position
|
||||||
|
BigDecimal vatAmount;
|
||||||
|
if (taxCode.hasVat()) {
|
||||||
|
vatAmount = netAmount.multiply(vatRate).divide(HUNDRED, MONEY_SCALE, MONEY_ROUNDING);
|
||||||
|
} else {
|
||||||
|
// 0% / np I / np II - no VAT amount, even if vatRate would be > 0
|
||||||
|
vatAmount = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
BigDecimal grossAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
|
// Write the FaWiersz block
|
||||||
|
writeFaWiersz(model, elementFa, invoiceItem, pos, taxCode, netAmount, vatAmount, grossAmount);
|
||||||
|
|
||||||
|
// Aggregate per TaxCode
|
||||||
|
aggregates.merge(taxCode,
|
||||||
|
new NetVatPair(netAmount, vatAmount),
|
||||||
|
NetVatPair::plus);
|
||||||
|
|
||||||
|
grossTotal = grossTotal.add(grossAmount);
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return grossTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a single {@code <FaWiersz>} block at the schema-correct position
|
||||||
|
* (before the first of {@code Rozliczenie}, {@code Platnosc},
|
||||||
|
* {@code WarunkiTransakcji}, {@code Zamowienie}).
|
||||||
|
*/
|
||||||
|
private void writeFaWiersz(
|
||||||
|
EInvoiceModelKSeF model,
|
||||||
|
Element elementFa,
|
||||||
|
ItemCollection invoiceItem,
|
||||||
|
int pos,
|
||||||
|
TaxCode taxCode,
|
||||||
|
BigDecimal netAmount,
|
||||||
|
BigDecimal vatAmount,
|
||||||
|
BigDecimal grossAmount) {
|
||||||
|
|
||||||
|
// Determine schema-correct insert position
|
||||||
|
Element insertBefore = findFirstSuccessorOfFaWiersz(elementFa, model);
|
||||||
|
Element faWiersz = model.createChildNode(elementFa, EInvoiceNS.KSEF, "FaWiersz", insertBefore);
|
||||||
|
|
||||||
|
// NrWierszaFa - line number
|
||||||
|
model.updateElementValue(faWiersz, EInvoiceNS.KSEF, "NrWierszaFa", String.valueOf(pos));
|
||||||
|
|
||||||
|
// P_7 - description (concatenated billingtext, max 512 chars)
|
||||||
|
String description = buildDescription(invoiceItem);
|
||||||
|
if (!description.isEmpty()) {
|
||||||
|
model.updateElementValue(faWiersz, EInvoiceNS.KSEF, "P_7", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
// P_8A - unit of measure (default "szt." = pieces)
|
||||||
|
model.updateElementValue(faWiersz, EInvoiceNS.KSEF, "P_8A", "szt.");
|
||||||
|
|
||||||
|
// P_8B - quantity (always 1.00 in this context, see business rule
|
||||||
|
// of 16.04.2026: total = net x quantity, quantity is always 1)
|
||||||
|
model.updateElementValue(faWiersz, EInvoiceNS.KSEF, "P_8B", "1.00");
|
||||||
|
|
||||||
|
// P_9A - net unit price (= net amount, because quantity is 1)
|
||||||
|
model.updateElementValue(faWiersz, EInvoiceNS.KSEF, "P_9A", format(netAmount));
|
||||||
|
|
||||||
|
// P_11 - net amount of the position
|
||||||
|
model.updateElementValue(faWiersz, EInvoiceNS.KSEF, "P_11", format(netAmount));
|
||||||
|
|
||||||
|
// P_11Vat - VAT amount of the position (0 for 0% / np codes)
|
||||||
|
model.updateElementValue(faWiersz, EInvoiceNS.KSEF, "P_11Vat", format(vatAmount));
|
||||||
|
|
||||||
|
// P_11A - gross amount of the position
|
||||||
|
model.updateElementValue(faWiersz, EInvoiceNS.KSEF, "P_11A", format(grossAmount));
|
||||||
|
|
||||||
|
// P_12 - tax category value driven by the CargoSoft code
|
||||||
|
model.updateElementValue(faWiersz, EInvoiceNS.KSEF, "P_12", taxCode.getP12Value());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first child of the {@code <Fa>} element that must appear
|
||||||
|
* AFTER all {@code <FaWiersz>} blocks. {@code null} if none exists, in
|
||||||
|
* which case new {@code <FaWiersz>} blocks are simply appended.
|
||||||
|
*/
|
||||||
|
private Element findFirstSuccessorOfFaWiersz(Element elementFa, EInvoiceModelKSeF model) {
|
||||||
|
String prefix = model.getPrefix(EInvoiceNS.KSEF);
|
||||||
|
String[] successors = { "Rozliczenie", "Platnosc", "WarunkiTransakcji", "Zamowienie" };
|
||||||
|
Node child = elementFa.getFirstChild();
|
||||||
|
while (child != null) {
|
||||||
|
if (child.getNodeType() == Node.ELEMENT_NODE) {
|
||||||
|
String childName = child.getNodeName();
|
||||||
|
for (String name : successors) {
|
||||||
|
if ((prefix + name).equals(childName)) {
|
||||||
|
return (Element) child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child = child.getNextSibling();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenates the {@code billingtext} multi-value into one string,
|
||||||
|
* truncated to 512 characters (XSD limit for {@code <P_7>}).
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private String buildDescription(ItemCollection invoiceItem) {
|
||||||
|
final int MAX_LENGTH = 512;
|
||||||
|
List<String> billingTexts = invoiceItem.getItemValue("billingtext");
|
||||||
|
if (billingTexts == null || billingTexts.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String joined = String.join(", ", billingTexts);
|
||||||
|
if (joined.length() > MAX_LENGTH) {
|
||||||
|
joined = joined.substring(0, MAX_LENGTH);
|
||||||
|
}
|
||||||
|
return joined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Phase 2 - summary fields
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the aggregated net and VAT amounts into the corresponding
|
||||||
|
* {@code <P_13_x>}/{@code <P_14_x>} elements of the {@code <Fa>} block,
|
||||||
|
* and writes {@code <P_15>} as the gross total.
|
||||||
|
*/
|
||||||
|
private void writeSummaryFields(
|
||||||
|
EInvoiceModelKSeF model,
|
||||||
|
Element elementFa,
|
||||||
|
Map<TaxCode, NetVatPair> aggregates,
|
||||||
|
BigDecimal grossTotal) {
|
||||||
|
|
||||||
|
for (Map.Entry<TaxCode, NetVatPair> entry : aggregates.entrySet()) {
|
||||||
|
TaxCode taxCode = entry.getKey();
|
||||||
|
NetVatPair pair = entry.getValue();
|
||||||
|
|
||||||
|
// Net summary field (always present)
|
||||||
|
String netField = taxCode.getNetSummaryField();
|
||||||
|
Element netElement = model.findOrCreateChildNode(elementFa, EInvoiceNS.KSEF, netField);
|
||||||
|
netElement.setTextContent(format(pair.net));
|
||||||
|
logger.info("│ ├── set " + netField + " = " + format(pair.net));
|
||||||
|
|
||||||
|
// VAT summary field (only for taxable codes)
|
||||||
|
if (taxCode.hasVat()) {
|
||||||
|
String vatField = taxCode.getVatSummaryField();
|
||||||
|
Element vatElement = model.findOrCreateChildNode(elementFa, EInvoiceNS.KSEF, vatField);
|
||||||
|
vatElement.setTextContent(format(pair.vat));
|
||||||
|
logger.info("│ ├── set " + vatField + " = " + format(pair.vat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// P_15 - gross total of the entire invoice
|
||||||
|
Element p15 = model.findOrCreateChildNode(elementFa, EInvoiceNS.KSEF, "P_15");
|
||||||
|
p15.setTextContent(format(grossTotal));
|
||||||
|
logger.info("│ ├── set P_15 = " + format(grossTotal));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Phase 3 - foreign currency
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the {@code <P_14_xW>} fields (VAT amount converted to PLN) and
|
||||||
|
* the {@code <KursWalutyZ>} element after {@code <P_15>}.
|
||||||
|
*/
|
||||||
|
private void writeForeignCurrencyFields(
|
||||||
|
EInvoiceModelKSeF model,
|
||||||
|
Element elementFa,
|
||||||
|
Map<TaxCode, NetVatPair> aggregates,
|
||||||
|
BigDecimal exchangeRate) {
|
||||||
|
|
||||||
|
// For every aggregated VAT entry write the corresponding P_14_xW
|
||||||
|
for (Map.Entry<TaxCode, NetVatPair> entry : aggregates.entrySet()) {
|
||||||
|
TaxCode taxCode = entry.getKey();
|
||||||
|
if (!taxCode.hasVat()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String vatField = taxCode.getVatSummaryField();
|
||||||
|
String vatFieldW = vatField + "W";
|
||||||
|
|
||||||
|
// P_14_xW = P_14_x * exchangeRate, rounded to 2 decimals
|
||||||
|
BigDecimal vatInPln = entry.getValue().vat
|
||||||
|
.multiply(exchangeRate)
|
||||||
|
.setScale(MONEY_SCALE, MONEY_ROUNDING);
|
||||||
|
|
||||||
|
// Insert P_14_xW directly after its sibling P_14_x
|
||||||
|
Element vatWElement = model.findOrCreateChildNodeAfter(
|
||||||
|
elementFa, EInvoiceNS.KSEF, vatFieldW, vatField);
|
||||||
|
vatWElement.setTextContent(format(vatInPln));
|
||||||
|
logger.info("│ ├── set " + vatFieldW + " = " + format(vatInPln));
|
||||||
|
}
|
||||||
|
|
||||||
|
// KursWalutyZ - exchange rate, written directly after P_15
|
||||||
|
Element kursElement = model.findOrCreateChildNodeAfter(
|
||||||
|
elementFa, EInvoiceNS.KSEF, "KursWalutyZ", "P_15");
|
||||||
|
kursElement.setTextContent(exchangeRate.toPlainString());
|
||||||
|
logger.info("│ ├── set KursWalutyZ = " + exchangeRate.toPlainString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a monetary amount as a plain string with exactly 2 decimal
|
||||||
|
* places and HALF_UP rounding, suitable for KSeF FA(3) XML.
|
||||||
|
*/
|
||||||
|
private String format(BigDecimal value) {
|
||||||
|
return value.setScale(MONEY_SCALE, MONEY_ROUNDING).toPlainString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregation bucket: net and VAT amount summed per {@link TaxCode}.
|
||||||
|
*/
|
||||||
|
private static final class NetVatPair {
|
||||||
|
final BigDecimal net;
|
||||||
|
final BigDecimal vat;
|
||||||
|
|
||||||
|
NetVatPair(BigDecimal net, BigDecimal vat) {
|
||||||
|
this.net = net;
|
||||||
|
this.vat = vat;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetVatPair plus(NetVatPair other) {
|
||||||
|
return new NetVatPair(this.net.add(other.net), this.vat.add(other.vat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
package com.alexanderlogistics.einvoice;
|
||||||
|
|
||||||
|
import org.imixs.workflow.exceptions.PluginException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for the mapping between CargoSoft VAT codes and the
|
||||||
|
* Polish KSeF FA(3) XML schema fields.
|
||||||
|
* <p>
|
||||||
|
* Each enum constant maps a CargoSoft VAT code (as it appears in the
|
||||||
|
* {@code cargosoft.vat.code} item of an invoice line) to:
|
||||||
|
* <ul>
|
||||||
|
* <li>the value to be written into the {@code <P_12>} element of a
|
||||||
|
* {@code <FaWiersz>} (line item) block,</li>
|
||||||
|
* <li>the target net summary field in the {@code <Fa>} block (one of
|
||||||
|
* {@code P_13_1}, {@code P_13_2}, {@code P_13_6_1}, {@code P_13_8},
|
||||||
|
* {@code P_13_9}),</li>
|
||||||
|
* <li>the optional target VAT summary field in the {@code <Fa>} block
|
||||||
|
* ({@code P_14_1}, {@code P_14_2}) - {@code null} for 0% / non-taxable
|
||||||
|
* codes.</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* The mapping is derived from the field-mapping document provided by the
|
||||||
|
* Polish tax advisor of AGLP sp. z o.o. and reflects the requirements of the
|
||||||
|
* Polish VAT act (UStG-PL) and the SAF-T reporting (JPK_V7).
|
||||||
|
* <p>
|
||||||
|
* <b>Note on Java identifiers:</b> The enum constants are prefixed with
|
||||||
|
* {@code CARGO_} because plain numeric names like {@code 8} or {@code 23} are
|
||||||
|
* not valid Java identifiers. The CargoSoft code itself is held as a String
|
||||||
|
* field and matches the original code unchanged ({@code "8"}, {@code "23"}).
|
||||||
|
* <p>
|
||||||
|
* <b>Mixed code "023":</b> The CargoSoft code {@code 023} is intentionally
|
||||||
|
* NOT defined here. It is a CargoSoft-internal mixed code (either 0% or 23%)
|
||||||
|
* for which no deterministic mapping exists. Such codes will trigger a
|
||||||
|
* {@link PluginException} via {@link #fromCargosoftCode(String)}.
|
||||||
|
*
|
||||||
|
* @see <a href="http://crd.gov.pl/wzor/2025/06/25/13775/schemat.xsd">FA(3) XSD
|
||||||
|
* schema</a>
|
||||||
|
*/
|
||||||
|
public enum TaxCode {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domestic 0% rate, services in Polish seaports (Art. 83 sec. 1 no. 9 UStG-PL).
|
||||||
|
*/
|
||||||
|
CARGO_0("0", "0 KR", "P_13_6_1", null),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domestic 0% rate, forwarding services for export of goods (Art. 83 sec. 1 no.
|
||||||
|
* 19 UStG-PL).
|
||||||
|
*/
|
||||||
|
CARGO_0E("0%E", "0 KR", "P_13_6_1", null),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domestic 0% rate, shipping agency services (Art. 83 sec. 1 no. 17 UStG-PL).
|
||||||
|
*/
|
||||||
|
CARGO_01("01", "0 KR", "P_13_6_1", null),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domestic 0% rate, other maritime transport services (Art. 83 sec. 1 no. 17 or
|
||||||
|
* 20 UStG-PL).
|
||||||
|
*/
|
||||||
|
CARGO_0MR("0MR", "0 KR", "P_13_6_1", null),
|
||||||
|
|
||||||
|
/** Standard tax rate 23% (Art. 41 sec. 1 / Art. 146ef UStG-PL). */
|
||||||
|
CARGO_23("23", "23", "P_13_1", "P_14_1"),
|
||||||
|
|
||||||
|
/** First reduced tax rate 8% (Art. 41 sec. 2 UStG-PL). */
|
||||||
|
CARGO_8("8", "8", "P_13_2", "P_14_2"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not subject to Polish VAT, B2B service to an EU taxable person
|
||||||
|
* (Art. 28b + Art. 100 sec. 1 no. 4 UStG-PL).
|
||||||
|
* Reportable in the EU recapitulative statement (VAT-UE / ZM).
|
||||||
|
*/
|
||||||
|
CARGO_NP("NP", "np II", "P_13_9", null),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not subject to Polish VAT, customer outside the EU or transit
|
||||||
|
* (Art. 28b UStG-PL, excluding Art. 100 sec. 1 no. 4).
|
||||||
|
* NOT reportable in VAT-UE.
|
||||||
|
*/
|
||||||
|
CARGO_NPT("NPT", "np I", "P_13_8", null);
|
||||||
|
|
||||||
|
private final String cargosoftCode;
|
||||||
|
private final String p12Value;
|
||||||
|
private final String netSummaryField;
|
||||||
|
private final String vatSummaryField;
|
||||||
|
|
||||||
|
TaxCode(String cargosoftCode, String p12Value, String netSummaryField, String vatSummaryField) {
|
||||||
|
this.cargosoftCode = cargosoftCode;
|
||||||
|
this.p12Value = p12Value;
|
||||||
|
this.netSummaryField = netSummaryField;
|
||||||
|
this.vatSummaryField = vatSummaryField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the original CargoSoft VAT code (e.g. "23", "0%E", "NP")
|
||||||
|
*/
|
||||||
|
public String getCargosoftCode() {
|
||||||
|
return cargosoftCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the value to be written into the {@code <P_12>} element of a
|
||||||
|
* {@code <FaWiersz>} block (e.g. "23", "0 KR", "np II")
|
||||||
|
*/
|
||||||
|
public String getP12Value() {
|
||||||
|
return p12Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the name of the target net summary tag in the {@code <Fa>}
|
||||||
|
* block (e.g. "P_13_1", "P_13_6_1", "P_13_9")
|
||||||
|
*/
|
||||||
|
public String getNetSummaryField() {
|
||||||
|
return netSummaryField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the name of the target VAT summary tag in the {@code <Fa>}
|
||||||
|
* block (e.g. "P_14_1") or {@code null} for 0% / non-taxable codes
|
||||||
|
*/
|
||||||
|
public String getVatSummaryField() {
|
||||||
|
return vatSummaryField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} if this code carries a non-zero VAT amount
|
||||||
|
* (i.e. a VAT summary field exists); {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean hasVat() {
|
||||||
|
return vatSummaryField != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a CargoSoft VAT code string to the corresponding {@link TaxCode}
|
||||||
|
* enum constant.
|
||||||
|
* <p>
|
||||||
|
* Throws a {@link PluginException} if the code is unknown, blank or
|
||||||
|
* {@code null}. This is by design: an unmapped code MUST stop processing,
|
||||||
|
* because writing the wrong summary field would silently produce an
|
||||||
|
* invalid JPK_V7 entry while still passing KSeF XSD validation.
|
||||||
|
*
|
||||||
|
* @param cargosoftCode the value of {@code cargosoft.vat.code} from a
|
||||||
|
* CargoSoft invoice line (e.g. "23", "NP")
|
||||||
|
* @return the matching {@link TaxCode} enum constant
|
||||||
|
* @throws PluginException if the code is unknown, blank or {@code null}
|
||||||
|
*/
|
||||||
|
public static TaxCode fromCargosoftCode(String cargosoftCode) throws PluginException {
|
||||||
|
if (cargosoftCode == null || cargosoftCode.isBlank()) {
|
||||||
|
throw new PluginException(
|
||||||
|
TaxCode.class.getSimpleName(),
|
||||||
|
"TAX_CODE_ERROR",
|
||||||
|
"CargoSoft VAT code is null or blank - cannot map to KSeF tax field");
|
||||||
|
}
|
||||||
|
for (TaxCode tc : values()) {
|
||||||
|
if (tc.cargosoftCode.equals(cargosoftCode)) {
|
||||||
|
return tc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new PluginException(
|
||||||
|
TaxCode.class.getSimpleName(),
|
||||||
|
"TAX_CODE_ERROR",
|
||||||
|
"Unknown CargoSoft VAT code: '" + cargosoftCode
|
||||||
|
+ "' - no mapping to a KSeF FA(3) tax field defined");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.alexanderlogistics.ksef.api;
|
package com.alexanderlogistics.ksef.api;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
@ -19,6 +20,7 @@ import java.util.logging.Logger;
|
||||||
import org.imixs.workflow.FileData;
|
import org.imixs.workflow.FileData;
|
||||||
import org.imixs.workflow.ItemCollection;
|
import org.imixs.workflow.ItemCollection;
|
||||||
import org.imixs.workflow.engine.DocumentService;
|
import org.imixs.workflow.engine.DocumentService;
|
||||||
|
import org.imixs.workflow.exceptions.PluginException;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -39,10 +41,14 @@ import com.alexanderlogistics.einvoice.KSeFAdapter;
|
||||||
* Der Test simuliert eine Invoice und ruft dann den KSeFAdapter an um ein
|
* Der Test simuliert eine Invoice und ruft dann den KSeFAdapter an um ein
|
||||||
* Output File zu erzeugen.
|
* Output File zu erzeugen.
|
||||||
* <p>
|
* <p>
|
||||||
* Zum validieren kann man dann den validate_xml.sh Script hernehmen
|
* Zum validieren kann man dann den validate_xml.sh Script hernehmen.
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
|
* Each test produces an XML output file under
|
||||||
|
* {@code src/test/resources/ksef/output/} for manual inspection and external
|
||||||
|
* schema validation. JUnit assertions are intentionally limited to a sanity
|
||||||
|
* check that the file was produced.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
public class KSeFAdapterTest {
|
public class KSeFAdapterTest {
|
||||||
|
|
||||||
|
|
@ -81,14 +87,14 @@ public class KSeFAdapterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erzeugt ein lokales XML File zum testen
|
* Simple PLN invoice with one tax rate (23%).
|
||||||
|
* Expected: P_13_1, P_14_1, P_15.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Test Simple Invoice")
|
@DisplayName("Test Simple Invoice (PLN, 23%)")
|
||||||
public void testSimpleInvoice() throws Exception {
|
public void testSimpleInvoice() throws Exception {
|
||||||
logger.info("==> Test: Upload Invoice XML");
|
logger.info("==> Test: Simple Invoice");
|
||||||
|
|
||||||
// Prepare a workitem with typical invoice data
|
|
||||||
ItemCollection workitem = new ItemCollection();
|
ItemCollection workitem = new ItemCollection();
|
||||||
workitem.setItemValue("invoice.number", "FV/2025/001");
|
workitem.setItemValue("invoice.number", "FV/2025/001");
|
||||||
workitem.setItemValue("invoice.date", LocalDate.of(2025, 2, 10));
|
workitem.setItemValue("invoice.date", LocalDate.of(2025, 2, 10));
|
||||||
|
|
@ -105,52 +111,55 @@ public class KSeFAdapterTest {
|
||||||
|
|
||||||
// Prepare child items (invoice line items)
|
// Prepare child items (invoice line items)
|
||||||
List<Object> childItems = new ArrayList<>();
|
List<Object> childItems = new ArrayList<>();
|
||||||
|
|
||||||
ItemCollection lineItem1 = new ItemCollection();
|
ItemCollection lineItem1 = new ItemCollection();
|
||||||
lineItem1.setItemValue("numpos", "1");
|
lineItem1.setItemValue("numpos", "1");
|
||||||
lineItem1.setItemValue("datev.text", "Transport Berlin - Warsaw");
|
lineItem1.setItemValue("datev.text", "Transport Berlin - Warsaw");
|
||||||
|
lineItem1.setItemValue("billingtext", "Transport Berlin - Warsaw");
|
||||||
lineItem1.setItemValue("datev.umsatz", 6000.00);
|
lineItem1.setItemValue("datev.umsatz", 6000.00);
|
||||||
|
lineItem1.setItemValue("datev.vatrate", 23.0);
|
||||||
|
lineItem1.setItemValue("cargosoft.vat.code", "23");
|
||||||
childItems.add(lineItem1.getAllItems());
|
childItems.add(lineItem1.getAllItems());
|
||||||
|
|
||||||
ItemCollection lineItem2 = new ItemCollection();
|
ItemCollection lineItem2 = new ItemCollection();
|
||||||
lineItem2.setItemValue("numpos", "2");
|
lineItem2.setItemValue("numpos", "2");
|
||||||
lineItem2.setItemValue("datev.text", "Customs handling");
|
lineItem2.setItemValue("datev.text", "Customs handling");
|
||||||
|
lineItem2.setItemValue("billingtext", "Customs handling");
|
||||||
lineItem2.setItemValue("datev.umsatz", 4000.00);
|
lineItem2.setItemValue("datev.umsatz", 4000.00);
|
||||||
|
lineItem2.setItemValue("datev.vatrate", 23.0);
|
||||||
|
lineItem2.setItemValue("cargosoft.vat.code", "23");
|
||||||
childItems.add(lineItem2.getAllItems());
|
childItems.add(lineItem2.getAllItems());
|
||||||
|
|
||||||
workitem.setItemValue("_childitems", childItems);
|
workitem.setItemValue("_childitems", childItems);
|
||||||
|
|
||||||
// Arrange
|
|
||||||
when(businessPartnerService.getBusinessPartnerByID("BP-001"))
|
when(businessPartnerService.getBusinessPartnerByID("BP-001"))
|
||||||
.thenReturn(businessPartner);
|
.thenReturn(businessPartner);
|
||||||
|
|
||||||
FileData xmlTemplate = loadTemplateFromResources(TEMPLATE_FILE);
|
FileData xmlTemplate = loadTemplateFromResources(TEMPLATE_FILE);
|
||||||
|
|
||||||
// Act
|
|
||||||
adapter.updateEInvoice(xmlTemplate, workitem);
|
adapter.updateEInvoice(xmlTemplate, workitem);
|
||||||
|
|
||||||
// Assert - basic checks
|
|
||||||
assertNotNull(xmlTemplate.getContent(), "XML content should not be null after update");
|
assertNotNull(xmlTemplate.getContent(), "XML content should not be null after update");
|
||||||
assertTrue(xmlTemplate.getContent().length > 0, "XML content should not be empty");
|
assertTrue(xmlTemplate.getContent().length > 0, "XML content should not be empty");
|
||||||
|
|
||||||
// Write output for manual inspection
|
|
||||||
writeOutputToResources(xmlTemplate, "invoice-simple.xml");
|
writeOutputToResources(xmlTemplate, "invoice-simple.xml");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erzeugt ein lokales XML File zum testen
|
* Correction invoice (KOR) in EUR with 23% tax rate.
|
||||||
|
* Expected: KOR header data, P_13_1, P_14_1, P_14_1W, KursWalutyZ.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Test Invoice Korrektur")
|
@DisplayName("Test Invoice Korrektur (EUR, 23%)")
|
||||||
public void testInvoiceKorektur() throws Exception {
|
public void testInvoiceKorektur() throws Exception {
|
||||||
logger.info("==> Test: Upload Invoice XML");
|
logger.info("==> Test: Invoice Korrektur");
|
||||||
|
|
||||||
// Prepare a workitem with typical invoice data
|
|
||||||
ItemCollection workitem = new ItemCollection();
|
ItemCollection workitem = new ItemCollection();
|
||||||
workitem.setItemValue("invoice.number", "FV/2025/001");
|
workitem.setItemValue("invoice.number", "FV/2025/001");
|
||||||
workitem.setItemValue("invoice.date", LocalDate.of(2025, 2, 10));
|
workitem.setItemValue("invoice.date", LocalDate.of(2025, 2, 10));
|
||||||
workitem.setItemValue("invoice.duedate", LocalDate.of(2025, 3, 10));
|
workitem.setItemValue("invoice.duedate", LocalDate.of(2025, 3, 10));
|
||||||
workitem.setItemValue("invoice.currency", "EUR");
|
workitem.setItemValue("invoice.currency", "EUR");
|
||||||
|
workitem.setItemValue("invoice.rate", 4.30);
|
||||||
workitem.setItemValue("invoice.total.net", 0.00);
|
workitem.setItemValue("invoice.total.net", 0.00);
|
||||||
workitem.setItemValue("invoice.total.tax", 0.0);
|
workitem.setItemValue("invoice.total.tax", 0.0);
|
||||||
workitem.setItemValue("invoice.total", 0.00);
|
workitem.setItemValue("invoice.total", 0.00);
|
||||||
|
|
@ -162,7 +171,7 @@ public class KSeFAdapterTest {
|
||||||
workitem.setItemValue("invoice.correction", true);
|
workitem.setItemValue("invoice.correction", true);
|
||||||
workitem.setItemValue("invoice.CorrectionInvoiceNumber", "6551");
|
workitem.setItemValue("invoice.CorrectionInvoiceNumber", "6551");
|
||||||
workitem.setItemValue("invoice.performancedate", new Date());
|
workitem.setItemValue("invoice.performancedate", new Date());
|
||||||
// Prepare child items (invoice line items)
|
|
||||||
List<Object> childItems = new ArrayList<>();
|
List<Object> childItems = new ArrayList<>();
|
||||||
|
|
||||||
ItemCollection lineItem1 = new ItemCollection()
|
ItemCollection lineItem1 = new ItemCollection()
|
||||||
|
|
@ -173,93 +182,194 @@ public class KSeFAdapterTest {
|
||||||
.setItemValue("datev.text", "Transport Berlin - Warsaw")
|
.setItemValue("datev.text", "Transport Berlin - Warsaw")
|
||||||
.setItemValue("billingtext", "usługa spedycyjna / transport w relacji, PL63 - CZ43")
|
.setItemValue("billingtext", "usługa spedycyjna / transport w relacji, PL63 - CZ43")
|
||||||
.setItemValue("datev.umsatz", -900.00)
|
.setItemValue("datev.umsatz", -900.00)
|
||||||
|
.setItemValue("datev.vatrate", 23.0)
|
||||||
|
.setItemValue("cargosoft.vat.code", "23")
|
||||||
.setItemValue("datev.wkz", "EUR")
|
.setItemValue("datev.wkz", "EUR")
|
||||||
.setItemValue("billingcode", "TRANSLKW");
|
.setItemValue("billingcode", "TRANSLKW");
|
||||||
|
|
||||||
childItems.add(lineItem1.getAllItems());
|
childItems.add(lineItem1.getAllItems());
|
||||||
|
|
||||||
/**
|
|
||||||
* <NrWierszaFa>1</NrWierszaFa>
|
|
||||||
* <UU_ID>7f846436-7c29-48b3-8f5a-fed996ea30a3</UU_ID>
|
|
||||||
* <P_7>IM-POL-2512-008</P_7>
|
|
||||||
* <P_8A>szt.</P_8A>
|
|
||||||
* <P_8B>1</P_8B>
|
|
||||||
* <P_9A>0.00</P_9A>
|
|
||||||
* <P_11>0.00</P_11>
|
|
||||||
* </FaWiersz>
|
|
||||||
* <FaWiersz>
|
|
||||||
* <NrWierszaFa>2</NrWierszaFa>
|
|
||||||
* <UU_ID>f3077e37-9198-4431-a82b-68d9439b587e</UU_ID>
|
|
||||||
* <P_7>IM-POL-2512-008</P_7>
|
|
||||||
* <P_8A>szt.</P_8A>
|
|
||||||
* <P_8B>1</P_8B>
|
|
||||||
* <P_9A>-900.00</P_9A>
|
|
||||||
* <P_11>-900.00</P_11>
|
|
||||||
* </FaWiersz>
|
|
||||||
* <FaWiersz>
|
|
||||||
* <NrWierszaFa>3</NrWierszaFa>
|
|
||||||
* <UU_ID>31fdfe9e-671e-42f6-9f7e-ceb227a3a42c</UU_ID>
|
|
||||||
* <P_7>IM-POL-2512-008</P_7>
|
|
||||||
* <P_8A>szt.</P_8A>
|
|
||||||
* <P_8B>1</P_8B>
|
|
||||||
* <P_9A>0.00</P_9A>
|
|
||||||
* <P_11>0.00</P_11>
|
|
||||||
* </FaWiersz>
|
|
||||||
* <FaWiersz>
|
|
||||||
* <NrWierszaFa>4</NrWierszaFa>
|
|
||||||
* <UU_ID>79ffd1e8-be87-478b-97b5-d37aa302d090</UU_ID>
|
|
||||||
* <P_7>IM-POL-2512-008</P_7>
|
|
||||||
* <P_8A>szt.</P_8A>
|
|
||||||
* <P_8B>1</P_8B>
|
|
||||||
* <P_9A>0.00</P_9A>
|
|
||||||
* <P_11>0.00</P_11>
|
|
||||||
* </FaWiersz>
|
|
||||||
* <FaWiersz>
|
|
||||||
* <NrWierszaFa>5</NrWierszaFa>
|
|
||||||
* <UU_ID>38e8cbd3-e558-4b0e-875c-c4f2d96d1659</UU_ID>
|
|
||||||
* <P_7>IM-POL-2512-008</P_7>
|
|
||||||
* <P_8A>szt.</P_8A>
|
|
||||||
* <P_8B>1</P_8B>
|
|
||||||
* <P_9A>900.00</P_9A>
|
|
||||||
* <P_11>900.00</P_11>
|
|
||||||
* </FaWiersz>
|
|
||||||
* <FaWiersz>
|
|
||||||
* <NrWierszaFa>6</NrWierszaFa>
|
|
||||||
* <UU_ID>3774dab3-b6c4-433e-8298-60ecf6cbab74</UU_ID>
|
|
||||||
* <P_7>IM-POL-2512-008</P_7>
|
|
||||||
* <P_8A>szt.</P_8A>
|
|
||||||
* <P_8B>1</P_8B>
|
|
||||||
* <P_9A>0.00</P_9A>
|
|
||||||
* <P_11>0.00</P_11>
|
|
||||||
* </FaWiersz>
|
|
||||||
* <FaWiersz>
|
|
||||||
* <NrWierszaFa>7</NrWierszaFa>
|
|
||||||
* <UU_ID>f19846ab-3567-4e5d-823b-807d474af062</UU_ID>
|
|
||||||
* <P_7>IM-POL-2512-008</P_7>
|
|
||||||
* <P_8A>szt.</P_8A>
|
|
||||||
* <P_8B>1</P_8B>
|
|
||||||
* <P_9A>0.00</P_9A>
|
|
||||||
* <P_11>0.00</P_11>
|
|
||||||
*/
|
|
||||||
|
|
||||||
workitem.setItemValue("_childitems", childItems);
|
workitem.setItemValue("_childitems", childItems);
|
||||||
|
|
||||||
// Arrange
|
|
||||||
when(businessPartnerService.getBusinessPartnerByID("BP-001"))
|
when(businessPartnerService.getBusinessPartnerByID("BP-001"))
|
||||||
.thenReturn(businessPartner);
|
.thenReturn(businessPartner);
|
||||||
|
|
||||||
FileData xmlTemplate = loadTemplateFromResources(TEMPLATE_FILE);
|
FileData xmlTemplate = loadTemplateFromResources(TEMPLATE_FILE);
|
||||||
|
|
||||||
// Act
|
|
||||||
adapter.updateEInvoice(xmlTemplate, workitem);
|
adapter.updateEInvoice(xmlTemplate, workitem);
|
||||||
|
|
||||||
// Assert - basic checks
|
|
||||||
assertNotNull(xmlTemplate.getContent(), "XML content should not be null after update");
|
assertNotNull(xmlTemplate.getContent(), "XML content should not be null after update");
|
||||||
assertTrue(xmlTemplate.getContent().length > 0, "XML content should not be empty");
|
assertTrue(xmlTemplate.getContent().length > 0, "XML content should not be empty");
|
||||||
|
|
||||||
// Write output for manual inspection
|
|
||||||
writeOutputToResources(xmlTemplate, "test-korrekturrechnung.xml");
|
writeOutputToResources(xmlTemplate, "test-korrekturrechnung.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.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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EU B2B service (Art. 28b UStG-PL) in EUR.
|
||||||
|
* <p>
|
||||||
|
* This is the case from the tax advisor mapping document (invoice 7470)
|
||||||
|
* where code "NP" must map to {@code <P_12>np II</P_12>} and
|
||||||
|
* {@code <P_13_9>}, NOT to {@code <P_13_8>}. The destination field drives
|
||||||
|
* correct VAT-UE / ZM reporting in JPK_V7.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@DisplayName("Test EU B2B Service (EUR, NP / np II)")
|
||||||
|
public void testEUService_NP() throws Exception {
|
||||||
|
logger.info("==> Test: EU B2B Service");
|
||||||
|
|
||||||
|
// German EU customer
|
||||||
|
ItemCollection germanPartner = new ItemCollection();
|
||||||
|
germanPartner.setItemValue("partner.name", "Muster GmbH");
|
||||||
|
germanPartner.setItemValue("partner.country", "DE");
|
||||||
|
germanPartner.setItemValue("partner.city", "Bremen");
|
||||||
|
germanPartner.setItemValue("partner.zip", "28195");
|
||||||
|
germanPartner.setItemValue("partner.address", "Museumstr. 2-6");
|
||||||
|
germanPartner.setItemValue("partner.vat", "DE310968950");
|
||||||
|
germanPartner.setItemValue("dbtr.number", "D-18804");
|
||||||
|
|
||||||
|
ItemCollection workitem = new ItemCollection();
|
||||||
|
workitem.setItemValue("invoice.number", "FV/2025/7470");
|
||||||
|
workitem.setItemValue("invoice.date", LocalDate.of(2025, 2, 10));
|
||||||
|
workitem.setItemValue("invoice.duedate", LocalDate.of(2025, 3, 10));
|
||||||
|
workitem.setItemValue("invoice.currency", "EUR");
|
||||||
|
workitem.setItemValue("invoice.rate", 4.30);
|
||||||
|
workitem.setItemValue("invoice.correction", "false");
|
||||||
|
workitem.setItemValue("invoice.CorrectionInvoiceNumber", "");
|
||||||
|
workitem.setItemValue("partner.id", "BP-DE-001");
|
||||||
|
workitem.setItemValue("partner.vat", "DE310968950");
|
||||||
|
workitem.setItemValue("invoice.performancedate", new Date());
|
||||||
|
|
||||||
|
List<Object> childItems = new ArrayList<>();
|
||||||
|
|
||||||
|
ItemCollection lineItem1 = new ItemCollection()
|
||||||
|
.setItemValue("numpos", "1")
|
||||||
|
.setItemValue("datev.text", "Transport DE-PL")
|
||||||
|
.setItemValue("billingtext", "International transport service Germany - Poland")
|
||||||
|
.setItemValue("datev.umsatz", 4620.00)
|
||||||
|
.setItemValue("datev.vatrate", 0.0)
|
||||||
|
.setItemValue("cargosoft.vat.code", "NP");
|
||||||
|
childItems.add(lineItem1.getAllItems());
|
||||||
|
|
||||||
|
workitem.setItemValue("_childitems", childItems);
|
||||||
|
|
||||||
|
when(businessPartnerService.getBusinessPartnerByID("BP-DE-001"))
|
||||||
|
.thenReturn(germanPartner);
|
||||||
|
|
||||||
|
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-eu-service-np.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Negative test: an unknown CargoSoft VAT code (here the mixed code "023"
|
||||||
|
* which is intentionally not mapped) must abort processing with a
|
||||||
|
* PluginException. This guards against silently producing JPK_V7-invalid
|
||||||
|
* invoices when CargoSoft is extended with a new code.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@DisplayName("Test Unknown CargoSoft Code raises PluginException")
|
||||||
|
public void testUnknownTaxCode() throws Exception {
|
||||||
|
logger.info("==> Test: Unknown CargoSoft Code");
|
||||||
|
|
||||||
|
ItemCollection workitem = new ItemCollection();
|
||||||
|
workitem.setItemValue("invoice.number", "FV/2025/999");
|
||||||
|
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.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<>();
|
||||||
|
ItemCollection lineItem = new ItemCollection()
|
||||||
|
.setItemValue("numpos", "1")
|
||||||
|
.setItemValue("datev.text", "Mixed code position")
|
||||||
|
.setItemValue("billingtext", "Mixed code position")
|
||||||
|
.setItemValue("datev.umsatz", 1000.00)
|
||||||
|
.setItemValue("datev.vatrate", 23.0)
|
||||||
|
// 023 is the CargoSoft mixed code - NOT mapped, expected to fail
|
||||||
|
.setItemValue("cargosoft.vat.code", "023");
|
||||||
|
childItems.add(lineItem.getAllItems());
|
||||||
|
|
||||||
|
workitem.setItemValue("_childitems", childItems);
|
||||||
|
|
||||||
|
when(businessPartnerService.getBusinessPartnerByID("BP-001"))
|
||||||
|
.thenReturn(businessPartner);
|
||||||
|
|
||||||
|
FileData xmlTemplate = loadTemplateFromResources(TEMPLATE_FILE);
|
||||||
|
|
||||||
|
// Expect a PluginException with TAX_CODE_ERROR error code
|
||||||
|
assertThrows(PluginException.class,
|
||||||
|
() -> adapter.updateEInvoice(xmlTemplate, workitem),
|
||||||
|
"Unknown CargoSoft VAT code must raise a PluginException");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helper methods ──────────────────────────────────────────────
|
// ── Helper methods ──────────────────────────────────────────────
|
||||||
|
|
@ -293,4 +403,4 @@ public class KSeFAdapterTest {
|
||||||
System.out.println("Output written to: " + outputPath.toAbsolutePath());
|
System.out.println("Output written to: " + outputPath.toAbsolutePath());
|
||||||
System.out.println("──────────────────────────────────────────────");
|
System.out.println("──────────────────────────────────────────────");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Faktura xmlns="http://crd.gov.pl/wzor/2025/06/25/13775/"
|
||||||
|
xmlns:etd="http://crd.gov.pl/xml/schematy/dziedzinowe/mf/2022/01/05/eD/DefinicjeTypy/"
|
||||||
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<Naglowek>
|
||||||
|
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
|
||||||
|
<WariantFormularza>3</WariantFormularza>
|
||||||
|
<DataWytworzeniaFa>2026-04-27T22:36:23.680605Z</DataWytworzeniaFa>
|
||||||
|
<SystemInfo>Imixs eInvoice</SystemInfo>
|
||||||
|
</Naglowek>
|
||||||
|
<!-- Seller (Your Polish Company - Pre-filled) -->
|
||||||
|
<Podmiot1>
|
||||||
|
<PrefiksPodatnika>PL</PrefiksPodatnika>
|
||||||
|
<DaneIdentyfikacyjne>
|
||||||
|
<NIP>9552521552</NIP>
|
||||||
|
<Nazwa>ALEXANDER GLOBAL LOGISTICS POLAND sp. z o.o.</Nazwa>
|
||||||
|
</DaneIdentyfikacyjne>
|
||||||
|
<Adres>
|
||||||
|
<KodKraju>PL</KodKraju>
|
||||||
|
<AdresL1>ul. Gdańska 36, 70-660 Szczecin</AdresL1>
|
||||||
|
</Adres>
|
||||||
|
<DaneKontaktowe>
|
||||||
|
<Email>MBudas@alexander-logistics.com</Email>
|
||||||
|
</DaneKontaktowe>
|
||||||
|
</Podmiot1>
|
||||||
|
<!-- Buyer (Customer - Empty, to be filled) -->
|
||||||
|
<Podmiot2>
|
||||||
|
<DaneIdentyfikacyjne>
|
||||||
|
<!-- NIP falls eine PL Ust Vorliegt
|
||||||
|
ansonsten NrID - geht immer -->
|
||||||
|
<!-- <Nazwa>#Customer Name#</Nazwa> -->
|
||||||
|
<NrID>DE310968950</NrID>
|
||||||
|
<Nazwa>Muster GmbH</Nazwa>
|
||||||
|
</DaneIdentyfikacyjne>
|
||||||
|
<Adres>
|
||||||
|
<KodKraju>DE</KodKraju>
|
||||||
|
<AdresL1>Museumstr. 2-6</AdresL1>
|
||||||
|
<AdresL2>28195 Bremen</AdresL2>
|
||||||
|
</Adres>
|
||||||
|
<!---
|
||||||
|
Contact information from buyer can be left
|
||||||
|
<DaneKontaktowe>
|
||||||
|
<Email></Email>
|
||||||
|
<Telefon></Telefon>
|
||||||
|
</DaneKontaktowe>
|
||||||
|
-->
|
||||||
|
<NrKlienta>D-18804</NrKlienta>
|
||||||
|
<JST>2</JST>
|
||||||
|
<!-- fixed -->
|
||||||
|
<GV>2</GV>
|
||||||
|
<!-- fixed -->
|
||||||
|
</Podmiot2>
|
||||||
|
<!-- Invoice Data -->
|
||||||
|
<Fa>
|
||||||
|
<KodWaluty>EUR</KodWaluty>
|
||||||
|
<P_1>2025-02-10</P_1>
|
||||||
|
<!-- Invoice Date -->
|
||||||
|
<P_1M>Szczecin</P_1M>
|
||||||
|
<P_2>FV/2025/7470</P_2>
|
||||||
|
<!-- Invoice Number -->
|
||||||
|
<P_6>2026-04-27</P_6>
|
||||||
|
<!-- Due Date -->
|
||||||
|
<!-- Totals will only be computed by the EInvoiceModelKSeF Class-->
|
||||||
|
<!--
|
||||||
|
<P_13_1>0.00</P_13_1>
|
||||||
|
<P_14_1>0.00</P_14_1>
|
||||||
|
<P_15>0.00</P_15>
|
||||||
|
-->
|
||||||
|
<Adnotacje>
|
||||||
|
<!-- 1 yes - 2 no -->
|
||||||
|
<P_16>2</P_16>
|
||||||
|
<!-- Keine Selbstfakturierung -->
|
||||||
|
<P_17>2</P_17>
|
||||||
|
<!-- Kein Reverse Charge -->
|
||||||
|
<P_18>2</P_18>
|
||||||
|
<!-- Kein Split Payment (Rechnung unter 15.000 PLN/EUR) -->
|
||||||
|
<P_18A>2</P_18A>
|
||||||
|
<!-- Keine Steuerbefreiung -->
|
||||||
|
<Zwolnienie>
|
||||||
|
<P_19N>1</P_19N>
|
||||||
|
</Zwolnienie>
|
||||||
|
<!-- Keine neuen Verkehrsmittel -->
|
||||||
|
<NoweSrodkiTransportu>
|
||||||
|
<P_22N>1</P_22N>
|
||||||
|
</NoweSrodkiTransportu>
|
||||||
|
<!-- Kein vereinfachtes Verfahren nach Art. 135 -->
|
||||||
|
<P_23>2</P_23>
|
||||||
|
<!-- Keine Margenregelung -->
|
||||||
|
<PMarzy>
|
||||||
|
<P_PMarzyN>1</P_PMarzyN>
|
||||||
|
</PMarzy>
|
||||||
|
</Adnotacje>
|
||||||
|
<!-- Invoice Type (VAT) -->
|
||||||
|
<RodzajFaktury>VAT</RodzajFaktury>
|
||||||
|
<!-- Invoice Positions: FaWiersz -->
|
||||||
|
<FaWiersz>
|
||||||
|
<NrWierszaFa>1</NrWierszaFa>
|
||||||
|
<P_7>International transport service Germany - Poland</P_7>
|
||||||
|
<P_8A>szt.</P_8A>
|
||||||
|
<P_8B>1.00</P_8B>
|
||||||
|
<P_9A>4620.00</P_9A>
|
||||||
|
<P_11>4620.00</P_11>
|
||||||
|
<P_11Vat>0.00</P_11Vat>
|
||||||
|
<P_11A>4620.00</P_11A>
|
||||||
|
<P_12>np II</P_12>
|
||||||
|
</FaWiersz>
|
||||||
|
<Platnosc>
|
||||||
|
|
||||||
|
<!-- Setzen der Zahlungsart (immer 6)
|
||||||
|
* 1 – Barzahlung
|
||||||
|
* 2 – Karte
|
||||||
|
* 3 – Gutschein
|
||||||
|
* 4 – Scheck
|
||||||
|
* 5 – Kredit
|
||||||
|
* 6 – Überweisung
|
||||||
|
* 7 – Mobilzahlung
|
||||||
|
-->
|
||||||
|
<TerminPlatnosci>
|
||||||
|
<Termin>2025-03-10</Termin>
|
||||||
|
</TerminPlatnosci>
|
||||||
|
<FormaPlatnosci>6</FormaPlatnosci>
|
||||||
|
<!-- BANKEN-->
|
||||||
|
<RachunekBankowy>
|
||||||
|
<NrRB>PL79116022020000000654306674</NrRB>
|
||||||
|
<SWIFT>BIGBPLPWXXX</SWIFT>
|
||||||
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
|
<OpisRachunku>Konto PLN</OpisRachunku>
|
||||||
|
</RachunekBankowy>
|
||||||
|
<RachunekBankowy>
|
||||||
|
<NrRB>PL16116022020000000654910469</NrRB>
|
||||||
|
<SWIFT>BIGBPLPWXXX</SWIFT>
|
||||||
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
|
<OpisRachunku>Konto EUR</OpisRachunku>
|
||||||
|
</RachunekBankowy>
|
||||||
|
<RachunekBankowy>
|
||||||
|
<NrRB>PL72116022020000000654911595</NrRB>
|
||||||
|
<SWIFT>BIGBPLPWXXX</SWIFT>
|
||||||
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
|
<OpisRachunku>Konto USD</OpisRachunku>
|
||||||
|
</RachunekBankowy>
|
||||||
|
</Platnosc>
|
||||||
|
<P_13_9>4620.00</P_13_9>
|
||||||
|
<P_15>4620.00</P_15>
|
||||||
|
<KursWalutyZ>4.3</KursWalutyZ>
|
||||||
|
</Fa>
|
||||||
|
</Faktura>
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Faktura xmlns="http://crd.gov.pl/wzor/2025/06/25/13775/"
|
||||||
|
xmlns:etd="http://crd.gov.pl/xml/schematy/dziedzinowe/mf/2022/01/05/eD/DefinicjeTypy/"
|
||||||
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<Naglowek>
|
||||||
|
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
|
||||||
|
<WariantFormularza>3</WariantFormularza>
|
||||||
|
<DataWytworzeniaFa>2026-04-27T22:36:23.965831Z</DataWytworzeniaFa>
|
||||||
|
<SystemInfo>Imixs eInvoice</SystemInfo>
|
||||||
|
</Naglowek>
|
||||||
|
<!-- Seller (Your Polish Company - Pre-filled) -->
|
||||||
|
<Podmiot1>
|
||||||
|
<PrefiksPodatnika>PL</PrefiksPodatnika>
|
||||||
|
<DaneIdentyfikacyjne>
|
||||||
|
<NIP>9552521552</NIP>
|
||||||
|
<Nazwa>ALEXANDER GLOBAL LOGISTICS POLAND sp. z o.o.</Nazwa>
|
||||||
|
</DaneIdentyfikacyjne>
|
||||||
|
<Adres>
|
||||||
|
<KodKraju>PL</KodKraju>
|
||||||
|
<AdresL1>ul. Gdańska 36, 70-660 Szczecin</AdresL1>
|
||||||
|
</Adres>
|
||||||
|
<DaneKontaktowe>
|
||||||
|
<Email>MBudas@alexander-logistics.com</Email>
|
||||||
|
</DaneKontaktowe>
|
||||||
|
</Podmiot1>
|
||||||
|
<!-- Buyer (Customer - Empty, to be filled) -->
|
||||||
|
<Podmiot2>
|
||||||
|
<DaneIdentyfikacyjne>
|
||||||
|
<!-- NIP falls eine PL Ust Vorliegt
|
||||||
|
ansonsten NrID - geht immer -->
|
||||||
|
<!-- <Nazwa>#Customer Name#</Nazwa> -->
|
||||||
|
<NIP>1234567890</NIP>
|
||||||
|
<Nazwa>Test Sp. z o.o.</Nazwa>
|
||||||
|
</DaneIdentyfikacyjne>
|
||||||
|
<Adres>
|
||||||
|
<KodKraju>PL</KodKraju>
|
||||||
|
<AdresL1>ul. Testowa 1</AdresL1>
|
||||||
|
<AdresL2>00-001 Warszawa</AdresL2>
|
||||||
|
</Adres>
|
||||||
|
<!---
|
||||||
|
Contact information from buyer can be left
|
||||||
|
<DaneKontaktowe>
|
||||||
|
<Email></Email>
|
||||||
|
<Telefon></Telefon>
|
||||||
|
</DaneKontaktowe>
|
||||||
|
-->
|
||||||
|
<NrKlienta>D-12345</NrKlienta>
|
||||||
|
<JST>2</JST>
|
||||||
|
<!-- fixed -->
|
||||||
|
<GV>2</GV>
|
||||||
|
<!-- fixed -->
|
||||||
|
</Podmiot2>
|
||||||
|
<!-- Invoice Data -->
|
||||||
|
<Fa>
|
||||||
|
<KodWaluty>PLN</KodWaluty>
|
||||||
|
<P_1>2025-02-10</P_1>
|
||||||
|
<!-- Invoice Date -->
|
||||||
|
<P_1M>Szczecin</P_1M>
|
||||||
|
<P_2>FV/2025/7471</P_2>
|
||||||
|
<!-- Invoice Number -->
|
||||||
|
<P_6>2026-04-27</P_6>
|
||||||
|
<!-- Due Date -->
|
||||||
|
<!-- Totals will only be computed by the EInvoiceModelKSeF Class-->
|
||||||
|
<!--
|
||||||
|
<P_13_1>0.00</P_13_1>
|
||||||
|
<P_14_1>0.00</P_14_1>
|
||||||
|
<P_15>0.00</P_15>
|
||||||
|
-->
|
||||||
|
<Adnotacje>
|
||||||
|
<!-- 1 yes - 2 no -->
|
||||||
|
<P_16>2</P_16>
|
||||||
|
<!-- Keine Selbstfakturierung -->
|
||||||
|
<P_17>2</P_17>
|
||||||
|
<!-- Kein Reverse Charge -->
|
||||||
|
<P_18>2</P_18>
|
||||||
|
<!-- Kein Split Payment (Rechnung unter 15.000 PLN/EUR) -->
|
||||||
|
<P_18A>2</P_18A>
|
||||||
|
<!-- Keine Steuerbefreiung -->
|
||||||
|
<Zwolnienie>
|
||||||
|
<P_19N>1</P_19N>
|
||||||
|
</Zwolnienie>
|
||||||
|
<!-- Keine neuen Verkehrsmittel -->
|
||||||
|
<NoweSrodkiTransportu>
|
||||||
|
<P_22N>1</P_22N>
|
||||||
|
</NoweSrodkiTransportu>
|
||||||
|
<!-- Kein vereinfachtes Verfahren nach Art. 135 -->
|
||||||
|
<P_23>2</P_23>
|
||||||
|
<!-- Keine Margenregelung -->
|
||||||
|
<PMarzy>
|
||||||
|
<P_PMarzyN>1</P_PMarzyN>
|
||||||
|
</PMarzy>
|
||||||
|
</Adnotacje>
|
||||||
|
<!-- Invoice Type (VAT) -->
|
||||||
|
<RodzajFaktury>VAT</RodzajFaktury>
|
||||||
|
<!-- Invoice Positions: FaWiersz -->
|
||||||
|
<FaWiersz>
|
||||||
|
<NrWierszaFa>1</NrWierszaFa>
|
||||||
|
<P_7>Forwarding service for export shipment</P_7>
|
||||||
|
<P_8A>szt.</P_8A>
|
||||||
|
<P_8B>1.00</P_8B>
|
||||||
|
<P_9A>14040.00</P_9A>
|
||||||
|
<P_11>14040.00</P_11>
|
||||||
|
<P_11Vat>0.00</P_11Vat>
|
||||||
|
<P_11A>14040.00</P_11A>
|
||||||
|
<P_12>0 KR</P_12>
|
||||||
|
</FaWiersz>
|
||||||
|
<FaWiersz>
|
||||||
|
<NrWierszaFa>2</NrWierszaFa>
|
||||||
|
<P_7>Krajowa obsługa transportu</P_7>
|
||||||
|
<P_8A>szt.</P_8A>
|
||||||
|
<P_8B>1.00</P_8B>
|
||||||
|
<P_9A>3960.00</P_9A>
|
||||||
|
<P_11>3960.00</P_11>
|
||||||
|
<P_11Vat>910.80</P_11Vat>
|
||||||
|
<P_11A>4870.80</P_11A>
|
||||||
|
<P_12>23</P_12>
|
||||||
|
</FaWiersz>
|
||||||
|
<Platnosc>
|
||||||
|
|
||||||
|
<!-- Setzen der Zahlungsart (immer 6)
|
||||||
|
* 1 – Barzahlung
|
||||||
|
* 2 – Karte
|
||||||
|
* 3 – Gutschein
|
||||||
|
* 4 – Scheck
|
||||||
|
* 5 – Kredit
|
||||||
|
* 6 – Überweisung
|
||||||
|
* 7 – Mobilzahlung
|
||||||
|
-->
|
||||||
|
<TerminPlatnosci>
|
||||||
|
<Termin>2025-03-10</Termin>
|
||||||
|
</TerminPlatnosci>
|
||||||
|
<FormaPlatnosci>6</FormaPlatnosci>
|
||||||
|
<!-- BANKEN-->
|
||||||
|
<RachunekBankowy>
|
||||||
|
<NrRB>PL79116022020000000654306674</NrRB>
|
||||||
|
<SWIFT>BIGBPLPWXXX</SWIFT>
|
||||||
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
|
<OpisRachunku>Konto PLN</OpisRachunku>
|
||||||
|
</RachunekBankowy>
|
||||||
|
<RachunekBankowy>
|
||||||
|
<NrRB>PL16116022020000000654910469</NrRB>
|
||||||
|
<SWIFT>BIGBPLPWXXX</SWIFT>
|
||||||
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
|
<OpisRachunku>Konto EUR</OpisRachunku>
|
||||||
|
</RachunekBankowy>
|
||||||
|
<RachunekBankowy>
|
||||||
|
<NrRB>PL72116022020000000654911595</NrRB>
|
||||||
|
<SWIFT>BIGBPLPWXXX</SWIFT>
|
||||||
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
|
<OpisRachunku>Konto USD</OpisRachunku>
|
||||||
|
</RachunekBankowy>
|
||||||
|
</Platnosc>
|
||||||
|
<P_13_6_1>14040.00</P_13_6_1>
|
||||||
|
<P_13_1>3960.00</P_13_1>
|
||||||
|
<P_14_1>910.80</P_14_1>
|
||||||
|
<P_15>18910.80</P_15>
|
||||||
|
</Fa>
|
||||||
|
</Faktura>
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<Naglowek>
|
<Naglowek>
|
||||||
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
|
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
|
||||||
<WariantFormularza>3</WariantFormularza>
|
<WariantFormularza>3</WariantFormularza>
|
||||||
<DataWytworzeniaFa>2026-04-27T16:20:53.265515Z</DataWytworzeniaFa>
|
<DataWytworzeniaFa>2026-04-27T22:36:24.034379Z</DataWytworzeniaFa>
|
||||||
<SystemInfo>Imixs eInvoice</SystemInfo>
|
<SystemInfo>Imixs eInvoice</SystemInfo>
|
||||||
</Naglowek>
|
</Naglowek>
|
||||||
<!-- Seller (Your Polish Company - Pre-filled) -->
|
<!-- Seller (Your Polish Company - Pre-filled) -->
|
||||||
|
|
@ -67,9 +67,6 @@
|
||||||
<P_14_1>0.00</P_14_1>
|
<P_14_1>0.00</P_14_1>
|
||||||
<P_15>0.00</P_15>
|
<P_15>0.00</P_15>
|
||||||
-->
|
-->
|
||||||
<P_13_1>10000.00</P_13_1>
|
|
||||||
<P_14_1>2300.00</P_14_1>
|
|
||||||
<P_15>12300.00</P_15>
|
|
||||||
<Adnotacje>
|
<Adnotacje>
|
||||||
<!-- 1 yes - 2 no -->
|
<!-- 1 yes - 2 no -->
|
||||||
<P_16>2</P_16>
|
<P_16>2</P_16>
|
||||||
|
|
@ -99,17 +96,25 @@
|
||||||
<!-- Invoice Positions: FaWiersz -->
|
<!-- Invoice Positions: FaWiersz -->
|
||||||
<FaWiersz>
|
<FaWiersz>
|
||||||
<NrWierszaFa>1</NrWierszaFa>
|
<NrWierszaFa>1</NrWierszaFa>
|
||||||
|
<P_7>Transport Berlin - Warsaw</P_7>
|
||||||
<P_8A>szt.</P_8A>
|
<P_8A>szt.</P_8A>
|
||||||
<P_8B>1</P_8B>
|
<P_8B>1.00</P_8B>
|
||||||
<P_9A>6000.00</P_9A>
|
<P_9A>6000.00</P_9A>
|
||||||
<P_11>6000.00</P_11>
|
<P_11>6000.00</P_11>
|
||||||
|
<P_11Vat>1380.00</P_11Vat>
|
||||||
|
<P_11A>7380.00</P_11A>
|
||||||
|
<P_12>23</P_12>
|
||||||
</FaWiersz>
|
</FaWiersz>
|
||||||
<FaWiersz>
|
<FaWiersz>
|
||||||
<NrWierszaFa>2</NrWierszaFa>
|
<NrWierszaFa>2</NrWierszaFa>
|
||||||
|
<P_7>Customs handling</P_7>
|
||||||
<P_8A>szt.</P_8A>
|
<P_8A>szt.</P_8A>
|
||||||
<P_8B>1</P_8B>
|
<P_8B>1.00</P_8B>
|
||||||
<P_9A>4000.00</P_9A>
|
<P_9A>4000.00</P_9A>
|
||||||
<P_11>4000.00</P_11>
|
<P_11>4000.00</P_11>
|
||||||
|
<P_11Vat>920.00</P_11Vat>
|
||||||
|
<P_11A>4920.00</P_11A>
|
||||||
|
<P_12>23</P_12>
|
||||||
</FaWiersz>
|
</FaWiersz>
|
||||||
<Platnosc>
|
<Platnosc>
|
||||||
|
|
||||||
|
|
@ -139,6 +144,15 @@
|
||||||
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
<OpisRachunku>Konto EUR</OpisRachunku>
|
<OpisRachunku>Konto EUR</OpisRachunku>
|
||||||
</RachunekBankowy>
|
</RachunekBankowy>
|
||||||
|
<RachunekBankowy>
|
||||||
|
<NrRB>PL72116022020000000654911595</NrRB>
|
||||||
|
<SWIFT>BIGBPLPWXXX</SWIFT>
|
||||||
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
|
<OpisRachunku>Konto USD</OpisRachunku>
|
||||||
|
</RachunekBankowy>
|
||||||
</Platnosc>
|
</Platnosc>
|
||||||
|
<P_13_1>10000.00</P_13_1>
|
||||||
|
<P_14_1>2300.00</P_14_1>
|
||||||
|
<P_15>12300.00</P_15>
|
||||||
</Fa>
|
</Fa>
|
||||||
</Faktura>
|
</Faktura>
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,19 @@
|
||||||
<Naglowek>
|
<Naglowek>
|
||||||
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
|
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
|
||||||
<WariantFormularza>3</WariantFormularza>
|
<WariantFormularza>3</WariantFormularza>
|
||||||
<DataWytworzeniaFa>2026-04-16T06:22:32.198172335Z</DataWytworzeniaFa>
|
<DataWytworzeniaFa>2026-04-27T22:36:23.999774Z</DataWytworzeniaFa>
|
||||||
<SystemInfo>Imixs eInvoice</SystemInfo>
|
<SystemInfo>Imixs eInvoice</SystemInfo>
|
||||||
</Naglowek>
|
</Naglowek>
|
||||||
<!-- Seller (Your Polish Company - Pre-filled) -->
|
<!-- Seller (Your Polish Company - Pre-filled) -->
|
||||||
<Podmiot1>
|
<Podmiot1>
|
||||||
|
<PrefiksPodatnika>PL</PrefiksPodatnika>
|
||||||
<DaneIdentyfikacyjne>
|
<DaneIdentyfikacyjne>
|
||||||
<NIP>9552521552</NIP>
|
<NIP>9552521552</NIP>
|
||||||
<Nazwa>Alexander Global Logistics</Nazwa>
|
<Nazwa>ALEXANDER GLOBAL LOGISTICS POLAND sp. z o.o.</Nazwa>
|
||||||
</DaneIdentyfikacyjne>
|
</DaneIdentyfikacyjne>
|
||||||
<Adres>
|
<Adres>
|
||||||
<KodKraju>PL</KodKraju>
|
<KodKraju>PL</KodKraju>
|
||||||
<AdresL1>Gdanska 36</AdresL1>
|
<AdresL1>ul. Gdańska 36, 70-660 Szczecin</AdresL1>
|
||||||
<AdresL2>70-660 Szczecin</AdresL2>
|
|
||||||
</Adres>
|
</Adres>
|
||||||
<DaneKontaktowe>
|
<DaneKontaktowe>
|
||||||
<Email>MBudas@alexander-logistics.com</Email>
|
<Email>MBudas@alexander-logistics.com</Email>
|
||||||
|
|
@ -56,9 +56,10 @@
|
||||||
<KodWaluty>EUR</KodWaluty>
|
<KodWaluty>EUR</KodWaluty>
|
||||||
<P_1>2025-02-10</P_1>
|
<P_1>2025-02-10</P_1>
|
||||||
<!-- Invoice Date -->
|
<!-- Invoice Date -->
|
||||||
|
<P_1M>Szczecin</P_1M>
|
||||||
<P_2>FV/2025/001</P_2>
|
<P_2>FV/2025/001</P_2>
|
||||||
<!-- Invoice Number -->
|
<!-- Invoice Number -->
|
||||||
<P_6>2025-03-10</P_6>
|
<P_6>2026-04-27</P_6>
|
||||||
<!-- Due Date -->
|
<!-- Due Date -->
|
||||||
<!-- Totals will only be computed by the EInvoiceModelKSeF Class-->
|
<!-- Totals will only be computed by the EInvoiceModelKSeF Class-->
|
||||||
<!--
|
<!--
|
||||||
|
|
@ -66,9 +67,6 @@
|
||||||
<P_14_1>0.00</P_14_1>
|
<P_14_1>0.00</P_14_1>
|
||||||
<P_15>0.00</P_15>
|
<P_15>0.00</P_15>
|
||||||
-->
|
-->
|
||||||
<P_13_1>0.00</P_13_1>
|
|
||||||
<P_14_1>0.00</P_14_1>
|
|
||||||
<P_15>0.00</P_15>
|
|
||||||
<Adnotacje>
|
<Adnotacje>
|
||||||
<!-- 1 yes - 2 no -->
|
<!-- 1 yes - 2 no -->
|
||||||
<P_16>2</P_16>
|
<P_16>2</P_16>
|
||||||
|
|
@ -103,13 +101,54 @@
|
||||||
</DaneFaKorygowanej>
|
</DaneFaKorygowanej>
|
||||||
<FaWiersz>
|
<FaWiersz>
|
||||||
<NrWierszaFa>1</NrWierszaFa>
|
<NrWierszaFa>1</NrWierszaFa>
|
||||||
<UU_ID>d437ac36-9439-4bd6-96fb-0fe0b91fd32d</UU_ID>
|
|
||||||
<P_7>usługa spedycyjna / transport w relacji, PL63 - CZ43</P_7>
|
<P_7>usługa spedycyjna / transport w relacji, PL63 - CZ43</P_7>
|
||||||
<P_8A>szt.</P_8A>
|
<P_8A>szt.</P_8A>
|
||||||
<P_8B>1</P_8B>
|
<P_8B>1.00</P_8B>
|
||||||
<P_9A>-900.00</P_9A>
|
<P_9A>-900.00</P_9A>
|
||||||
<P_11>-900.00</P_11>
|
<P_11>-900.00</P_11>
|
||||||
<P_12>0</P_12>
|
<P_11Vat>-207.00</P_11Vat>
|
||||||
|
<P_11A>-1107.00</P_11A>
|
||||||
|
<P_12>23</P_12>
|
||||||
</FaWiersz>
|
</FaWiersz>
|
||||||
|
<Platnosc>
|
||||||
|
|
||||||
|
<!-- Setzen der Zahlungsart (immer 6)
|
||||||
|
* 1 – Barzahlung
|
||||||
|
* 2 – Karte
|
||||||
|
* 3 – Gutschein
|
||||||
|
* 4 – Scheck
|
||||||
|
* 5 – Kredit
|
||||||
|
* 6 – Überweisung
|
||||||
|
* 7 – Mobilzahlung
|
||||||
|
-->
|
||||||
|
<TerminPlatnosci>
|
||||||
|
<Termin>2025-03-10</Termin>
|
||||||
|
</TerminPlatnosci>
|
||||||
|
<FormaPlatnosci>6</FormaPlatnosci>
|
||||||
|
<!-- BANKEN-->
|
||||||
|
<RachunekBankowy>
|
||||||
|
<NrRB>PL79116022020000000654306674</NrRB>
|
||||||
|
<SWIFT>BIGBPLPWXXX</SWIFT>
|
||||||
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
|
<OpisRachunku>Konto PLN</OpisRachunku>
|
||||||
|
</RachunekBankowy>
|
||||||
|
<RachunekBankowy>
|
||||||
|
<NrRB>PL16116022020000000654910469</NrRB>
|
||||||
|
<SWIFT>BIGBPLPWXXX</SWIFT>
|
||||||
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
|
<OpisRachunku>Konto EUR</OpisRachunku>
|
||||||
|
</RachunekBankowy>
|
||||||
|
<RachunekBankowy>
|
||||||
|
<NrRB>PL72116022020000000654911595</NrRB>
|
||||||
|
<SWIFT>BIGBPLPWXXX</SWIFT>
|
||||||
|
<NazwaBanku>Bank Millennium S.A.</NazwaBanku>
|
||||||
|
<OpisRachunku>Konto USD</OpisRachunku>
|
||||||
|
</RachunekBankowy>
|
||||||
|
</Platnosc>
|
||||||
|
<P_13_1>-900.00</P_13_1>
|
||||||
|
<P_14_1>-207.00</P_14_1>
|
||||||
|
<P_14_1W>-890.10</P_14_1W>
|
||||||
|
<P_15>-1107.00</P_15>
|
||||||
|
<KursWalutyZ>4.3</KursWalutyZ>
|
||||||
</Fa>
|
</Fa>
|
||||||
</Faktura>
|
</Faktura>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue