diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java index cd1a0fc..dc3a26a 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java @@ -38,6 +38,12 @@ public class InvoiceUtil { return bd.doubleValue(); } + public static BigDecimal roundBigDecimal(double value) { + BigDecimal bd = BigDecimal.valueOf(value); + bd = bd.setScale(2, RoundingMode.HALF_UP); + return bd; + } + /** * Rounds a Flat Value * diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java index 94b4374..a8c20aa 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/EInvoiceAdapter.java @@ -224,9 +224,9 @@ public class EInvoiceAdapter implements SignalAdapter { // Update Rechnungssummen... if (workitem.getItemValueDouble("invoice.total.tax") > 0) { // wir haben eine Steuer! - model.setTaxTotalAmount(InvoiceUtil.round(workitem.getItemValueDouble("invoice.total") + model.setTaxTotalAmount(InvoiceUtil.roundBigDecimal(workitem.getItemValueDouble("invoice.total") - workitem.getItemValueDouble("invoice.total.net"))); - model.setTaxRate(workitem.getItemValueDouble("invoice.total.tax")); + model.setTaxRate(InvoiceUtil.roundBigDecimal(workitem.getItemValueDouble("invoice.total.tax"))); } // Update Invoice Items @@ -234,13 +234,13 @@ public class EInvoiceAdapter implements SignalAdapter { double lineTotalAmount = 0.00; for (ItemCollection invoiceItem : invoiceItems) { TradeLineItem tradeLineItem = buildTradeLineItem(invoiceItem); - model.setTradeLineItem(tradeLineItem); + model.addTradeLineItem(tradeLineItem); lineTotalAmount = lineTotalAmount + tradeLineItem.getTotal(); } // // Summenbildung - model.setNetTotalAmount(workitem.getItemValueDouble("invoice.total.net")); - model.setGrandTotalAmount(workitem.getItemValueDouble("invoice.total")); + model.setNetTotalAmount(InvoiceUtil.roundBigDecimal(workitem.getItemValueDouble("invoice.total.net"))); + model.setGrandTotalAmount(InvoiceUtil.roundBigDecimal(workitem.getItemValueDouble("invoice.total"))); // date model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date")); diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapterDeprecated.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapterDeprecated.java deleted file mode 100644 index 68cdcd7..0000000 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAdapterDeprecated.java +++ /dev/null @@ -1,600 +0,0 @@ -package com.alexanderlogistics.einvoice; - -import java.io.ByteArrayInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.text.NumberFormat; -import java.text.SimpleDateFormat; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.logging.Logger; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.TransformerException; - -import org.imixs.archive.core.SnapshotService; -import org.imixs.einvoice.EInvoiceFormatException; -import org.imixs.einvoice.EInvoiceModel; -import org.imixs.einvoice.EInvoiceModelFactory; -import org.imixs.einvoice.EInvoiceModelKSeF; -import org.imixs.einvoice.EInvoiceNS; -import org.imixs.einvoice.TradeLineItem; -import org.imixs.einvoice.TradeParty; -import org.imixs.workflow.FileData; -import org.imixs.workflow.ItemCollection; -import org.imixs.workflow.SignalAdapter; -import org.imixs.workflow.engine.DocumentService; -import org.imixs.workflow.engine.WorkflowService; -import org.imixs.workflow.exceptions.AdapterException; -import org.imixs.workflow.exceptions.PluginException; -import org.imixs.workflow.exceptions.QueryException; -import org.imixs.workflow.util.XMLParser; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import com.alexanderlogistics.BusinessPartnerService; -import com.alexanderlogistics.InvoiceService; -import com.alexanderlogistics.InvoiceUtil; -import com.alexanderlogistics.xml.CargosoftXMLInvoiceImportService; - -import jakarta.inject.Inject; - -/** - * The KSeFAdapter converts a Cargsoft outbound invoice into an KSeF XML - * e-invoice and sends the xml file to the polish KSeF API - * - * The adapter can be configured by the model: - * - *
- * {@code
-
- textblock-ref
- filename
- true
-
-
- }
- *
- *
- * - * NIP: The adapter needs the NIP (vat-id). If we do not find the partner.vat in - * the business object, we do a lookup on the D-Cargosoft object and try the vat - * id from there. This is because the partner.vat is a field which was not - * defined before. - * - *
- * Wenn Debug = true gibt der Adapter alle einzelschritte auf der console aus
- * und erzeugt zusätzlich die factur-x xml und txt dateien.
- *
- *
- *
- * @version 1.0
- * @author rsoika
- */
-public class KSeFAdapterDeprecated implements SignalAdapter {
-
- final String TYPE_TEXTBLOCK = "textblock";
- public static final String DOCUMENT_ERROR = "DOCUMENT_ERROR";
- public static final String CONFIG_ERROR = "CONFIG_ERROR";
- public static final String API_ERROR = "API_ERROR";
- public static final String BUSINESSPARTNER_ERROR = "BUSINESSPARTNER_ERROR";
- public static final String LINE_ITEMS_PROPERTY = "invoice.items";
- private static Logger logger = Logger.getLogger(EInvoiceAdapter.class.getName());
-
- public static SimpleDateFormat dateFormatter = new SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN);
- public static NumberFormat numberFormat = NumberFormat.getInstance(Locale.GERMANY);
- boolean debug = false;
-
- @Inject
- WorkflowService workflowService;
-
- @Inject
- DocumentService documentService;
-
- @Inject
- SnapshotService snapshotService;
-
- @Inject
- BusinessPartnerService businessPartnerService;
-
- @Inject
- InvoiceService invoiceService;
-
- /**
- * This method
- *
- * @throws PluginException
- */
- @Override
- public ItemCollection execute(ItemCollection workitem, ItemCollection event)
- throws AdapterException, PluginException {
-
- logger.info("├── 🔜 Convert Invoice to KSeF...");
- // read configuration....
- ItemCollection eInvoiceConfig = workflowService.evalWorkflowResult(event, "ksef",
- workitem,
- false);
- if (eInvoiceConfig == null || !eInvoiceConfig.hasItem("CREATE")) {
- throw new PluginException(EInvoiceAdapter.class.getSimpleName(), CONFIG_ERROR,
- "missing e-invoice/ksef configuration in model event - please check model configuration");
- }
- ItemCollection ksefCreateDefinition = XMLParser.parseItemStructure(eInvoiceConfig.getItemValueString("CREATE"));
- try {
-
- // Load the e-invoice template....
- FileData xmlFileData = loadXMLTemplate(workitem, ksefCreateDefinition);
-
- updateEInvoice(xmlFileData, workitem);
-
- // append XML document
- logger.info("│ ├── attach KSeF e-invoice...");
- workitem.addFileData(xmlFileData);
-
- } catch (PluginException e) {
- throw new AdapterException(e);
- }
-
- return workitem;
- }
-
- /**
- * This method updates an e-invoice template with the data stored in the
- * workitem.
- *
- * First the method loads an EInvoiceModel based on the provided XML Template
- * and than updates the e-invoice data based on the items stored in the given
- * workitem.
- *
- * @param workitem
- * @throws PluginException
- */
- public void updateEInvoice(FileData fileDataXMLTemplate, ItemCollection workitem) throws PluginException {
-
- try {
- EInvoiceModel model = EInvoiceModelFactory.read(new ByteArrayInputStream(fileDataXMLTemplate.getContent()));
-
- model.setId(workitem.getItemValueString("invoice.number"));
-
- // date
- model.setIssueDateTime(workitem.getItemValueLocalDate("invoice.date"));
-
- // Set Performance Date
- if (workitem.getItemValueDate("invoice.performancedate") == null) {
- // hilfs code um das invoice.performancedate nachträglich zu parsen
- syncPerformanceDate(workitem);
- }
- ((EInvoiceModelKSeF) model)
- .setPerformanceDateTime(workitem.getItemValueLocalDate("invoice.performancedate"));
-
- // Update Addresses
- TradeParty billingAddress = buildAddress(workitem, "buyer", model);
- model.setTradeParty(billingAddress);
-
- // set Tax Type based on vatID - as a result taxType is 1=Poland 2=EU 3=Other
- ((EInvoiceModelKSeF) model).setTaxType(workitem.getItemValueString("partner.vat"));
- workitem.setItemValue("invoice.tax.type", ((EInvoiceModelKSeF) model).getTaxType());
-
- // Update Invoice Type (RodzajFaktury) -> VAT | KOR
- Element elementFa = model.findOrCreateChildNode(model.getRoot(), EInvoiceNS.KSEF, "Fa");
-
- if (isKorrekturRechnung(workitem)) {
- ((EInvoiceModelKSeF) model).setRodzajFaktury("KOR");
- logger.info("│ ├── invoice type=KOR");
-
- // Set correction data (DaneFaKorygowanej) - required for KOR invoices
- Element daneFaKorygowanej = model.findOrCreateChildNodeAfter(
- elementFa, EInvoiceNS.KSEF, "DaneFaKorygowanej", "RodzajFaktury");
-
- Date correctionInvoiceDate = workitem.getItemValueDate("invoice.date");
- // try to lookup original invoice by invoice.CorrectionInvoiceNumber
- ItemCollection correctionInvoice = invoiceService
- .findOutboundInvoiceByNumber(workitem.getItemValueString("invoice.CorrectionInvoiceNumber"));
- if (correctionInvoice != null) {
- correctionInvoiceDate = correctionInvoice.getItemValueDate("invoice.date");
- logger.info("│ ├── found original invoice, date: " + correctionInvoiceDate);
- } else {
- logger.info("│ ├── ⚠️ original invoice not found, using current date as fallback");
- }
- // Original invoice date (DataWystFaKorygowanej) - required
- if (correctionInvoiceDate != null) {
- SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
- String sCorrectionDate = formatter.format(correctionInvoiceDate);
- model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF,
- "DataWystFaKorygowanej", sCorrectionDate);
- }
-
- // Original invoice number (NrFaKorygowanej) - required
- model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF,
- "NrFaKorygowanej", workitem.getItemValueString("invoice.CorrectionInvoiceNumber"));
-
- // NrKSeFN = 1 means the original invoice was issued outside KSeF
- model.updateElementValue(daneFaKorygowanej, EInvoiceNS.KSEF, "NrKSeFN", "1");
-
- } else {
- // normale rechnung
- ((EInvoiceModelKSeF) model).setRodzajFaktury("VAT");
- logger.info("│ ├── invoice type=VAT");
- }
-
- // set currency
- model.updateElementValue(elementFa, EInvoiceNS.KSEF, "KodWaluty",
- workitem.getItemValueString("invoice.currency"));
-
- // Update Invoice Items
- // Here we compute the pos number independent if the _childitems hold a posnum.
- // This is because a new imported cargosoft invoice does not provide numPos
- // items in the child list. So it is important to generate the pos numbers here
- // one by one
- List
+ * This is the case from the tax advisor mapping document (invoice 7471)
+ * where the previous implementation incorrectly aggregated all amounts
+ * into a single field. Expected output: P_13_1+P_14_1 (for the 23%
+ * position) AND P_13_6_1 (for the 0% position) populated separately.
+ */
+ @Test
+ @DisplayName("Test Mixed Tax Rates (PLN, 0% KR + 23%)")
+ public void testMixedTaxRates() throws Exception {
+ logger.info("==> Test: Mixed Tax Rates");
+
+ ItemCollection workitem = new ItemCollection();
+ workitem.setItemValue("invoice.number", "FV/2025/7471");
+ workitem.setItemValue("invoice.date", LocalDate.of(2025, 2, 10));
+ workitem.setItemValue("invoice.duedate", LocalDate.of(2025, 3, 10));
+ workitem.setItemValue("invoice.currency", "PLN");
+ // workitem.setItemValue("invoice.total.net", 18000.00);
+ // workitem.setItemValue("invoice.total.tax", 23.0);
+ workitem.setItemValue("invoice.total", 18000.00);
+
+ workitem.setItemValue("invoice.correction", "false");
+ workitem.setItemValue("invoice.CorrectionInvoiceNumber", "");
+ workitem.setItemValue("partner.id", "BP-001");
+ workitem.setItemValue("partner.vat", "PL1234567890");
+ workitem.setItemValue("invoice.performancedate", new Date());
+
+ List
- *
- */
- 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