diff --git a/History.md b/History.md index d5537074..a281e817 100644 --- a/History.md +++ b/History.md @@ -1,13 +1,8 @@ -### 2.0.1 todo -- 2.1 support kleinunternehmer, reverse charge? -- dont show empty tax number field -- fail when no bankverbindung? -- fail when xr attrs missing? -- build xr skonto??? -- *validator not to XR error on ZF files (only notices) -- xmp errors may not show correctly in log -### 2.0.1 done +2.0.1 +===== +2020-11-12 + - corrected VAT calculation on prices with >2 decimals (PR#195 thanks to weclapp-dev) - have fax numbers only in appropriate profiles, i.e., extended - do not list tax numbers for shiptotradeparties @@ -16,6 +11,9 @@ - BigDecimal specific refactoring PR #192 Thanks to weclapp-dev - Preserving metadata PR #193 Thanks to mr-stephan - support zero-rated goods: confirm that VAT category code switches from S to Z on 0%VAT +- new sample invoice +- delivery period also on item level +- corrected more than 100 javadoc entries 2.0.0 ===== diff --git a/library/src/main/java/org/mustangproject/Item.java b/library/src/main/java/org/mustangproject/Item.java index 7ba5c359..a51afb97 100644 --- a/library/src/main/java/org/mustangproject/Item.java +++ b/library/src/main/java/org/mustangproject/Item.java @@ -5,12 +5,14 @@ import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Date; /*** * describes any invoice line */ public class Item implements IZUGFeRDExportableItem { protected BigDecimal price, quantity, tax, grossPrice, lineTotalAmount; + protected Date detailedDeliveryPeriodFrom=null, detailedDeliveryPeriodTo=null; protected String id; protected Product product; protected ArrayList notes = null; @@ -58,6 +60,7 @@ public class Item implements IZUGFeRDExportableItem { return this; } + public BigDecimal getTax() { return tax; } @@ -135,11 +138,23 @@ public class Item implements IZUGFeRDExportableItem { } + /*** + * Adds a item level addition to the price (will be multiplied by quantity) + * @see org.mustangproject.Charge + * @param izac a relative or absolute charge + * @return fluent setter + */ public Item addCharge(IZUGFeRDAllowanceCharge izac) { Charges.add(izac); return this; } + /*** + * Adds a item level reduction the price (will be multiplied by quantity) + * @see org.mustangproject.Allowance + * @param izac a relative or absolute allowance + * @return fluent setter + */ public Item addAllowance(IZUGFeRDAllowanceCharge izac) { Allowances.add(izac); return this; @@ -158,5 +173,37 @@ public class Item implements IZUGFeRDExportableItem { return this; } + /*** + * specify a item level delivery period + * (apart from the document level delivery period, and the document level + * delivery day, which is probably anyway required) + * + * @param from start date + * @param to end date + * @return fluent setter + */ + public Item setDetailedDeliveryPeriod(Date from, Date to) { + detailedDeliveryPeriodFrom=from; + detailedDeliveryPeriodTo=to; + return this; + } + + /*** + * specifies the item level delivery period (there is also one on document level), + * this will be included in a BillingSpecifiedPeriod element + * @return the beginning of the delivery period + */ + public Date getDetailedDeliveryPeriodFrom() { + return detailedDeliveryPeriodFrom; + } + + /*** + * specifies the item level delivery period (there is also one on document level), + * this will be included in a BillingSpecifiedPeriod element + * @return the end of the delivery period + */ + public Date getDetailedDeliveryPeriodTo() { + return detailedDeliveryPeriodTo; + } } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java index 8f542274..80ead5ed 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java @@ -27,6 +27,7 @@ package org.mustangproject.ZUGFeRD; * */ import java.math.BigDecimal; +import java.util.Date; public interface IZUGFeRDExportableItem extends IAbsoluteValueProvider{ @@ -88,4 +89,24 @@ public interface IZUGFeRDExportableItem extends IAbsoluteValueProvider{ return null; } + + + /*** + * specifies the item level delivery period (there is also one on document level), + * this will be included in a BillingSpecifiedPeriod element + * @return the beginning of the delivery period + */ + default Date getDetailedDeliveryPeriodFrom() { + return null; + } + + /*** + * specifies the item level delivery period (there is also one on document level), + * this will be included in a BillingSpecifiedPeriod element + * @return the end of the delivery period + */ + default Date getDetailedDeliveryPeriodTo() { + return null; + } + } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index f38930ae..914e23e1 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -107,6 +107,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { } // @todo check if the two boolean args can be refactored + /*** * returns the UN/CEFACT CII XML for companies(tradeparties), which is actually * the same for ZF1 (v 2013b) and ZF2 (v 2016b) @@ -127,7 +128,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { } xml += " " + XMLTools.encodeXML(party.getName()) + "\n"; //$NON-NLS-2$ - if ((party.getContact() != null)&&(isSender||profile==Profiles.getByName("Extended"))) { + if ((party.getContact() != null) && (isSender || profile == Profiles.getByName("Extended"))) { xml = xml + "\n" + " " + XMLTools.encodeXML(party.getContact().getName()) + "\n"; if (party.getContact().getPhone() != null) { @@ -137,7 +138,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { + " \n"; } - if ((party.getContact().getFax() != null)&&(profile==Profiles.getByName("Extended"))) { + if ((party.getContact().getFax() != null) && (profile == Profiles.getByName("Extended"))) { xml = xml + " \n" + " " + XMLTools.encodeXML(party.getContact().getFax()) + "\n" + " \n"; @@ -166,13 +167,13 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { + " " + XMLTools.encodeXML(party.getCountry()) + "\n" + " \n"; - if ((party.getVATID() != null)&&(!isShipToTradeParty)) { + if ((party.getVATID() != null) && (!isShipToTradeParty)) { xml += " \n" + " " + XMLTools.encodeXML(party.getVATID()) + "\n" + " \n"; } - if ((party.getTaxID() != null)&&(!isShipToTradeParty)) { + if ((party.getTaxID() != null) && (!isShipToTradeParty)) { xml += " \n" + " " + XMLTools.encodeXML(party.getTaxID()) + "\n" @@ -193,7 +194,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { protected String getAllowanceChargeStr(IZUGFeRDAllowanceCharge allowance, IAbsoluteValueProvider item) { String percentage = ""; String chargeIndicator = "false"; - if ((allowance.getPercent() != null)&&(profile==Profiles.getByName("Extended"))) { + if ((allowance.getPercent() != null) && (profile == Profiles.getByName("Extended"))) { percentage = "" + vatFormat(allowance.getPercent()) + ""; percentage += "" + item.getValue() + ""; } @@ -201,15 +202,15 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { chargeIndicator = "true"; } - String reason=""; - if ((allowance.getReason()!=null)&&(profile==Profiles.getByName("Extended"))) { + String reason = ""; + if ((allowance.getReason() != null) && (profile == Profiles.getByName("Extended"))) { // only in extended profile - reason=""+XMLTools.encodeXML(allowance.getReason())+""; + reason = "" + XMLTools.encodeXML(allowance.getReason()) + ""; } String allowanceChargeStr = "" + chargeIndicator + "" + percentage + "" + priceFormat(allowance.getTotalAmount(item)) + "" + - reason+ + reason + ""; return allowanceChargeStr; } @@ -228,7 +229,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { paymentTermsDescription = trans.getPaymentTermDescription(); } - if ((paymentTermsDescription == null)&&(trans.getDocumentCode()!= org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants.CORRECTEDINVOICE)) { + if ((paymentTermsDescription == null) && (trans.getDocumentCode() != org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants.CORRECTEDINVOICE)) { paymentTermsDescription = "Zahlbar ohne Abzug bis " + germanDateFormat.format(trans.getDueDate()); } @@ -378,8 +379,20 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { + " " + vatFormat(currentItem.getProduct().getVATPercent()) + "\n" - + " \n" - + " \n" + + " \n"; + if ((currentItem.getDetailedDeliveryPeriodFrom() != null) || (currentItem.getDetailedDeliveryPeriodTo() != null)) { + xml = xml + ""; + if (currentItem.getDetailedDeliveryPeriodFrom() != null) { + xml = xml + "" + zugferdDateFormat.format(currentItem.getDetailedDeliveryPeriodFrom()) + ""; + } + if (currentItem.getDetailedDeliveryPeriodTo() != null) { + xml = xml + "" + zugferdDateFormat.format(currentItem.getDetailedDeliveryPeriodTo()) + ""; + } + xml = xml + ""; + + } + + xml = xml + " \n" + " " + currencyFormat(lc.getItemTotalNetAmount()) + "\n" // currencyID=\"EUR\" + " \n"; @@ -481,8 +494,8 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { } } } - if (trans.getDocumentCode()== DocumentCodeTypeConstants.CORRECTEDINVOICE) { - hasDueDate=false; + if (trans.getDocumentCode() == DocumentCodeTypeConstants.CORRECTEDINVOICE) { + hasDueDate = false; } HashMap VATPercentAmountMap = calc.getVATPercentAmountMap(); @@ -517,7 +530,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { if ((trans.getZFCharges() != null) && (trans.getZFCharges().length > 0)) { for (BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { - if (calc.getChargesForPercent(currentTaxPercent).compareTo(BigDecimal.ZERO)!=0) { + if (calc.getChargesForPercent(currentTaxPercent).compareTo(BigDecimal.ZERO) != 0) { xml = xml + " \n" + @@ -525,7 +538,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { " true\n" + " \n" + " " + currencyFormat(calc.getChargesForPercent(currentTaxPercent)) + "\n" + - " "+XMLTools.encodeXML(calc.getChargeReasonForPercent(currentTaxPercent))+"\n" + + " " + XMLTools.encodeXML(calc.getChargeReasonForPercent(currentTaxPercent)) + "\n" + " \n" + " VAT\n" + " " + VATPercentAmountMap.get(currentTaxPercent).getCategoryCode() + "\n" + @@ -540,13 +553,13 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { if ((trans.getZFAllowances() != null) && (trans.getZFAllowances().length > 0)) { for (BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) { - if (calc.getAllowancesForPercent(currentTaxPercent).compareTo(BigDecimal.ZERO)!= 0) { + if (calc.getAllowancesForPercent(currentTaxPercent).compareTo(BigDecimal.ZERO) != 0) { xml = xml + " \n" + " \n" + " false\n" + " \n" + " " + currencyFormat(calc.getAllowancesForPercent(currentTaxPercent)) + "\n" + - " "+XMLTools.encodeXML(calc.getAllowanceReasonForPercent(currentTaxPercent))+"\n" + + " " + XMLTools.encodeXML(calc.getAllowanceReasonForPercent(currentTaxPercent)) + "\n" + " \n" + " VAT\n" + " " + VATPercentAmountMap.get(currentTaxPercent).getCategoryCode() + "\n" + @@ -620,7 +633,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { try { zugferdRaw = xml.getBytes("UTF-8"); - zugferdData=XMLTools.removeBOM(zugferdRaw); + zugferdData = XMLTools.removeBOM(zugferdRaw); } catch (UnsupportedEncodingException e) { Logger.getLogger(ZUGFeRD2PullProvider.class.getName()).log(Level.SEVERE, null, e); } diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java index 5dd64e24..3821e6d8 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java @@ -242,6 +242,9 @@ public class ZF2PushTest extends TestCase { } + /*** + * test the edge cases of the invoice class + */ public void testPushEdge() { String orgname = "Test company"; @@ -262,7 +265,7 @@ public class ZF2PushTest extends TestCase { .setSender(new TradeParty(orgname, "teststr", "55232", "teststadt", "DE").addTaxID(taxID).setID("0009845")) .setDeliveryAddress(new TradeParty("just the other side of the street", "teststr.12a", "55232", "Entenhausen", "DE").addVATID("DE47110")) .setRecipient(new TradeParty("Franz Müller", "teststr.12", "55232", "Entenhausen", "DE").setID("0008734").addVATID("DE4711").setContact(new Contact("Franz Müller", "01779999999", "franz@mueller.de", "teststr. 12", "55232", "Entenhausen", "DE").setFax("++49555123456"))) - .addItem(new Item(new Product("Testprodukt", "", "C62", new BigDecimal(16)), price, new BigDecimal(1.0)).addNote("item level 1/1").addAllowance(new Allowance(new BigDecimal(0.02)).setReason("item discount").setTaxPercent(new BigDecimal(16)))) + .addItem(new Item(new Product("Testprodukt", "", "C62", new BigDecimal(16)), price, new BigDecimal(1.0)).addNote("item level 1/1").addAllowance(new Allowance(new BigDecimal(0.02)).setReason("item discount").setTaxPercent(new BigDecimal(16))).setDetailedDeliveryPeriod(sdf.parse("2020-01-13"),sdf.parse("2020-01-15"))) .addCharge(new Charge(new BigDecimal(0.5)).setReason("quick delivery charge").setTaxPercent(new BigDecimal(16))) .addAllowance(new Allowance(new BigDecimal(0.2)).setReason("discount").setTaxPercent(new BigDecimal(16))) .setDetailedDeliveryPeriod(sdf.parse("2020-01-01"),sdf.parse("2020-01-31")) @@ -284,6 +287,10 @@ public class ZF2PushTest extends TestCase { assertTrue(zi.getUTF8().contains("0009845")); assertTrue(zi.getUTF8().contains("0008734")); + + assertTrue(zi.getUTF8().contains("20200113")); // to contain item delivery periods + assertTrue(zi.getUTF8().contains("20200115")); // to contain item delivery periods + assertTrue(zi.getUTF8().contains("item level 1/1")); assertTrue(zi.getUTF8().contains("DE4711")); // the VAT ID should be there... assertFalse(zi.getUTF8().contains("DE47110")); // but not the VAT ID of the shiptotradeparty