diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/TransactionCalculator.java b/library/src/main/java/org/mustangproject/ZUGFeRD/TransactionCalculator.java index 4f95773c..fc15b318 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/TransactionCalculator.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/TransactionCalculator.java @@ -4,9 +4,12 @@ import static java.math.BigDecimal.ZERO; import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -248,6 +251,104 @@ public class TransactionCalculator implements IAbsoluteValueProvider { return hm; } + protected List getVATAmountList() + { + final List vatAmounts = new ArrayList<>(); + final String vatDueDateTypeCode = this.trans.getVATDueDateTypeCode(); + for (final IZUGFeRDExportableItem currentItem : this.trans.getZFItems()) + { + BigDecimal percent = null; + if (currentItem.getProduct() != null) + { + percent = currentItem.getProduct().getVATPercent(); + } + if (percent != null) + { + final LineCalculator lc = new LineCalculator(currentItem); + final VATAmount itemVATAmount = new VATAmount(lc.getItemTotalNetAmount(), lc.getItemTotalVATAmount(), + currentItem.getProduct().getTaxCategoryCode(), vatDueDateTypeCode, percent); + final String reasonText = currentItem.getProduct().getTaxExemptionReason(); + if (reasonText != null) + { + itemVATAmount.setVatExemptionReasonText(reasonText); + } + final Optional currentVatAmount = this.getCurrentVatAmount(vatAmounts, currentItem.getProduct().getTaxCategoryCode(), percent); + if (currentVatAmount.isEmpty()) + { + vatAmounts.add(itemVATAmount); + } + else + { + this.mergeAdding(currentVatAmount.get(), itemVATAmount); + } + } + } + + final IZUGFeRDAllowanceCharge[] charges = this.trans.getZFCharges(); + if (charges != null && charges.length > 0) + { + for (final IZUGFeRDAllowanceCharge currentCharge : charges) + { + final BigDecimal taxPercent = currentCharge.getTaxPercent(); + if (taxPercent != null) + { + final String vatCategoryCode = currentCharge.getCategoryCode() != null ? currentCharge.getCategoryCode() : "S"; + final Optional currentChargeVatAmount = this.getCurrentVatAmount(vatAmounts, vatCategoryCode, taxPercent); + final BigDecimal chargeBasis = currentCharge.getTotalAmount(this); + final VATAmount chargeVatAmount = new VATAmount(chargeBasis, chargeBasis.multiply(taxPercent.divide(new BigDecimal(100))), vatCategoryCode, + vatDueDateTypeCode, taxPercent); + if (currentChargeVatAmount.isEmpty()) + { + vatAmounts.add(chargeVatAmount); + } + else + { + this.mergeAdding(currentChargeVatAmount.get(), chargeVatAmount); + } + } + } + } + final IZUGFeRDAllowanceCharge[] allowances = this.trans.getZFAllowances(); + if (allowances != null && allowances.length > 0) + { + for (final IZUGFeRDAllowanceCharge currentAllowance : allowances) + { + final BigDecimal taxPercent = currentAllowance.getTaxPercent(); + if (taxPercent != null) + { + final String vatCategoryCode = currentAllowance.getCategoryCode() != null ? currentAllowance.getCategoryCode() : "S"; + final Optional currentAllowanceVatAmount = this.getCurrentVatAmount(vatAmounts, vatCategoryCode, taxPercent); + final BigDecimal allowanceNegativeBasis = currentAllowance.getTotalAmount(this).multiply(BigDecimal.valueOf(-1)); + final VATAmount allowanceVATAmount = new VATAmount(allowanceNegativeBasis, + allowanceNegativeBasis.multiply(taxPercent.divide(new BigDecimal(100))), + currentAllowance.getCategoryCode() != null ? currentAllowance.getCategoryCode() : "S", + vatDueDateTypeCode, taxPercent); + if (currentAllowanceVatAmount.isEmpty()) + { + vatAmounts.add(allowanceVATAmount); + } + else + { + this.mergeAdding(currentAllowanceVatAmount.get(), allowanceVATAmount); + } + } + } + } + return vatAmounts; + } + + public void mergeAdding(VATAmount vatAmount, VATAmount toAdd) + { + vatAmount.setBasis(vatAmount.getBasis().add(toAdd.getBasis())); + vatAmount.setCalculated(vatAmount.getCalculated().add(toAdd.getCalculated())); + if (toAdd.getVatExemptionReasonText() != null && !toAdd.getVatExemptionReasonText().isBlank()) + { + Optional.ofNullable(vatAmount.getVatExemptionReasonText()).filter(reasonText -> !reasonText.equals(toAdd.getVatExemptionReasonText())).ifPresentOrElse( + text -> vatAmount.setVatExemptionReasonText(String.join(", ", text, toAdd.getVatExemptionReasonText())), + () -> vatAmount.setVatExemptionReasonText(toAdd.getVatExemptionReasonText())); + } + } + @Override public BigDecimal getValue() { return getTotal(); @@ -260,6 +361,15 @@ public class TransactionCalculator implements IAbsoluteValueProvider { public BigDecimal getAllowanceTotal() { return getAllowancesForPercent(null).setScale(2, RoundingMode.HALF_UP); } + + private Optional getCurrentVatAmount(List vatAmounts, String vatCategoryCode, BigDecimal percentage) + { + return vatAmounts.stream() + .filter(va -> Objects.equals(vatCategoryCode, va.getCategoryCode()) + && Optional.ofNullable(percentage).map(p -> va.getApplicablePercent() == null && p == null || p.compareTo(va.getApplicablePercent()) == 0) + .orElse(true)) + .findFirst(); + } public BigDecimal getDuePayable() { BigDecimal res = getGrandTotal().subtract(getTotalPrepaid()); diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java b/library/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java index 855e7427..813db1a2 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java @@ -57,6 +57,16 @@ public class VATAmount { this.categoryCode = categoryCode; this.dueDateTypeCode = dueDateTypeCode; } + + public VATAmount(BigDecimal basis, BigDecimal calculated, String categoryCode, String dueDateTypeCode, BigDecimal applicablePercent) + { + super(); + this.basis = basis; + this.calculated = calculated; + this.categoryCode = categoryCode; + this.dueDateTypeCode = dueDateTypeCode; + this.applicablePercent = applicablePercent; + } public BigDecimal getApplicablePercent() { return applicablePercent; diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index 8fe07564..8c7c4041 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -704,9 +704,9 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { hasDueDate = false; } - final Map VATPercentAmountMap = calc.getVATPercentAmountMap(); - for (final BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { - final VATAmount amount = VATPercentAmountMap.get(currentTaxPercent); + final List vatAmounts = calc.getVATAmountList(); + for (final VATAmount amount : vatAmounts) + { if (amount != null) { final String amountCategoryCode = amount.getCategoryCode(); final String amountDueDateTypeCode = amount.getDueDateTypeCode(); @@ -727,7 +727,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { + "" + amountCategoryCode + "" + (amountDueDateTypeCode != null ? "" + amountDueDateTypeCode + "" : "") + "" - + vatFormat(currentTaxPercent) + ""; + + vatFormat(amount.getApplicablePercent()) + ""; } } } @@ -765,19 +765,23 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { xml += "" + ""; } - } else { - for (final BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { - if (calc.getChargesForPercent(currentTaxPercent).compareTo(BigDecimal.ZERO) != 0) { + } + else + { + for (final VATAmount amount : vatAmounts) + { + if (calc.getChargesForPercent(amount.getApplicablePercent()).compareTo(BigDecimal.ZERO) != 0) + { xml += "" + "" + "true" + "" + - "" + currencyFormat(calc.getChargesForPercent(currentTaxPercent)) + "" + - "" + XMLTools.encodeXML(calc.getChargeReasonForPercent(currentTaxPercent)) + "" + + "" + currencyFormat(calc.getChargesForPercent(amount.getApplicablePercent())) + "" + + "" + XMLTools.encodeXML(calc.getChargeReasonForPercent(amount.getApplicablePercent())) + "" + "" + "VAT" + - "" + VATPercentAmountMap.get(currentTaxPercent).getCategoryCode() + "" + - "" + vatFormat(currentTaxPercent) + "" + + "" + amount.getCategoryCode() + "" + + "" + vatFormat(amount.getApplicablePercent()) + "" + "" + ""; } @@ -808,19 +812,23 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { xml += "" + ""; } - } else { - for (final BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { - if (calc.getAllowancesForPercent(currentTaxPercent).compareTo(BigDecimal.ZERO) != 0) { + } + else + { + for (final VATAmount amount : vatAmounts) + { + if (calc.getAllowancesForPercent(amount.getApplicablePercent()).compareTo(BigDecimal.ZERO) != 0) + { xml += "" + "" + "false" + "" + - "" + currencyFormat(calc.getAllowancesForPercent(currentTaxPercent)) + "" + - "" + XMLTools.encodeXML(calc.getAllowanceReasonForPercent(currentTaxPercent)) + "" + + "" + currencyFormat(calc.getAllowancesForPercent(amount.getApplicablePercent())) + "" + + "" + XMLTools.encodeXML(calc.getAllowanceReasonForPercent(amount.getApplicablePercent())) + "" + "" + "VAT" + - "" + VATPercentAmountMap.get(currentTaxPercent).getCategoryCode() + "" + - "" + vatFormat(currentTaxPercent) + "" + + "" + amount.getCategoryCode() + "" + + "" + vatFormat(amount.getApplicablePercent()) + "" + "" + ""; } diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java index 6d0a2664..df97bd6a 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java @@ -23,12 +23,21 @@ package org.mustangproject.ZUGFeRD; import junit.framework.TestCase; import org.mustangproject.*; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; +import org.w3c.dom.Document; +import org.w3c.dom.Node; import org.junit.FixMethodOrder; +import org.junit.jupiter.api.Assertions; import org.junit.runners.MethodSorters; - - +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; import java.io.FileWriter; import java.io.IOException; import java.math.BigDecimal; @@ -150,6 +159,77 @@ public class XRTest extends TestCase { } + public void testIssue830ApplicableHeaderTradeSettlementTax() throws XPathExpressionException, SAXException, IOException, ParserConfigurationException + { + final Invoice i = new Invoice().setDueDate(new Date()).setIssueDate(new Date()).setDeliveryDate(new Date()) + .setSender(new TradeParty("Test", "teststr", "55232", "teststadt", "DE").setEmail("sender@example.com").addTaxID("DE4711").addVATID("DE0815") + .setContact(new Contact("Hans Test", "+49123456789", "test@example.org")) + .addBankDetails(new BankDetails("DE12500105170648489890", "COBADEFXXX").setAccountName("kontoInhaber"))) + .setRecipient(new TradeParty("Franz Müller", "teststr.12", "55232", "Entenhausen", "DE").setEmail("recipient@sample.org")) + .setReferenceNumber("991-01484-64") + .setNumber("123") + .addItem( + new Item(new Product("Testprodukt1", "", "C62", BigDecimal.ZERO).setTaxCategoryCode("E").setTaxExemptionReason("Product is exempt"), BigDecimal.TEN, + BigDecimal.ONE)) + .addItem(new Item(new Product("Testprodukt2", "", "C62", BigDecimal.ZERO).setTaxCategoryCode("AE").setTaxExemptionReason("Reversecharge process"), + BigDecimal.ONE, BigDecimal.ONE)) + .addItem(new Item(new Product("Testprodukt3", "", "C62", BigDecimal.valueOf(19)).setTaxCategoryCode("S"), BigDecimal.valueOf(9), BigDecimal.ONE) + .addCharge(new Charge(BigDecimal.ONE).setReasonCode("64").setTaxPercent(BigDecimal.valueOf(19)))) + .addItem(new Item( + new Product("Testprodukt4", "", "C62", BigDecimal.ZERO).setTaxCategoryCode("AE").setTaxExemptionReason("Reversecharge process"), + BigDecimal.TEN, BigDecimal.ONE) + .addAllowance(new Allowance().setReasonCode("64").setTotalAmount(BigDecimal.valueOf(4)).setTaxPercent(BigDecimal.ZERO))) + .setPayee( + new TradeParty().setName("VR Factoring GmbH").setID("DE813838785").setLegalOrganisation(new LegalOrganisation("391200LDDFJDMIPPMZ54", "0199"))) + .addAllowance(new Allowance().setReasonCode("64").setTotalAmount(BigDecimal.valueOf(5)).setTaxPercent(BigDecimal.valueOf(19))); + + final ZUGFeRD2PullProvider zf2p = new ZUGFeRD2PullProvider(); + + zf2p.setProfile(Profiles.getByName("XRechnung")); + zf2p.generateXML(i); + final String xmlGen = new String(zf2p.getXML()); + System.out.println(xmlGen); + final Document doc = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(new ByteArrayInputStream(zf2p.getXML())); + + final XPath xpath = XPathFactory.newInstance().newXPath(); + final NodeList tradeTaxes = (NodeList) xpath + .compile("//*[local-name()='ApplicableHeaderTradeSettlement']/*[local-name()='ApplicableTradeTax']") + .evaluate(doc, XPathConstants.NODESET); + + Assertions.assertEquals(3, tradeTaxes.getLength()); + final Node taxNode0 = tradeTaxes.item(0); + final String categoryCode0 = xpath + .compile("*[local-name()='CategoryCode']/text()") + .evaluate(taxNode0); + Assertions.assertEquals("E", categoryCode0); + final String basisAmount0 = xpath + .compile("*[local-name()='BasisAmount']/text()") + .evaluate(taxNode0); + Assertions.assertTrue(BigDecimal.TEN.compareTo(new BigDecimal(basisAmount0)) == 0); + + final Node taxNode1 = tradeTaxes.item(1); + final String categoryCode1 = xpath + .compile("*[local-name()='CategoryCode']/text()") + .evaluate(taxNode1); + Assertions.assertEquals("AE", categoryCode1); + final String basisAmount1 = xpath + .compile("*[local-name()='BasisAmount']/text()") + .evaluate(taxNode1); + Assertions.assertTrue(BigDecimal.valueOf(7).compareTo(new BigDecimal(basisAmount1)) == 0); + + final Node taxNode2 = tradeTaxes.item(2); + final String categoryCode2 = xpath + .compile("*[local-name()='CategoryCode']/text()") + .evaluate(taxNode2); + Assertions.assertEquals("S", categoryCode2); + final String basisAmount2 = xpath + .compile("*[local-name()='BasisAmount']/text()") + .evaluate(taxNode2); + Assertions.assertTrue(BigDecimal.valueOf(5).compareTo(new BigDecimal(basisAmount2)) == 0); + } + public void testXRExportWithoutStreet() { // the writing part @@ -205,7 +285,7 @@ public class XRTest extends TestCase { String theXML = new String(zf2p.getXML(), StandardCharsets.UTF_8); assertThat(theXML).valueByXPath("count(//*[local-name()='ExemptionReason'])") .asInt() - .isEqualTo(1); + .isEqualTo(2); }