From b74241f1caa5eb55a729639a002d73d46d913c41 Mon Sep 17 00:00:00 2001 From: ean Date: Thu, 21 Nov 2024 11:00:46 +0100 Subject: [PATCH 1/3] Charges on item level Adds support for allowances and charges on item level Fixes #561 Previously charges/allowances on item level where handled same as header level --- .../main/java/org/mustangproject/Item.java | 15 ++ .../ZUGFeRD/ZUGFeRDInvoiceImporter.java | 180 ++++++++++-------- .../ZUGFeRD/ZF2ZInvoiceImporterTest.java | 9 + .../factur-x-with-position-allowance.xml | 177 +++++++++++++++++ 4 files changed, 303 insertions(+), 78 deletions(-) create mode 100644 library/src/test/resources/factur-x-with-position-allowance.xml diff --git a/library/src/main/java/org/mustangproject/Item.java b/library/src/main/java/org/mustangproject/Item.java index d726ed08e..6165022b8 100644 --- a/library/src/main/java/org/mustangproject/Item.java +++ b/library/src/main/java/org/mustangproject/Item.java @@ -36,6 +36,7 @@ public class Item implements IZUGFeRDExportableItem { protected ArrayList referencedDocuments = null; protected ArrayList additionalReference = null; protected ArrayList Allowances = new ArrayList<>(); + protected ArrayList totalAllowances = new ArrayList<>(); protected ArrayList Charges = new ArrayList<>(); /*** @@ -235,6 +236,15 @@ public IZUGFeRDAllowanceCharge[] getItemAllowances() { return Allowances.toArray(new IZUGFeRDAllowanceCharge[0]); } + @Override + public IZUGFeRDAllowanceCharge[] getItemTotalAllowances() { + if (totalAllowances.isEmpty()) { + return null; + } else { + return totalAllowances.toArray(new IZUGFeRDAllowanceCharge[0]); + } + } + @Override public IZUGFeRDAllowanceCharge[] getItemCharges() { if (Charges.isEmpty()) { @@ -280,6 +290,11 @@ public Item addAllowance(IZUGFeRDAllowanceCharge izac) { return this; } + public Item addTotalAllowance(IZUGFeRDAllowanceCharge izac) { + totalAllowances.add(izac); + return this; + } + /*** * adds item level freetext fields (includednote) * @param text UTF8 plain text diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java index 46594a26e..0654b5835 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java @@ -646,89 +646,47 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx Item it = new Item(currentItemNode.getChildNodes(), recalcPrice); zpp.addItem(it); - } - - // now handling base64 encoded attachments AttachmentBinaryObject=CII, EmbeddedDocumentBinaryObject=UBL - xpr = xpath.compile("//*[local-name()=\"AttachmentBinaryObject\"]|//*[local-name()=\"EmbeddedDocumentBinaryObject\"]"); - NodeList attachmentNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); - for (int i = 0; i < attachmentNodes.getLength(); i++) { - FileAttachment fa = new FileAttachment(attachmentNodes.item(i).getAttributes().getNamedItem("filename").getNodeValue(), attachmentNodes.item(i).getAttributes().getNamedItem("mimeCode").getNodeValue(), "Data", Base64.getDecoder().decode(XMLTools.trimOrNull(attachmentNodes.item(i)))); - fileAttachments.add(fa); - // filename = "Aufmass.png" mimeCode = "image/png" - //EmbeddedDocumentBinaryObject cbc:EmbeddedDocumentBinaryObject mimeCode="image/png" filename="Aufmass.png" - } - - // item level charges+allowances are not yet handled but a lower item price will - // be read, - // so the invoice remains arithmetically correct - // -> parse document level charges+allowances - xpr = xpath.compile("//*[local-name()=\"SpecifiedTradeAllowanceCharge\"]"); - NodeList chargeNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); - for (int i = 0; i < chargeNodes.getLength(); i++) { - NodeList chargeNodeChilds = chargeNodes.item(i).getChildNodes(); - boolean isCharge = true; - String chargeAmount = null; - String reason = null; - String reasonCode = null; - String taxPercent = null; - for (int chargeChildIndex = 0; chargeChildIndex < chargeNodeChilds.getLength(); chargeChildIndex++) { - String chargeChildName = chargeNodeChilds.item(chargeChildIndex).getLocalName(); - if (chargeChildName != null) { - - if (chargeChildName.equals("ChargeIndicator")) { - NodeList indicatorChilds = chargeNodeChilds.item(chargeChildIndex).getChildNodes(); - for (int indicatorChildIndex = 0; indicatorChildIndex < indicatorChilds.getLength(); indicatorChildIndex++) { - if ((indicatorChilds.item(indicatorChildIndex).getLocalName() != null) - && (indicatorChilds.item(indicatorChildIndex).getLocalName().equals("Indicator"))) { - isCharge = XMLTools.trimOrNull(indicatorChilds.item(indicatorChildIndex)).equalsIgnoreCase("true"); - } - } - } else if (chargeChildName.equals("ActualAmount")) { - chargeAmount = XMLTools.trimOrNull(chargeNodeChilds.item(chargeChildIndex)); - } else if (chargeChildName.equals("Reason")) { - reason = XMLTools.trimOrNull(chargeNodeChilds.item(chargeChildIndex)); - } else if (chargeChildName.equals("ReasonCode")) { - reasonCode = XMLTools.trimOrNull(chargeNodeChilds.item(chargeChildIndex)); - } else if (chargeChildName.equals("CategoryTradeTax")) { - NodeList taxChilds = chargeNodeChilds.item(chargeChildIndex).getChildNodes(); - for (int taxChildIndex = 0; taxChildIndex < taxChilds.getLength(); taxChildIndex++) { - String taxItemName = taxChilds.item(taxChildIndex).getLocalName(); - if ((taxItemName != null) && (taxItemName.equals("RateApplicablePercent") || taxItemName.equals("ApplicablePercent"))) { - taxPercent = XMLTools.trimOrNull(taxChilds.item(taxChildIndex)); - } - } - } + xpr = xpath.compile(".//*[local-name()=\"SpecifiedTradeAllowanceCharge\"]"); + NodeList chargeNodes = (NodeList) xpr.evaluate(currentItemNode, XPathConstants.NODESET); + for (int k = 0; k < chargeNodes.getLength(); k++) { + NodeList chargeNodeChilds = chargeNodes.item(k).getChildNodes(); + Charge c = parseCharge(chargeNodeChilds, false); + if (c instanceof Allowance) { + it.addTotalAllowance(c); + } else { + it.addCharge(c); } } + } + } - if (isCharge) { - Charge c = new Charge(new BigDecimal(chargeAmount)); - if (reason != null) { - c.setReason(reason); - } - if (reasonCode != null) { - c.setReasonCode(reasonCode); - } - if (taxPercent != null) { - c.setTaxPercent(new BigDecimal(taxPercent)); - } - zpp.addCharge(c); - } else { - Allowance a = new Allowance(new BigDecimal(chargeAmount)); - if (reason != null) { - a.setReason(reason); - } - if (reasonCode != null) { - a.setReasonCode(reasonCode); - } - if (taxPercent != null) { - a.setTaxPercent(new BigDecimal(taxPercent)); - } - zpp.addAllowance(a); - } - + // now handling base64 encoded attachments AttachmentBinaryObject=CII, EmbeddedDocumentBinaryObject=UBL + xpr = xpath.compile("//*[local-name()=\"AttachmentBinaryObject\"]|//*[local-name()=\"EmbeddedDocumentBinaryObject\"]"); + NodeList attachmentNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); + for (int i = 0; i < attachmentNodes.getLength(); i++) { + FileAttachment fa = new FileAttachment(attachmentNodes.item(i).getAttributes().getNamedItem("filename").getNodeValue(), attachmentNodes.item(i).getAttributes().getNamedItem("mimeCode").getNodeValue(), "Data", Base64.getDecoder().decode(XMLTools.trimOrNull(attachmentNodes.item(i)))); + fileAttachments.add(fa); + // filename = "Aufmass.png" mimeCode = "image/png" + //EmbeddedDocumentBinaryObject cbc:EmbeddedDocumentBinaryObject mimeCode="image/png" filename="Aufmass.png" + } + + // item level charges+allowances are not yet handled but a lower item price will + // be read, + // so the invoice remains arithmetically correct + // -> parse document level charges+allowances + xpr = xpath.compile("//*[local-name()=\"ApplicableHeaderTradeSettlement\"]//*[local-name()=\"SpecifiedTradeAllowanceCharge\"]"); + NodeList chargeNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); + for (int i = 0; i < chargeNodes.getLength(); i++) { + NodeList chargeNodeChilds = chargeNodes.item(i).getChildNodes(); + Charge c = parseCharge(chargeNodeChilds, true); + if (c instanceof Allowance) { + zpp.addAllowance(c); + } else { + zpp.addCharge(c); } + } + if (zpp.getZFItems() != null && zpp.getZFItems().length > 0) { TransactionCalculator tc = new TransactionCalculator(zpp); String expectedStringTotalGross = tc.getGrandTotal() .subtract(Objects.requireNonNullElse(zpp.getTotalPrepaidAmount(), BigDecimal.ZERO)).toPlainString(); @@ -749,6 +707,72 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx } + /** + * @param isHeader true = header level charge, false = item level charge + */ + private Charge parseCharge(NodeList chargeNodeChilds, boolean isHeader) { + boolean isCharge = true; + String chargeAmount = null; + String reason = null; + String reasonCode = null; + String taxPercent = null; + for (int chargeChildIndex = 0; chargeChildIndex < chargeNodeChilds.getLength(); chargeChildIndex++) { + String chargeChildName = chargeNodeChilds.item(chargeChildIndex).getLocalName(); + if (chargeChildName != null) { + + if (chargeChildName.equals("ChargeIndicator")) { + NodeList indicatorChilds = chargeNodeChilds.item(chargeChildIndex).getChildNodes(); + for (int indicatorChildIndex = 0; indicatorChildIndex < indicatorChilds.getLength(); indicatorChildIndex++) { + if ((indicatorChilds.item(indicatorChildIndex).getLocalName() != null) + && (indicatorChilds.item(indicatorChildIndex).getLocalName().equals("Indicator"))) { + isCharge = XMLTools.trimOrNull(indicatorChilds.item(indicatorChildIndex)).equalsIgnoreCase("true"); + } + } + } else if (chargeChildName.equals("ActualAmount")) { + chargeAmount = XMLTools.trimOrNull(chargeNodeChilds.item(chargeChildIndex)); + } else if (chargeChildName.equals("Reason")) { + reason = XMLTools.trimOrNull(chargeNodeChilds.item(chargeChildIndex)); + } else if (chargeChildName.equals("ReasonCode")) { + reasonCode = XMLTools.trimOrNull(chargeNodeChilds.item(chargeChildIndex)); + } else if (isHeader && chargeChildName.equals("CategoryTradeTax")) { + NodeList taxChilds = chargeNodeChilds.item(chargeChildIndex).getChildNodes(); + for (int taxChildIndex = 0; taxChildIndex < taxChilds.getLength(); taxChildIndex++) { + String taxItemName = taxChilds.item(taxChildIndex).getLocalName(); + if ((taxItemName != null) && (taxItemName.equals("RateApplicablePercent") || taxItemName.equals("ApplicablePercent"))) { + taxPercent = XMLTools.trimOrNull(taxChilds.item(taxChildIndex)); + } + } + } + } + } + + if (isCharge) { + Charge c = new Charge(new BigDecimal(chargeAmount)); + if (reason != null) { + c.setReason(reason); + } + if (reasonCode != null) { + c.setReasonCode(reasonCode); + } + if (taxPercent != null) { + c.setTaxPercent(new BigDecimal(taxPercent)); + } + return c; + } else { + Allowance a = new Allowance(new BigDecimal(chargeAmount)); + if (reason != null) { + a.setReason(reason); + } + if (reasonCode != null) { + a.setReasonCode(reasonCode); + } + if (taxPercent != null) { + a.setTaxPercent(new BigDecimal(taxPercent)); + } + return a; + } + } + protected Document getDocument() { return document; } diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java index b9342e1ed..86ea30ee9 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java @@ -28,6 +28,7 @@ import javax.xml.xpath.XPathExpressionException; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; @@ -397,4 +398,12 @@ public void testImportIncludedNotes() throws XPathExpressionException, ParseExce } + public void testPositionAllowance() throws FileNotFoundException, XPathExpressionException, ParseException { + File inputFile = getResourceAsFile("factur-x-with-position-allowance.xml"); + ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter(new FileInputStream(inputFile)); + + CalculatedInvoice invoice = new CalculatedInvoice(); + zii.extractInto(invoice); + assertThat(invoice.getGrandTotal()).isEqualByComparingTo(BigDecimal.valueOf(82.63)); + } } diff --git a/library/src/test/resources/factur-x-with-position-allowance.xml b/library/src/test/resources/factur-x-with-position-allowance.xml new file mode 100644 index 000000000..6229e6e60 --- /dev/null +++ b/library/src/test/resources/factur-x-with-position-allowance.xml @@ -0,0 +1,177 @@ + + + + + urn:cen.eu:en16931:2017 + + + + 24/639340666/001 + 380 + + 20241015 + + + SEPA-Lastschrift + AAB + + + PAYMENTAMOUNT:82.63 + PMD + + + PAYMENTCURRENCY:EUR + PMD + + + + + + 1 + + + 0009 + DIESEL + + LeoID + 123456.0123456789 + + + Uhrzeit + 19:47 + + + Ort + DÜSSELDORF + + + Marke + TOTALENERGIES + + + Servicestelle + SS0021989 + + + Lieferscheinnummer + 38163001 + + + Kilometerstand + 0130000 + + + DE + + + + + 128.49 + 100. + + + + 56.69 + + + + VAT + S + 19.00 + + + + 20241005 + + + 20241005 + + + + + false + + 3.40 + Rebate + + + 69.44 + + + AB CD 1234 + 130 + ABZ + + + + + + DE119375450 + DKV Euro Service GmbH + Co. KG + + Employee card service + + 0049 (2102) 5517-472 + + + myemployeecard@dkv-mobility.com + + + + 40882 + Balcke-Dürr-Allee 3 + Ratingen + DE + + + DE119375450 + + + + 199295 + Max Mustermann + + 12345 + Musterstr. 1 + Musterstadt + DE + + + + 24/639340666/001 + 130 + + + + + EUR + + 70 + + DE26330403100827038100 + + + + 13.19 + VAT + 69.44 + S + 19.00 + + + 14.11.2024 00:00:00 + + 20241114 + + + + 69.44 + 69.44 + 13.19 + 82.63 + 0.00 + 82.63 + + + + From 2a8d44896d25cf0e46032c3ee7d58ead6c9fcfff Mon Sep 17 00:00:00 2001 From: ean Date: Wed, 27 Nov 2024 09:46:35 +0100 Subject: [PATCH 2/3] Charges on item level Add missing @Test Annotation --- .../java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java index 86ea30ee9..b56bc3b45 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java @@ -398,6 +398,7 @@ public void testImportIncludedNotes() throws XPathExpressionException, ParseExce } + @Test public void testPositionAllowance() throws FileNotFoundException, XPathExpressionException, ParseException { File inputFile = getResourceAsFile("factur-x-with-position-allowance.xml"); ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter(new FileInputStream(inputFile)); From dc4c06dfbc67f1426aa761e9ec6175279b7a0151 Mon Sep 17 00:00:00 2001 From: ean Date: Mon, 3 Feb 2025 11:50:12 +0100 Subject: [PATCH 3/3] Handle position total charges --- .../src/main/java/org/mustangproject/Item.java | 15 +++++++++++++++ .../ZUGFeRD/IZUGFeRDExportableItem.java | 9 +++++++++ .../mustangproject/ZUGFeRD/LineCalculator.java | 11 ++++++++++- .../ZUGFeRD/ZUGFeRDInvoiceImporter.java | 2 +- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/org/mustangproject/Item.java b/library/src/main/java/org/mustangproject/Item.java index 4084e4c0a..32fd8e414 100644 --- a/library/src/main/java/org/mustangproject/Item.java +++ b/library/src/main/java/org/mustangproject/Item.java @@ -42,6 +42,7 @@ public class Item implements IZUGFeRDExportableItem { protected ArrayList Allowances = new ArrayList<>(); protected ArrayList totalAllowances = new ArrayList<>(); protected ArrayList Charges = new ArrayList<>(); + protected ArrayList totalCharges = new ArrayList<>(); protected List includedNotes = null; //protected HashMap attributes = new HashMap<>(); @@ -332,6 +333,15 @@ public IZUGFeRDAllowanceCharge[] getItemAllowances() { return Allowances.toArray(new IZUGFeRDAllowanceCharge[0]); } + @Override + public IZUGFeRDAllowanceCharge[] getItemTotalCharges() { + if (totalCharges.isEmpty()) { + return null; + } else { + return totalCharges.toArray(new IZUGFeRDAllowanceCharge[0]); + } + } + @Override public IZUGFeRDAllowanceCharge[] getItemTotalAllowances() { if (totalAllowances.isEmpty()) { @@ -391,6 +401,11 @@ public Item addTotalAllowance(IZUGFeRDAllowanceCharge izac) { return this; } + public Item addTotalCharge(IZUGFeRDAllowanceCharge izac) { + totalCharges.add(izac); + return this; + } + /*** * adds item level freetext fields (includednote) * @param text UTF8 plain text diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java index 45c210eac..0a58a9c12 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java @@ -146,6 +146,15 @@ default Date getDetailedDeliveryPeriodTo() { return null; } + /*** + * specify charges amount for the line item total + * + * @return the sum of allowances for this item + */ + default IZUGFeRDAllowanceCharge[] getItemTotalCharges() { + return null; + } + /*** * specify allowances amount for the line item total * diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/LineCalculator.java b/library/src/main/java/org/mustangproject/ZUGFeRD/LineCalculator.java index d17ccfec6..72ec7ec31 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/LineCalculator.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/LineCalculator.java @@ -15,6 +15,7 @@ public class LineCalculator { private BigDecimal allowance = BigDecimal.ZERO; private BigDecimal charge = BigDecimal.ZERO; private BigDecimal allowanceItemTotal = BigDecimal.ZERO; + private BigDecimal chargeItemTotal = BigDecimal.ZERO; public LineCalculator(IZUGFeRDExportableItem currentItem) { @@ -28,6 +29,11 @@ public LineCalculator(IZUGFeRDExportableItem currentItem) { addCharge(charge.getTotalAmount(currentItem)); } } + if (currentItem.getItemTotalCharges() != null && currentItem.getItemTotalCharges().length > 0) { + for (final IZUGFeRDAllowanceCharge itemTotalCharge : currentItem.getItemTotalCharges()) { + addChargeItemTotal(itemTotalCharge.getTotalAmount(currentItem)); + } + } if (currentItem.getItemTotalAllowances() != null && currentItem.getItemTotalAllowances().length > 0) { for (final IZUGFeRDAllowanceCharge itemTotalAllowance : currentItem.getItemTotalAllowances()) { addAllowanceItemTotal(itemTotalAllowance.getTotalAmount(currentItem)); @@ -56,7 +62,7 @@ public LineCalculator(IZUGFeRDExportableItem currentItem) { ? BigDecimal.ONE.setScale(4) : currentItem.getBasisQuantity(); itemTotalNetAmount = quantity.multiply(price).divide(basisQuantity, 18, RoundingMode.HALF_UP) - .subtract(allowanceItemTotal).setScale(2, RoundingMode.HALF_UP); + .add(chargeItemTotal).subtract(allowanceItemTotal).setScale(2, RoundingMode.HALF_UP); itemTotalVATAmount = itemTotalNetAmount.multiply(multiplicator); } @@ -92,4 +98,7 @@ public void addAllowanceItemTotal(BigDecimal b) { allowanceItemTotal = allowanceItemTotal.add(b); } + public void addChargeItemTotal(BigDecimal b) { + chargeItemTotal = chargeItemTotal.add(b); + } } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java index 323115375..2a9d6b9d2 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java @@ -901,7 +901,7 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx if (c instanceof Allowance) { it.addTotalAllowance(c); } else { - it.addCharge(c); + it.addTotalCharge(c); } } }