working on cash discount abstraction (XR and reading is still missing)

This commit is contained in:
jstaerk
2024-05-09 08:59:20 +02:00
parent e8bbc6c008
commit 757910fff8
9 changed files with 309 additions and 213 deletions

View File

@@ -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
=======

View File

@@ -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,

View File

@@ -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 "<ram:SpecifiedTradePaymentTerms>"+
"<ram:Description>Cash Discount</ram:Description>"+
" <ram:ApplicableTradePaymentDiscountTerms>"+
" <ram:BasisPeriodMeasure unitCode=\"DAY\">"+days+"</ram:BasisPeriodMeasure>"+
" <ram:CalculationPercent>"+XMLTools.nDigitFormat(percent,3)+"</ram:CalculationPercent>"+
" </ram:ApplicableTradePaymentDiscountTerms>"+
"</ram:SpecifiedTradePaymentTerms>";
}
/***
* 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";
}
}

View File

@@ -26,12 +26,7 @@ import java.util.Collection;
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,13 +42,14 @@ 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<CashDiscount> cashDiscounts = null;
@JsonDeserialize(contentAs = Item.class)
protected ArrayList<IZUGFeRDExportableItem> ZFItems = null;
protected ArrayList<String> notes = null;
private List<IncludedNote> includedNotes = null;
protected String sellerOrderReferencedDocumentID;
protected String contractReferencedDocument = null;
protected ArrayList<FileAttachment> xmlEmbeddedFiles=null;
protected ArrayList<FileAttachment> xmlEmbeddedFiles = null;
protected BigDecimal totalPrepaidAmount = null;
protected Date detailedDeliveryDateStart = null;
@@ -70,6 +66,7 @@ public class Invoice implements IExportableTransaction {
public Invoice() {
ZFItems = new ArrayList<>();
cashDiscounts = new ArrayList<>();
setCurrency("EUR");
}
@@ -100,7 +97,7 @@ public class Invoice implements IExportableTransaction {
public Invoice embedFileInXML(FileAttachment fa) {
if (xmlEmbeddedFiles == null) {
xmlEmbeddedFiles= new ArrayList<>();
xmlEmbeddedFiles = new ArrayList<>();
}
xmlEmbeddedFiles.add(fa);
return this;
@@ -115,6 +112,10 @@ public class Invoice implements IExportableTransaction {
}
@Override
public IZUGFeRDCashDiscount[] getCashDiscounts() {
return cashDiscounts.toArray(new IZUGFeRDCashDiscount[0]);
}
@Override
public String getNumber() {
@@ -139,6 +140,7 @@ public class Invoice implements IExportableTransaction {
documentCode = DocumentCodeTypeConstants.CORRECTEDINVOICE;
return this;
}
public Invoice setCreditNote() {
documentCode = DocumentCodeTypeConstants.CREDITNOTE;
return this;
@@ -229,6 +231,7 @@ public class Invoice implements IExportableTransaction {
public String getBuyerOrderReferencedDocumentID() {
return buyerOrderReferencedDocumentID;
}
@Override
public String getSellerOrderReferencedDocumentID() {
return sellerOrderReferencedDocumentID;
@@ -239,6 +242,7 @@ public class Invoice implements IExportableTransaction {
this.sellerOrderReferencedDocumentID = sellerOrderReferencedDocumentID;
return this;
}
/***
* usually the order number
* @param buyerOrderReferencedDocumentID string with number
@@ -258,6 +262,7 @@ public class Invoice implements IExportableTransaction {
this.invoiceReferencedDocumentID = invoiceReferencedDocumentID;
return this;
}
@Override
public String getInvoiceReferencedDocumentID() {
return invoiceReferencedDocumentID;
@@ -285,7 +290,7 @@ public class Invoice implements IExportableTransaction {
* @return fluent setter
*/
public Invoice setTotalPrepaidAmount(BigDecimal prepaid) {
totalPrepaidAmount=prepaid;
totalPrepaidAmount = prepaid;
return this;
}
@@ -480,6 +485,7 @@ public class Invoice implements IExportableTransaction {
/**
* required.
* sets the invoice receiving institution = invoicee
*
* @param recipient the invoicee organisation
* @return fluent setter
*/
@@ -491,6 +497,7 @@ public class Invoice implements IExportableTransaction {
/**
* required.
* sets the invoicing institution = invoicer
*
* @param sender the invoicer
* @return fluent setter
*/
@@ -569,6 +576,16 @@ public class Invoice implements IExportableTransaction {
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() {
@@ -576,15 +593,16 @@ public class Invoice implements IExportableTransaction {
}
public void setZFItems(ArrayList<IZUGFeRDExportableItem> ims) {
ZFItems=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 class Invoice implements IExportableTransaction {
/**
* adds a free text paragraph, which will become an includedNote element
*
* @param text freeform UTF8 plain text
* @return fluent setter
*/
@@ -694,6 +713,7 @@ public class Invoice implements IExportableTransaction {
/**
* 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
*/
@@ -708,6 +728,7 @@ public class Invoice implements IExportableTransaction {
/**
* 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
*/
@@ -722,6 +743,7 @@ public class Invoice implements IExportableTransaction {
/**
* 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
*/
@@ -736,6 +758,7 @@ public class Invoice implements IExportableTransaction {
/**
* 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
*/
@@ -750,6 +773,7 @@ public class Invoice implements IExportableTransaction {
/**
* 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
*/
@@ -764,6 +788,7 @@ public class Invoice implements IExportableTransaction {
/**
* 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
*/
@@ -778,6 +803,7 @@ public class Invoice implements IExportableTransaction {
/**
* 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
*/
@@ -788,9 +814,11 @@ public class Invoice implements IExportableTransaction {
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
*/
@@ -841,6 +869,7 @@ public class Invoice implements IExportableTransaction {
/**
* Decide when the VAT should be collected.
*
* @param vatDueDateTypeCode use EventTimeCodeTypeConstants
* @return fluent setter
*/

View File

@@ -120,36 +120,6 @@ public class XMLTools extends XMLWriter {
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 <a href="https://www.w3.org/TR/xml/#sec-guessing">Autodetection of Character Encodings</a>
*
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

View File

@@ -140,6 +140,11 @@ public interface IExportableTransaction {
return null;
}
default IZUGFeRDCashDiscount[] getCashDiscounts() { return null; }
/***
* @return the invoice line items with the positions
*/
IZUGFeRDExportableItem[] getZFItems();
/**

View File

@@ -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();
}

View File

@@ -689,6 +689,12 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
} 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 = "<ram:AllowanceTotalAmount>" + currencyFormat(calc.getAllowancesForPercent(null)) + "</ram:AllowanceTotalAmount>";

View File

@@ -93,7 +93,7 @@ public class ZF2PushTest extends TestCase {
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 class ZF2PushTest extends TestCase {
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
ze.export(TARGET_ATTACHMENTSPDF);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
}
// now check the contents (like MustangReaderTest)
@@ -192,11 +192,11 @@ public class ZF2PushTest extends TestCase {
//assertEquals(IBAN,read.getSender().getBankDetails().get(0).getIBAN());
ze.export(TARGET_BANKPDF);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
} catch (XPathExpressionException e) {
fail("XPathException should not be raised in testEdgeExport");
fail("XPathException should not be raised");
} catch (ParseException e) {
fail("ParseException should not be raised in testEdgeExport");
fail("ParseException should not be raised");
}
@@ -230,7 +230,7 @@ public class ZF2PushTest extends TestCase {
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
ze.export(TARGET_ITEMCHARGESALLOWANCESPDF);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
}
// now check the contents (like MustangReaderTest)
@@ -284,7 +284,7 @@ public class ZF2PushTest extends TestCase {
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
ze.export(TARGET_INTRACOMMUNITYSUPPLYPDF);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
}
// now check the contents (like MustangReaderTest)
@@ -333,7 +333,7 @@ public class ZF2PushTest extends TestCase {
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
ze.export(TARGET_REVERSECHARGEPDF);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
}
// now check the contents (like MustangReaderTest)
@@ -379,7 +379,7 @@ public class ZF2PushTest extends TestCase {
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
ze.export(TARGET_CHARGESALLOWANCESPDF);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
}
// now check the contents (like MustangReaderTest)
@@ -417,7 +417,7 @@ public class ZF2PushTest extends TestCase {
.getResourceAsStream("/MustangGnuaccountingBeispielRE-20170509_505blanko.pdf");
ZUGFeRDExporterFromA1 ze = new ZUGFeRDExporterFromA1().setProducer("My Application")
.setCreator(System.getProperty("user.name")).setZUGFeRDVersion(2).ignorePDFAErrors()
.setCreator(System.getProperty("user.name")).setZUGFeRDVersion(2).setProfile(Profiles.getByName("extended")).ignorePDFAErrors()
.load(SOURCE_PDF)) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
@@ -435,6 +435,7 @@ public class ZF2PushTest extends TestCase {
.addItem(new Item(new Product("Testprodukt", "", "C62", new BigDecimal(16)).addGlobalID(gtin).setSellerAssignedID("4711"), price, new BigDecimal(1.0)).addReferencedLineID("xxx").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)))
.addCashDiscount(new CashDiscount(new BigDecimal(2),14))
.setDeliveryDate(sdf.parse("2020-11-02")).setOwnVATID("DE0815").setNumber(number).setVATDueDateTypeCode(EventTimeCodeTypeConstants.PAYMENT_DATE)
);
} catch (ParseException e) {
@@ -445,7 +446,7 @@ public class ZF2PushTest extends TestCase {
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
ze.export(TARGET_PUSHEDGE);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
}
// now check the contents (like MustangReaderTest)
@@ -473,7 +474,8 @@ public class ZF2PushTest extends TestCase {
assertTrue(zi.getUTF8().contains("DE4711")); // the VAT ID should be there...
assertFalse(zi.getUTF8().contains("DE47110")); // but not the VAT ID of the shiptotradeparty
assertTrue(zi.getUTF8().contains("document level 2/2"));
assertFalse(zi.getUTF8().contains("++49555123456")); // in profile EN16931 contact fax number is not allowed
assertTrue(zi.getUTF8().contains("++49555123456"));
assertTrue(zi.getUTF8().contains("Cash Discount")); // default description for cash discounts
assertThat(zi.getUTF8()).valueByXPath("//*[local-name()='ApplicableTradeTax']/*[local-name()='DueDateTypeCode']").asString()
.isEqualTo(EventTimeCodeTypeConstants.PAYMENT_DATE);
@@ -486,9 +488,9 @@ public class ZF2PushTest extends TestCase {
} catch (XPathExpressionException e) {
fail("XPathExpressionException should not be raised in testEdgeExport");
fail("XPathExpressionException should not be raised");
} catch (ParseException e) {
fail("ParseException should not be raised in testEdgeExport");
fail("ParseException should not be raised");
/* a parseException would also be fired if the calculated grand total does not
match the read grand total */
}
@@ -518,7 +520,7 @@ public class ZF2PushTest extends TestCase {
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
ze.export(TARGET_ALLOWANCESPDF);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
}
// now check the contents (like MustangReaderTest)
@@ -563,7 +565,7 @@ public class ZF2PushTest extends TestCase {
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
ze.export(TARGET_RELATIVECHARGESALLOWANCESPDF);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
}
// now check the contents (like MustangReaderTest)
@@ -613,7 +615,7 @@ public class ZF2PushTest extends TestCase {
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
ze.export(TARGET_CORRECTIONPDF);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
}
// now check the contents (like MustangReaderTest)
@@ -662,7 +664,7 @@ public class ZF2PushTest extends TestCase {
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
ze.export(TARGET_CREDITNOTEPDF);
} catch (IOException e) {
fail("IOException should not be raised in testEdgeExport");
fail("IOException should not be raised");
}
// now check the contents (like MustangReaderTest)