diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java b/library/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java index 855e7427..d12727ad 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/VATAmount.java @@ -135,7 +135,7 @@ public class VATAmount { } public VATAmount add(VATAmount v) { - return new VATAmount(basis.add(v.getBasis()), calculated.add(v.getCalculated()), this.categoryCode, this.dueDateTypeCode).setVatExemptionReasonText(v.getVatExemptionReasonText()); + return new VATAmount(basis.add(v.getBasis()), calculated.add(v.getCalculated()), this.categoryCode, this.dueDateTypeCode).setVatExemptionReasonText(v.getVatExemptionReasonText() != null ? v.getVatExemptionReasonText(): this.vatExemptionReasonText); } public VATAmount subtract(VATAmount v) { diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index 6e23e7f2..cce7012c 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -48,6 +48,7 @@ import org.mustangproject.IncludedNote; import org.mustangproject.ReferencedDocument; import org.mustangproject.XMLTools; import org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants; +import org.mustangproject.ZUGFeRD.model.TaxCategoryCodeTypeConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -150,12 +151,12 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { if (profile == Profiles.getByName("Minimum")) { xml += "" + XMLTools.encodeXML(party.getLegalOrganisation().getSchemedID().getID()) + ""; } else { - String schemeAttribute=""; - if ((party.getLegalOrganisation().getSchemedID().getScheme()!=null)&&(party.getLegalOrganisation().getSchemedID().getScheme().length()>0)) { - schemeAttribute="schemeID=\"" + XMLTools.encodeXML(party.getLegalOrganisation().getSchemedID().getScheme())+"\""; + String schemeAttribute = ""; + if ((party.getLegalOrganisation().getSchemedID().getScheme() != null) && (party.getLegalOrganisation().getSchemedID().getScheme().length() > 0)) { + schemeAttribute = "schemeID=\"" + XMLTools.encodeXML(party.getLegalOrganisation().getSchemedID().getScheme()) + "\""; } - xml += "" + XMLTools.encodeXML(party.getLegalOrganisation().getSchemedID().getID()) + ""; + xml += "" + XMLTools.encodeXML(party.getLegalOrganisation().getSchemedID().getID()) + ""; } } if (party.getLegalOrganisation().getTradingBusinessName() != null) { @@ -393,20 +394,20 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { + "" + "" + "" + XMLTools.encodeXML(trans.getNumber()) + ""; - if (profile == Profiles.getByName("Extended") && trans.getDocumentName() != null) { - xml += "" + XMLTools.encodeXML(trans.getDocumentName()) + ""; - } - xml += "" + typecode + "" - + "" + DATE.udtFormat(trans.getIssueDate()) + "" // date - + buildNotes(trans) - + "" - + ""; + if (profile == Profiles.getByName("Extended") && trans.getDocumentName() != null) { + xml += "" + XMLTools.encodeXML(trans.getDocumentName()) + ""; + } + xml += "" + typecode + "" + + "" + DATE.udtFormat(trans.getIssueDate()) + "" // date + + buildNotes(trans) + + "" + + ""; int lineID = 0; for (final IZUGFeRDExportableItem currentItem : trans.getZFItems()) { lineID++; String lineIDStr = Integer.toString(lineID); - if (currentItem.getId()!=null) { - lineIDStr=currentItem.getId(); + if (currentItem.getId() != null) { + lineIDStr = currentItem.getId(); } final LineCalculator lc = new LineCalculator(currentItem); if ((getProfile() != Profiles.getByName("Minimum")) && (getProfile() != Profiles.getByName("BasicWL"))) { @@ -529,15 +530,16 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { + "" + "" + "VAT"; - if (currentItem.getProduct().getTaxExemptionReason() != null) { xml += "" + XMLTools.encodeXML(currentItem.getProduct().getTaxExemptionReason()) + ""; } - - xml += "" + currentItem.getProduct().getTaxCategoryCode() + "" - + "" - + vatFormat(currentItem.getProduct().getVATPercent()) + "" - + ""; + xml += "" + currentItem.getProduct().getTaxCategoryCode() + ""; + if (!currentItem.getProduct().getTaxCategoryCode().equals(TaxCategoryCodeTypeConstants.UNTAXEDSERVICE)) { + xml += "" + + vatFormat(currentItem.getProduct().getVATPercent()) + ""; + } + xml += ""; + if ((currentItem.getDetailedDeliveryPeriodFrom() != null) || (currentItem.getDetailedDeliveryPeriodTo() != null)) { xml += ""; if (currentItem.getDetailedDeliveryPeriodFrom() != null) { @@ -723,9 +725,12 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { + exemptionReasonTextXML + "" + currencyFormat(amount.getBasis()) + "" // currencyID=\"EUR\" + "" + amountCategoryCode + "" - + (amountDueDateTypeCode != null ? "" + amountDueDateTypeCode + "" : "") - + "" - + vatFormat(currentTaxPercent) + ""; + + (amountDueDateTypeCode != null ? "" + amountDueDateTypeCode + "" : ""); + if (!amountCategoryCode.equals(TaxCategoryCodeTypeConstants.UNTAXEDSERVICE)) { + xml += "" + + vatFormat(currentTaxPercent) + ""; + } + xml += ""; } } } @@ -741,7 +746,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { } if ((trans.getZFCharges() != null) && (trans.getZFCharges().length > 0)) { - if ((profile == Profiles.getByName("XRechnung")) || (profile == Profiles.getByName("EN16931")) || (profile == Profiles.getByName("EXTENDED"))) { + if ((profile == Profiles.getByName("XRechnung")) || (profile == Profiles.getByName("EN16931")) || (profile == Profiles.getByName("EXTENDED"))) { for (IZUGFeRDAllowanceCharge charge : trans.getZFCharges()) { xml += "" + "" + @@ -757,7 +762,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { xml += "" + "VAT" + "" + charge.getCategoryCode() + ""; - if (charge.getTaxPercent() != null) { + if (charge.getTaxPercent() != null && !charge.getCategoryCode().equals(TaxCategoryCodeTypeConstants.UNTAXEDSERVICE)) { xml += "" + vatFormat(charge.getTaxPercent()) + ""; } xml += "" + @@ -832,7 +837,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { if (paymentTermsDescription != null) { xml += "" + paymentTermsDescription + ""; } - + if (trans.getDueDate() != null) { xml += "" // $NON-NLS-2$ + DATE.udtFormat(trans.getDueDate()) @@ -900,12 +905,12 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { if (trans.getInvoiceReferencedDocuments() != null) { for (ReferencedDocument doc : trans.getInvoiceReferencedDocuments()) { xml += "" - + "" - + XMLTools.encodeXML(doc.getIssuerAssignedID()) + ""; + + "" + + XMLTools.encodeXML(doc.getIssuerAssignedID()) + ""; if (doc.getFormattedIssueDateTime() != null) { xml += "" - + DATE.qdtFormat(doc.getFormattedIssueDateTime()) - + ""; + + DATE.qdtFormat(doc.getFormattedIssueDateTime()) + + ""; } xml += ""; } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/model/TaxCategoryCodeTypeConstants.java b/library/src/main/java/org/mustangproject/ZUGFeRD/model/TaxCategoryCodeTypeConstants.java index c9ecc2a0..afd5ee28 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/model/TaxCategoryCodeTypeConstants.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/model/TaxCategoryCodeTypeConstants.java @@ -1,21 +1,23 @@ -/** ********************************************************************** - * +/** + * ********************************************************************* + *

* Copyright 2018 Jochen Staerk - * + *

* Use is subject to license terms. - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0. - * + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * + *

* See the License for the specific language governing permissions and * limitations under the License. - * - *********************************************************************** */ + *

+ * ********************************************************************** + */ package org.mustangproject.ZUGFeRD.model; import java.util.Set; @@ -31,5 +33,5 @@ public class TaxCategoryCodeTypeConstants { public static final String INTRACOMMUNITY = "K"; public static final String FREEEXPORT = "G"; - public static Set CATEGORY_CODES_WITH_EXEMPTION_REASON = Stream.of(INTRACOMMUNITY, REVERSECHARGE, TAXEXEMPT, FREEEXPORT).collect(Collectors.toSet()); + public static Set CATEGORY_CODES_WITH_EXEMPTION_REASON = Stream.of(INTRACOMMUNITY, REVERSECHARGE, TAXEXEMPT, FREEEXPORT, UNTAXEDSERVICE).collect(Collectors.toSet()); } diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java index 6d0a2664..9902a05c 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java @@ -25,6 +25,7 @@ import junit.framework.TestCase; import org.mustangproject.*; import org.junit.FixMethodOrder; import org.junit.runners.MethodSorters; +import org.mustangproject.ZUGFeRD.model.TaxCategoryCodeTypeConstants; import javax.xml.xpath.XPathExpressionException; @@ -52,8 +53,8 @@ public class XRTest extends TestCase { TradeParty recipient = new TradeParty("Franz Müller", "teststr.12", "55232", "Entenhausen", "DE"); recipient.setEmail("quack@ducktown.org"); Invoice i = createInvoice(recipient); - String legalOrgID="aCustomSellerLegalOrgId"; - String sellerID="aSellerTradePartyID"; + String legalOrgID = "aCustomSellerLegalOrgId"; + String sellerID = "aSellerTradePartyID"; i.getSender().setLegalOrganisation(new LegalOrganisation(legalOrgID)); i.getSender().setID(sellerID); ZUGFeRD2PullProvider zf2p = new ZUGFeRD2PullProvider(); @@ -61,8 +62,8 @@ public class XRTest extends TestCase { zf2p.generateXML(i); String theXML = new String(zf2p.getXML(), StandardCharsets.UTF_8); assertTrue(theXML.contains(""+sellerID+""));// must be possible without scheme # - assertTrue(theXML.contains(""+legalOrgID+""));// must be possible without scheme # + assertTrue(theXML.contains("" + sellerID + ""));// must be possible without scheme # + assertTrue(theXML.contains("" + legalOrgID + ""));// must be possible without scheme # assertThat(theXML).valueByXPath("count(//*[local-name()='IncludedSupplyChainTradeLineItem'])") .asInt() .isEqualTo(1); //2 errors are OK because there is a known bug @@ -81,6 +82,7 @@ public class XRTest extends TestCase { } + public void testXREdgeExport() { // the writing part @@ -100,7 +102,7 @@ public class XRTest extends TestCase { .setReferenceNumber("991-01484-64")//leitweg-id // not using any VAT, this is also a test of zero-rated goods: .setNumber(number).addItem(new Item(new Product("Testprodukt", "", "C62", BigDecimal.ZERO).setTaxExemptionReason("Kleinunternehmer"), amount, new BigDecimal(1.0))) - .setPayee( new TradeParty().setName("VR Factoring GmbH").setID("DE813838785").setLegalOrganisation(new LegalOrganisation("391200LDDFJDMIPPMZ54", "0199"))) + .setPayee(new TradeParty().setName("VR Factoring GmbH").setID("DE813838785").setLegalOrganisation(new LegalOrganisation("391200LDDFJDMIPPMZ54", "0199"))) .embedFileInXML(fe1); @@ -140,9 +142,9 @@ public class XRTest extends TestCase { fail("ParseException not expected"); } catch (IOException e) { fail("IOException not expected"); - } - FileAttachment[] attachedFiles=readInvoice.getAdditionalReferencedDocuments(); - assertNotNull(attachedFiles); + } + FileAttachment[] attachedFiles = readInvoice.getAdditionalReferencedDocuments(); + assertNotNull(attachedFiles); assertEquals(attachedFiles.length, 1); assertTrue(Arrays.equals(attachedFiles[0].getData(), b)); @@ -205,10 +207,40 @@ 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); } + public void testApplicablePercentInUntaxedService() { + + // the writing part + TradeParty recipient = new TradeParty("Franz Müller", null, "55232", "Entenhausen", "DE"); + String orgname = "Test company"; + String number = "123"; + String amountStr = "1.00"; + BigDecimal amount = new BigDecimal(amountStr); + var i = new Invoice().setDueDate(new java.util.Date()).setIssueDate(new java.util.Date()).setDeliveryDate(new java.util.Date()) + .setSender(new TradeParty(orgname, "teststr", "55232", "teststadt", "DE").addTaxID("DE4711").addVATID("DE0815").setEmail("info@example.org").setContact(new org.mustangproject.Contact("Hans Test", "+49123456789", "test@example.org")).addBankDetails(new org.mustangproject.BankDetails("DE12500105170648489890", "COBADEFXXX"))) + .setRecipient(recipient) + .setReferenceNumber("991-01484-64")//leitweg-id + // not using any VAT, this is also a test of zero-rated goods: + .setNumber(number).addItem(new org.mustangproject.Item(new org.mustangproject.Product("Testprodukt", "", "C62", java.math.BigDecimal.ZERO).setTaxCategoryCode(TaxCategoryCodeTypeConstants.UNTAXEDSERVICE).setTaxExemptionReason("Expemtion reason"), amount, new java.math.BigDecimal(1.0))); + + ZUGFeRD2PullProvider zf2p = new ZUGFeRD2PullProvider(); + zf2p.setProfile(Profiles.getByName("XRechnung")); + zf2p.generateXML(i); + String theXML = new String(zf2p.getXML(), StandardCharsets.UTF_8); + // Untaxed services don't have the field RateApplicablePercent, since it would be always 0. An error is thrown on validation, if 0 is set. + assertThat(theXML).valueByXPath("count(//*[local-name()='RateApplicablePercent'])") + .asInt() + .isEqualTo(0); + + //Exemption reason needs to be set if TaxCategoryCode == "O", reason should be at the product and in the ApplicableTradeTax + assertThat(theXML).valueByXPath("count(//*[local-name()='ExemptionReason'])") + .asInt() + .isEqualTo(2); + } + private org.mustangproject.Invoice createInvoice(TradeParty recipient) { String orgname = "Test company"; String number = "123";