diff --git a/History.md b/History.md index 95f2618e3..49efca878 100644 --- a/History.md +++ b/History.md @@ -7,6 +7,8 @@ - UBL importer to also parse contacts - https://github.com/ZUGFeRD/mustangproject/pull/369 - upgrade ph-schematron from 6.3.3 to 8 +- support inputstreams https://github.com/ZUGFeRD/mustangproject/pull/379 +- #314 ZUGFeRDInvoiceImporter additional constructur 2.10.0 ======= diff --git a/doc/development_documentation.md b/doc/development_documentation.md index 447763201..e84890a40 100644 --- a/doc/development_documentation.md +++ b/doc/development_documentation.md @@ -1,5 +1,5 @@ -## General approach +## Typical process 1. build @@ -29,6 +29,18 @@ If you do a pull request, please do a feature branch, e.g. if you are working on Most of mustang is a library, adding (autmated junit) test cases is often not only the most sustainable but also the fastest way to see if new/changed functionality works. If something is changed so that old test cases break on purpose please do not just remove them but take the time to fix the test cases +## Typical workflow + +If e.g. new elements or attributes are added, they are often added +* in the object so that a developer can use them +* in the interface so that a old fashioned developer could use them as well +* in the pullprovider so that it actually finds it's way into the XML +* in at least one test, after the test has been run this should at least once be +* validated. If that works one can start implementing the +* reading part (along with tests), then it needs to be +* documented e.g. on the homepage and +* communicated, at the very least by mentioning it in the history.md + ## Architecture Mustang contains a library to read/write e-invoices, diff --git a/library/src/main/java/org/mustangproject/CashDiscount.java b/library/src/main/java/org/mustangproject/CashDiscount.java new file mode 100644 index 000000000..b88c5517e --- /dev/null +++ b/library/src/main/java/org/mustangproject/CashDiscount.java @@ -0,0 +1,50 @@ +package org.mustangproject; + +import org.mustangproject.ZUGFeRD.IZUGFeRDCashDiscount; + +import java.math.BigDecimal; + +public class CashDiscount implements IZUGFeRDCashDiscount { + + protected BigDecimal percent; + protected Integer days=null; + + /*** + * Create a cash discount (skonto) with the specified height in the specified period. + * Should someone add more period types than just "days" there + * is be space for a (optional) third parameter + * + * @param percent max 3 decimals "behind the dot", more precision is currently ignored + * @param days + */ + public CashDiscount(BigDecimal percent, int days) { + this.percent = percent; + this.days = days; + } + + /*** + * @return this particular cash discount as cross industry invoice XML + */ + public String getAsCII() { + return ""+ + "Cash Discount"+ + " "+ + " "+days+""+ + " "+XMLTools.nDigitFormat(percent,3)+""+ + " "+ + ""; + } + + /*** + * since EN16931 voted not to have (or even allow) cash discounts in their core invoice the german + * XRechnung CIUS defined it's own proprietary format for a freetext field + * @return this particular cash discount in proprietary xrechnung format + */ + public String getAsXRechnung() { + return "#SKONTO#TAGE="+days+"#PROZENT="+XMLTools.nDigitFormat(percent,3)+"#\n"; + } + + + + +} diff --git a/library/src/main/java/org/mustangproject/Invoice.java b/library/src/main/java/org/mustangproject/Invoice.java index 2a1e8cd1d..aa26b64f1 100644 --- a/library/src/main/java/org/mustangproject/Invoice.java +++ b/library/src/main/java/org/mustangproject/Invoice.java @@ -26,12 +26,7 @@ import java.util.Date; import java.util.List; -import org.mustangproject.ZUGFeRD.IExportableTransaction; -import org.mustangproject.ZUGFeRD.IZUGFeRDAllowanceCharge; -import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem; -import org.mustangproject.ZUGFeRD.IZUGFeRDExportableTradeParty; -import org.mustangproject.ZUGFeRD.IZUGFeRDPaymentTerms; -import org.mustangproject.ZUGFeRD.IZUGFeRDTradeSettlement; +import org.mustangproject.ZUGFeRD.*; import org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -47,20 +42,21 @@ public class Invoice implements IExportableTransaction { protected String documentName = null, documentCode = null, number = null, ownOrganisationFullPlaintextInfo = null, referenceNumber = null, shipToOrganisationID = null, shipToOrganisationName = null, shipToStreet = null, shipToZIP = null, shipToLocation = null, shipToCountry = null, buyerOrderReferencedDocumentID = null, invoiceReferencedDocumentID = null, buyerOrderReferencedDocumentIssueDateTime = null, ownForeignOrganisationID = null, ownOrganisationName = null, currency = null, paymentTermDescription = null; protected Date issueDate = null, dueDate = null, deliveryDate = null; protected TradeParty sender = null, recipient = null, deliveryAddress = null; - @JsonDeserialize(contentAs=Item.class) + protected ArrayList cashDiscounts = null; + @JsonDeserialize(contentAs = Item.class) protected ArrayList ZFItems = null; protected ArrayList notes = null; - private List includedNotes = null; - protected String sellerOrderReferencedDocumentID; + private List includedNotes = null; + protected String sellerOrderReferencedDocumentID; protected String contractReferencedDocument = null; - protected ArrayList xmlEmbeddedFiles=null; + protected ArrayList xmlEmbeddedFiles = null; protected BigDecimal totalPrepaidAmount = null; protected Date detailedDeliveryDateStart = null; protected Date detailedDeliveryPeriodEnd = null; protected ArrayList Allowances = new ArrayList<>(), - Charges = new ArrayList<>(), LogisticsServiceCharges = new ArrayList<>(); + Charges = new ArrayList<>(), LogisticsServiceCharges = new ArrayList<>(); protected IZUGFeRDPaymentTerms paymentTerms = null; protected Date invoiceReferencedIssueDate; protected String specifiedProcuringProjectID = null; @@ -70,6 +66,7 @@ public class Invoice implements IExportableTransaction { public Invoice() { ZFItems = new ArrayList<>(); + cashDiscounts = new ArrayList<>(); setCurrency("EUR"); } @@ -79,7 +76,7 @@ public String getDocumentName() { } @Override - public String getContractReferencedDocument() { + public String getContractReferencedDocument() { return contractReferencedDocument; } @@ -100,14 +97,14 @@ public Invoice setDocumentCode(String documentCode) { public Invoice embedFileInXML(FileAttachment fa) { if (xmlEmbeddedFiles == null) { - xmlEmbeddedFiles= new ArrayList<>(); + xmlEmbeddedFiles = new ArrayList<>(); } xmlEmbeddedFiles.add(fa); return this; } @Override - public FileAttachment[] getAdditionalReferencedDocuments() { + public FileAttachment[] getAdditionalReferencedDocuments() { if (xmlEmbeddedFiles == null) { return null; } @@ -115,6 +112,10 @@ public FileAttachment[] getAdditionalReferencedDocuments() { } + @Override + public IZUGFeRDCashDiscount[] getCashDiscounts() { + return cashDiscounts.toArray(new IZUGFeRDCashDiscount[0]); + } @Override public String getNumber() { @@ -139,6 +140,7 @@ public Invoice setCorrection(String number) { documentCode = DocumentCodeTypeConstants.CORRECTEDINVOICE; return this; } + public Invoice setCreditNote() { documentCode = DocumentCodeTypeConstants.CREDITNOTE; return this; @@ -229,16 +231,18 @@ public Invoice setShipToCountry(String shipToCountry) { public String getBuyerOrderReferencedDocumentID() { return buyerOrderReferencedDocumentID; } + @Override public String getSellerOrderReferencedDocumentID() { return sellerOrderReferencedDocumentID; } - public Invoice setSellerOrderReferencedDocumentID(String sellerOrderReferencedDocumentID) { - this.sellerOrderReferencedDocumentID = sellerOrderReferencedDocumentID; - return this; - } + public Invoice setSellerOrderReferencedDocumentID(String sellerOrderReferencedDocumentID) { + this.sellerOrderReferencedDocumentID = sellerOrderReferencedDocumentID; + return this; + } + /*** * usually the order number * @param buyerOrderReferencedDocumentID string with number @@ -258,22 +262,23 @@ public Invoice setInvoiceReferencedDocumentID(String invoiceReferencedDocumentID this.invoiceReferencedDocumentID = invoiceReferencedDocumentID; return this; } + @Override public String getInvoiceReferencedDocumentID() { return invoiceReferencedDocumentID; } - @Override - public Date getInvoiceReferencedIssueDate() { - return invoiceReferencedIssueDate; - } + @Override + public Date getInvoiceReferencedIssueDate() { + return invoiceReferencedIssueDate; + } - public Invoice setInvoiceReferencedIssueDate(Date issueDate) { - this.invoiceReferencedIssueDate = issueDate; - return this; - } - - @Override + public Invoice setInvoiceReferencedIssueDate(Date issueDate) { + this.invoiceReferencedIssueDate = issueDate; + return this; + } + + @Override public String getBuyerOrderReferencedDocumentIssueDateTime() { return buyerOrderReferencedDocumentIssueDateTime; } @@ -285,7 +290,7 @@ public String getBuyerOrderReferencedDocumentIssueDateTime() { * @return fluent setter */ public Invoice setTotalPrepaidAmount(BigDecimal prepaid) { - totalPrepaidAmount=prepaid; + totalPrepaidAmount = prepaid; return this; } @@ -380,13 +385,13 @@ public String getOwnZIP() { @Override - public String getOwnLocation() { + public String getOwnLocation() { return sender.getLocation(); } @Override - public String getOwnCountry() { + public String getOwnCountry() { return sender.getCountry(); } @@ -399,12 +404,12 @@ public String[] getNotes() { return notes.toArray(new String[0]); } - @Override - public List getNotesWithSubjectCode() { - return includedNotes; - } + @Override + public List getNotesWithSubjectCode() { + return includedNotes; + } - @Override + @Override public String getCurrency() { return currency; } @@ -473,13 +478,14 @@ public Invoice setOwnContact(Contact ownContact) { } @Override - public TradeParty getRecipient() { + public TradeParty getRecipient() { return recipient; } /** * required. * sets the invoice receiving institution = invoicee + * * @param recipient the invoicee organisation * @return fluent setter */ @@ -491,6 +497,7 @@ public Invoice setRecipient(TradeParty recipient) { /** * required. * sets the invoicing institution = invoicer + * * @param sender the invoicer * @return fluent setter */ @@ -508,8 +515,8 @@ public IZUGFeRDAllowanceCharge[] getZFAllowances() { if (Allowances.isEmpty()) { return null; } else { - return Allowances.toArray(new IZUGFeRDAllowanceCharge[0]); - } + return Allowances.toArray(new IZUGFeRDAllowanceCharge[0]); + } } @@ -518,8 +525,8 @@ public IZUGFeRDAllowanceCharge[] getZFCharges() { if (Charges.isEmpty()) { return null; } else { - return Charges.toArray(new IZUGFeRDAllowanceCharge[0]); - } + return Charges.toArray(new IZUGFeRDAllowanceCharge[0]); + } } @@ -528,8 +535,8 @@ public IZUGFeRDAllowanceCharge[] getZFLogisticsServiceCharges() { if (LogisticsServiceCharges.isEmpty()) { return null; } else { - return LogisticsServiceCharges.toArray(new IZUGFeRDAllowanceCharge[0]); - } + return LogisticsServiceCharges.toArray(new IZUGFeRDAllowanceCharge[0]); + } } @@ -569,22 +576,33 @@ public Invoice setDeliveryAddress(TradeParty deliveryAddress) { this.deliveryAddress = deliveryAddress; return this; } + /*** + * Adds a cash discount (skonto) + * @param CashDiscount the percent/period combination + * @return fluent setter + */ + public Invoice addCashDiscount(CashDiscount c) { + this.cashDiscounts.add(c); + return this; + } + @Override public IZUGFeRDExportableItem[] getZFItems() { return ZFItems.toArray(new IZUGFeRDExportableItem[0]); } - public void setZFItems(ArrayList ims) { - ZFItems=ims; + public void setZFItems(ArrayList ims) { + ZFItems = ims; } /** * required * adds invoice "lines" :-) - * @see Item + * * @param item the invoice line * @return fluent setter + * @see Item */ public Invoice addItem(IZUGFeRDExportableItem item) { ZFItems.add(item); @@ -669,6 +687,7 @@ public Date getDetailedDeliveryPeriodTo() { /** * adds a free text paragraph, which will become an includedNote element + * * @param text freeform UTF8 plain text * @return fluent setter */ @@ -679,129 +698,138 @@ public Invoice addNote(String text) { notes.add(text); return this; } - - public Invoice addNotes(Collection notes) { - if (notes == null) { - return this; - } - if (includedNotes == null) { - includedNotes = new ArrayList<>(); - } - includedNotes.addAll(notes); - return this; - } - - /** - * adds a free text paragraph, which will become an includedNote element with explicit - * subjectCode {@link SubjectCode#AAI} - * @param content freeform UTF8 plain text - * @return fluent setter - */ - public Invoice addGeneralNote(String content) { - if (includedNotes == null) { - includedNotes = new ArrayList<>(); - } - includedNotes.add(IncludedNote.generalNote(content)); - return this; - } - - /** - * adds a free text paragraph, which will become an includedNote element with explicit - * subjectCode {@link SubjectCode#REG} - * @param content freeform UTF8 plain text - * @return fluent setter - */ - public Invoice addRegulatoryNote(String content) { - if (includedNotes == null) { - includedNotes = new ArrayList<>(); - } - includedNotes.add(IncludedNote.regulatoryNote(content)); - return this; - } - - /** - * adds a free text paragraph, which will become an includedNote element with explicit - * subjectCode {@link SubjectCode#ABL} - * @param content freeform UTF8 plain text - * @return fluent setter - */ - public Invoice addLegalNote(String content) { - if (includedNotes == null) { - includedNotes = new ArrayList<>(); - } - includedNotes.add(IncludedNote.legalNote(content)); - return this; - } - - /** - * adds a free text paragraph, which will become an includedNote element with explicit - * subjectCode {@link SubjectCode#CUS} - * @param content freeform UTF8 plain text - * @return fluent setter - */ - public Invoice addCustomsNote(String content) { - if (includedNotes == null) { - includedNotes = new ArrayList<>(); - } - includedNotes.add(IncludedNote.customsNote(content)); - return this; - } - - /** - * adds a free text paragraph, which will become an includedNote element with explicit - * subjectCode {@link SubjectCode#SUR} - * @param content freeform UTF8 plain text - * @return fluent setter - */ - public Invoice addSellerNote(String content) { - if (includedNotes == null) { - includedNotes = new ArrayList<>(); - } - includedNotes.add(IncludedNote.sellerNote(content)); - return this; - } - - /** - * adds a free text paragraph, which will become an includedNote element with explicit - * subjectCode {@link SubjectCode#TXD} - * @param content freeform UTF8 plain text - * @return fluent setter - */ - public Invoice addTaxNote(String content) { - if (includedNotes == null) { - includedNotes = new ArrayList<>(); - } - includedNotes.add(IncludedNote.taxNote(content)); - return this; - } - - /** - * adds a free text paragraph, which will become an includedNote element with explicit - * subjectCode {@link SubjectCode#ACY} - * @param content freeform UTF8 plain text - * @return fluent setter - */ - public Invoice addIntroductionNote(String content) { - if (includedNotes == null) { - includedNotes = new ArrayList<>(); - } - includedNotes.add(IncludedNote.introductionNote(content)); - return this; - } - /** - * adds a free text paragraph, which will become an includedNote element with explicit - * subjectCode {@link SubjectCode#AAK} - * @param content freeform UTF8 plain text - * @return fluent setter - */ - public Invoice addDiscountBonusNote(String content) { - if (includedNotes == null) { - includedNotes = new ArrayList<>(); - } - includedNotes.add(IncludedNote.discountBonusNote(content)); - return this; - } - + + public Invoice addNotes(Collection notes) { + if (notes == null) { + return this; + } + if (includedNotes == null) { + includedNotes = new ArrayList<>(); + } + includedNotes.addAll(notes); + return this; + } + + /** + * adds a free text paragraph, which will become an includedNote element with explicit + * subjectCode {@link SubjectCode#AAI} + * + * @param content freeform UTF8 plain text + * @return fluent setter + */ + public Invoice addGeneralNote(String content) { + if (includedNotes == null) { + includedNotes = new ArrayList<>(); + } + includedNotes.add(IncludedNote.generalNote(content)); + return this; + } + + /** + * adds a free text paragraph, which will become an includedNote element with explicit + * subjectCode {@link SubjectCode#REG} + * + * @param content freeform UTF8 plain text + * @return fluent setter + */ + public Invoice addRegulatoryNote(String content) { + if (includedNotes == null) { + includedNotes = new ArrayList<>(); + } + includedNotes.add(IncludedNote.regulatoryNote(content)); + return this; + } + + /** + * adds a free text paragraph, which will become an includedNote element with explicit + * subjectCode {@link SubjectCode#ABL} + * + * @param content freeform UTF8 plain text + * @return fluent setter + */ + public Invoice addLegalNote(String content) { + if (includedNotes == null) { + includedNotes = new ArrayList<>(); + } + includedNotes.add(IncludedNote.legalNote(content)); + return this; + } + + /** + * adds a free text paragraph, which will become an includedNote element with explicit + * subjectCode {@link SubjectCode#CUS} + * + * @param content freeform UTF8 plain text + * @return fluent setter + */ + public Invoice addCustomsNote(String content) { + if (includedNotes == null) { + includedNotes = new ArrayList<>(); + } + includedNotes.add(IncludedNote.customsNote(content)); + return this; + } + + /** + * adds a free text paragraph, which will become an includedNote element with explicit + * subjectCode {@link SubjectCode#SUR} + * + * @param content freeform UTF8 plain text + * @return fluent setter + */ + public Invoice addSellerNote(String content) { + if (includedNotes == null) { + includedNotes = new ArrayList<>(); + } + includedNotes.add(IncludedNote.sellerNote(content)); + return this; + } + + /** + * adds a free text paragraph, which will become an includedNote element with explicit + * subjectCode {@link SubjectCode#TXD} + * + * @param content freeform UTF8 plain text + * @return fluent setter + */ + public Invoice addTaxNote(String content) { + if (includedNotes == null) { + includedNotes = new ArrayList<>(); + } + includedNotes.add(IncludedNote.taxNote(content)); + return this; + } + + /** + * adds a free text paragraph, which will become an includedNote element with explicit + * subjectCode {@link SubjectCode#ACY} + * + * @param content freeform UTF8 plain text + * @return fluent setter + */ + public Invoice addIntroductionNote(String content) { + if (includedNotes == null) { + includedNotes = new ArrayList<>(); + } + includedNotes.add(IncludedNote.introductionNote(content)); + return this; + } + + /** + * adds a free text paragraph, which will become an includedNote element with explicit + * subjectCode {@link SubjectCode#AAK} + * + * @param content freeform UTF8 plain text + * @return fluent setter + */ + public Invoice addDiscountBonusNote(String content) { + if (includedNotes == null) { + includedNotes = new ArrayList<>(); + } + includedNotes.add(IncludedNote.discountBonusNote(content)); + return this; + } + @Override public String getSpecifiedProcuringProjectID() { return specifiedProcuringProjectID; @@ -841,6 +869,7 @@ public String getVATDueDateTypeCode() { /** * Decide when the VAT should be collected. + * * @param vatDueDateTypeCode use EventTimeCodeTypeConstants * @return fluent setter */ diff --git a/library/src/main/java/org/mustangproject/XMLTools.java b/library/src/main/java/org/mustangproject/XMLTools.java index e85fe5429..2eda39bbc 100644 --- a/library/src/main/java/org/mustangproject/XMLTools.java +++ b/library/src/main/java/org/mustangproject/XMLTools.java @@ -120,36 +120,6 @@ public static String encodeXML(CharSequence s) { return sb.toString(); } - - /** - * Returns the Byte Order Mark size and thus allows to skips over a BOM - * at the beginning of the given ByteArrayInputStream, if one exists. - * - * @param is the ByteArrayInputStream used - * @throws IOException if can not be read from is - * @see Autodetection of Character Encodings - * - public static int guessBOMSize(ByteArrayInputStream is) throws IOException { - byte[] pad = new byte[4]; - is.read(pad); - is.reset(); - int test2 = ((pad[0] & 0xFF) << 8) | (pad[1] & 0xFF); - int test3 = ((test2 & 0xFFFF) << 8) | (pad[2] & 0xFF); - int test4 = ((test3 & 0xFFFFFF) << 8) | (pad[3] & 0xFF); - // - if (test4 == 0x0000FEFF || test4 == 0xFFFE0000 || test4 == 0x0000FFFE || test4 == 0xFEFF0000) { - // UCS-4: BOM takes 4 bytes - return 4; - } else if (test3 == 0xEFBBFF) { - // UTF-8: BOM takes 3 bytes - return 3; - } else if (test2 == 0xFEFF || test2 == 0xFFFE) { - // UTF-16: BOM takes 2 bytes - return 2; - } - return 0; - }*/ - /*** * removes utf8 byte order marks from byte arrays, in case one is there * @param zugferdRaw the CII XML diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java index 6d7d7f896..219b7022d 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java @@ -140,6 +140,11 @@ default IZUGFeRDAllowanceCharge[] getZFLogisticsServiceCharges() { return null; } + default IZUGFeRDCashDiscount[] getCashDiscounts() { return null; } + + /*** + * @return the invoice line items with the positions + */ IZUGFeRDExportableItem[] getZFItems(); /** diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDCashDiscount.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDCashDiscount.java new file mode 100644 index 000000000..90040a500 --- /dev/null +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDCashDiscount.java @@ -0,0 +1,20 @@ +package org.mustangproject.ZUGFeRD; + +import org.mustangproject.XMLTools; + +public interface IZUGFeRDCashDiscount { + + + /*** + * @return this particular cash discount as cross industry invoice XML + */ + public String getAsCII(); + + /*** + * since EN16931 voted not to have (or even allow) cash discounts in their core invoice the german + * XRechnung CIUS defined it's own proprietary format for a freetext field + * @return this particular cash discount in proprietary xrechnung format + */ + public String getAsXRechnung(); + +} diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index 4e3077df8..1a935abc9 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -689,6 +689,12 @@ public void generateXML(IExportableTransaction trans) { } else { xml += buildPaymentTermsXml(); } + if ((profile == Profiles.getByName("Extended"))&&(trans.getCashDiscounts()!=null)&&(trans.getCashDiscounts().length>0)) { + for (IZUGFeRDCashDiscount discount:trans.getCashDiscounts() + ) { + xml += discount.getAsCII(); + } + } final String allowanceTotalLine = "" + currencyFormat(calc.getAllowancesForPercent(null)) + ""; diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java index 01115e095..40978a4e3 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java @@ -93,7 +93,7 @@ public void testPushExport() { ze.export(TARGET_PDF); } catch (IOException | ParseException e) { - fail("Exception should not be raised in testPushExport"); + fail("Exception should not be raised"); } // now check the contents (like MustangReaderTest) @@ -142,7 +142,7 @@ public void testAttachmentsExport() { assertTrue(theXML.contains("