diff --git a/History.md b/History.md index 701106d89..21dc49d36 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,30 @@ +#705 +#707 +-#708 +-#709 +607 +649 +650 +665 +684 +703 +701 +691 + + + + + +2.16.1 +======= +2025-01-21 +- #678 some ubl creditnote attributes are not parsed +- #679 validation of a XR does not ignore whitespace +- #681 IBAN assigned to invoice sender not recipient on direct debit +- #689 incorrect element order when both charge reason and reasoncode are specified +- be able to set detailedDeliveryPeriodFrom, detailedDeliveryPeriodTo MS188 +- updated verapdf from 1.26.1 to 1.26.2 +- cashDiscount JSON now corrently ignores values for cii and xr methods 2.16.0 ======= diff --git a/Mustang-CLI/pom.xml b/Mustang-CLI/pom.xml index 9b5e3bc9c..d5b924e99 100644 --- a/Mustang-CLI/pom.xml +++ b/Mustang-CLI/pom.xml @@ -3,7 +3,7 @@ org.mustangproject core - 2.15.3-SNAPSHOT + 2.16.2-SNAPSHOT 4.0.0 org.mustangproject @@ -12,7 +12,7 @@ should also work for XRechnung/CII. jar - 2.15.3-SNAPSHOT + 2.16.2-SNAPSHOT UTF-8 11 @@ -23,7 +23,7 @@ org.mustangproject validator - 2.15.3-SNAPSHOT + 2.16.2-SNAPSHOT diff --git a/library/pom.xml b/library/pom.xml index 073661c1f..916957913 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -3,13 +3,13 @@ org.mustangproject core - 2.15.3-SNAPSHOT + 2.16.2-SNAPSHOT 4.0.0 org.mustangproject library - 2.15.3-SNAPSHOT + 2.16.2-SNAPSHOT jar Library to write, read and validate e-invoices (Factur-X, ZUGFeRD, Order-X, XRechnung/CII) FOSS Java library to read, write and validate european electronic invoices and orders in the UN/CEFACT @@ -59,6 +59,12 @@ slf4j-api 2.0.9 + + net.sf.offo + fop-hyph + 2.0 + runtime + net.sf.saxon diff --git a/library/src/main/java/org/mustangproject/CashDiscount.java b/library/src/main/java/org/mustangproject/CashDiscount.java index 2151489aa..b1993a020 100644 --- a/library/src/main/java/org/mustangproject/CashDiscount.java +++ b/library/src/main/java/org/mustangproject/CashDiscount.java @@ -1,5 +1,6 @@ package org.mustangproject; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import org.mustangproject.ZUGFeRD.IZUGFeRDCashDiscount; @@ -63,6 +64,7 @@ public CashDiscount setDays(Integer days) { /*** * @return this particular cash discount as cross industry invoice XML */ + @JsonIgnore public String getAsCII() { return ""+ "Cash Discount"+ @@ -78,6 +80,7 @@ public String getAsCII() { * XRechnung CIUS defined it's own proprietary format for a freetext field * @return this particular cash discount in proprietary xrechnung format */ + @JsonIgnore public String getAsXRechnung() { return "#SKONTO#TAGE="+days+"#PROZENT="+XMLTools.nDigitFormat(percent,2)+"#\n"; } diff --git a/library/src/main/java/org/mustangproject/Invoice.java b/library/src/main/java/org/mustangproject/Invoice.java index 04c8b5dc3..a03487ae6 100644 --- a/library/src/main/java/org/mustangproject/Invoice.java +++ b/library/src/main/java/org/mustangproject/Invoice.java @@ -69,6 +69,7 @@ public class Invoice implements IExportableTransaction { protected String vatDueDateTypeCode = null; protected String creditorReferenceID; // required when direct debit is used. private BigDecimal roundingAmount=null; + private String paymentReference; // Remittance information / Verwendungszweck, BT-83 public Invoice() { ZFItems = new ArrayList<>(); @@ -650,6 +651,16 @@ public Invoice setPaymentTerms(IZUGFeRDPaymentTerms paymentTerms) { return this; } + @Override + public String getPaymentReference() { + return paymentReference; + } + + public Invoice setPaymentReference(String paymentReference) { + this.paymentReference = paymentReference; + return this; + } + @Override public TradeParty getDeliveryAddress() { return deliveryAddress; @@ -783,11 +794,20 @@ public Date getDetailedDeliveryPeriodFrom() { return detailedDeliveryDateStart; } + public Invoice setDetailedDeliveryPeriodFrom(Date dt) { + detailedDeliveryDateStart=dt; + return this; + } + @Override public Date getDetailedDeliveryPeriodTo() { return detailedDeliveryPeriodEnd; } + public Invoice setDetailedDeliveryPeriodTo(Date dt) { + detailedDeliveryPeriodEnd=dt; + return this; + } /** * adds a free text paragraph, which will become an includedNote element diff --git a/library/src/main/java/org/mustangproject/Item.java b/library/src/main/java/org/mustangproject/Item.java index 321199a53..4084e4c0a 100644 --- a/library/src/main/java/org/mustangproject/Item.java +++ b/library/src/main/java/org/mustangproject/Item.java @@ -174,6 +174,10 @@ public Item(NodeList itemChilds, boolean recalcPrice) { .flatMap(cnm -> cnm.getAsBigDecimal("RateApplicablePercent", "ApplicablePercent")) .ifPresent(product::setVATPercent); + icnm.getAsNodeMap("ApplicableTradeTax") + .flatMap(cnm -> cnm.getAsString("ExemptionReason")) + .ifPresent(product::setTaxExemptionReason); + if (recalcPrice && !BigDecimal.ZERO.equals(quantity)) { icnm.getAsNodeMap("SpecifiedTradeSettlementLineMonetarySummation") .flatMap(cnm -> cnm.getAsBigDecimal("LineTotalAmount")) diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java index 0a013ba34..686afd441 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java @@ -314,6 +314,10 @@ default IZUGFeRDPaymentTerms getPaymentTerms() { return null; } + default String getPaymentReference() { + return null; + } + /** * returns if a rebate agreements exists * diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index 2903d81d4..6958bb618 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -288,8 +288,8 @@ protected String getAllowanceChargeStr(IZUGFeRDAllowanceCharge allowance, IAbsol final String allowanceChargeStr = "" + chargeIndicator + "" + percentage + "" + priceFormat(allowance.getTotalAmount(item)) + "" + - reason + reasonCode + + reason + ""; return allowanceChargeStr; } @@ -323,8 +323,8 @@ protected String getItemTotalAllowanceChargeStr(IZUGFeRDAllowanceCharge allowanc final String itemTotalAllowanceChargeStr = "" + chargeIndicator + "" + percentage + "" + currencyFormat(allowance.getTotalAmount(item)) + "" + - reason + reasonCode + + reason + ""; return itemTotalAllowanceChargeStr; } @@ -388,14 +388,13 @@ public void generateXML(IExportableTransaction trans) { + "" + "" + "" - + "" + XMLTools.encodeXML(trans.getNumber()) + "" - // + "RECHNUNG" - // + "380" - + "" + typecode + "" - + "" - + DATE.udtFormat(trans.getIssueDate()) + "" // date + + "" + 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) - + "" + ""; int lineID = 0; @@ -662,8 +661,8 @@ public void generateXML(IExportableTransaction trans) { if ((trans.getCreditorReferenceID() != null) && (getProfile() != Profiles.getByName("Minimum"))) { xml += "" + XMLTools.encodeXML(trans.getCreditorReferenceID()) + ""; } - if ((trans.getNumber() != null) && (getProfile() != Profiles.getByName("Minimum"))) { - xml += "" + XMLTools.encodeXML(trans.getNumber()) + ""; + if ((trans.getPaymentReference() != null) && (getProfile() != Profiles.getByName("Minimum"))) { + xml += "" + XMLTools.encodeXML(trans.getPaymentReference()) + ""; } xml += "" + trans.getCurrency() + ""; if (this.trans.getPayee() != null) { @@ -744,12 +743,12 @@ public void generateXML(IExportableTransaction trans) { "true" + "" + "" + currencyFormat(charge.getTotalAmount(calc)) + ""; - if (charge.getReason() != null) { - xml += "" + XMLTools.encodeXML(charge.getReason()) + ""; - } if (charge.getReasonCode() != null) { xml += "" + charge.getReasonCode() + ""; } + if (charge.getReason() != null) { + xml += "" + XMLTools.encodeXML(charge.getReason()) + ""; + } xml += "" + "VAT" + "" + charge.getCategoryCode() + ""; diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java index c9c448c88..323115375 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java @@ -288,6 +288,7 @@ public void setID(String id) { public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseException { String number = ""; + String documentName = null; String typeCode = null; String deliveryPeriodStart = null; String deliveryPeriodEnd = null; @@ -309,7 +310,7 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx List includedNotes = new ArrayList<>(); //UBL... - XPathExpression UBLNotesEx = xpath.compile("/*[local-name()=\"Invoice\"]/*[local-name()=\"Note\"]"); + XPathExpression UBLNotesEx = xpath.compile("/*[local-name()=\"Invoice\" or local-name()=\"CreditNote\"]/*[local-name()=\"Note\"]"); NodeList UBLNotesNd = (NodeList) UBLNotesEx.evaluate(getDocument(), XPathConstants.NODESET); if ((UBLNotesNd != null) && (UBLNotesNd.getLength() > 0)) { for (int nodeIndex = 0; nodeIndex < UBLNotesNd.getLength(); nodeIndex++) { @@ -328,8 +329,8 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx deliveryLocationNodeMap.getNode("ID").ifPresent(s -> { if (s.getAttributes().getNamedItem("schemeID") != null) { - SchemedID sID = new SchemedID().setScheme(s.getAttributes().getNamedItem("schemeID").getTextContent()).setId(s.getTextContent()); - delivery.addGlobalID(sID); + SchemedID sID = new SchemedID().setScheme(s.getAttributes().getNamedItem("schemeID").getTextContent()).setId(s.getTextContent()); + delivery.addGlobalID(sID); } }); deliveryLocationNodeMap.getAsNodeMap("Address").ifPresent(s -> { @@ -496,6 +497,9 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx if ((item.getLocalName() != null) && (item.getLocalName().equals("ID"))) { number = XMLTools.trimOrNull(item); } + if ((item.getLocalName() != null) && (item.getLocalName().equals("Name"))) { + documentName = XMLTools.trimOrNull(item); + } if ((item.getLocalName() != null) && (item.getLocalName().equals("TypeCode"))) { typeCode = XMLTools.trimOrNull(item); } @@ -556,15 +560,16 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx } zpp.addNotes(includedNotes); String rootNode = extractString("local-name(/*)"); - if (rootNode.equals("Invoice")) { + if (rootNode.equals("Invoice") || rootNode.equals("CreditNote")) { // UBL... - number = extractString("//*[local-name()=\"Invoice\"]/*[local-name()=\"ID\"]").trim(); - typeCode = extractString("//*[local-name()=\"Invoice\"]/*[local-name()=\"InvoiceTypeCode\"]").trim(); - String issueDateStr = extractString("//*[local-name()=\"Invoice\"]/*[local-name()=\"IssueDate\"]").trim(); - if (issueDateStr.length()>0) { + // //*[local-name()="Invoice" or local-name()="CreditNote"] + number = extractString("/*[local-name()=\"Invoice\" or local-name()=\"CreditNote\"]/*[local-name()=\"ID\"]").trim(); + typeCode = extractString("/*[local-name()=\"Invoice\" or local-name()=\"CreditNote\"]/*[local-name()=\"InvoiceTypeCode\"]").trim(); + String issueDateStr = extractString("/*[local-name()=\"Invoice\" or local-name()=\"CreditNote\"]/*[local-name()=\"IssueDate\"]").trim(); + if (issueDateStr.length() > 0) { issueDate = new SimpleDateFormat("yyyy-MM-dd").parse(issueDateStr); } - String dueDt = extractString("//*[local-name()=\"Invoice\"]/*[local-name()=\"DueDate\"]").trim(); + String dueDt = extractString("/*[local-name()=\"Invoice\" or local-name()=\"CreditNote\"]/*[local-name()=\"DueDate\"]").trim(); if (dueDt.length() > 0) { dueDate = new SimpleDateFormat("yyyy-MM-dd").parse(dueDt); } @@ -650,7 +655,7 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx zpp.setCurrency(currency); String paymentTermsDescription = extractString("//*[local-name()=\"SpecifiedTradePaymentTerms\"]/*[local-name()=\"Description\"]|//*[local-name()=\"PaymentTerms\"]/*[local-name()=\"Note\"]"); - if ((paymentTermsDescription!=null)&&(!paymentTermsDescription.isEmpty())) { + if ((paymentTermsDescription != null) && (!paymentTermsDescription.isEmpty())) { zpp.setPaymentTermDescription(paymentTermsDescription); } @@ -667,6 +672,12 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx NodeList headerTradeSettlementChilds = headerTradeSettlementNode.getChildNodes(); for (int settlementChildIndex = 0; settlementChildIndex < headerTradeSettlementChilds.getLength(); settlementChildIndex++) { + if ((headerTradeSettlementChilds.item(settlementChildIndex).getLocalName() != null) + && (headerTradeSettlementChilds.item(settlementChildIndex).getLocalName().equals("PaymentReference"))) { + String paymentReference = headerTradeSettlementChilds.item(settlementChildIndex).getTextContent(); + zpp.setPaymentReference(paymentReference); + } + if ((headerTradeSettlementChilds.item(settlementChildIndex).getLocalName() != null) && (headerTradeSettlementChilds.item(settlementChildIndex).getLocalName().equals("SpecifiedTradePaymentTerms"))) { NodeList paymentTermChilds = headerTradeSettlementChilds.item(settlementChildIndex).getChildNodes(); @@ -801,7 +812,7 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx } - zpp.setIssueDate(issueDate).setDueDate(dueDate).setDeliveryDate(deliveryDate).setSender(new TradeParty(SellerNodes)).setRecipient(new TradeParty(BuyerNodes)).setNumber(number).setDocumentCode(typeCode); + zpp.setIssueDate(issueDate).setDueDate(dueDate).setDeliveryDate(deliveryDate).setSender(new TradeParty(SellerNodes)).setRecipient(new TradeParty(BuyerNodes)).setNumber(number).setDocumentName(documentName).setDocumentCode(typeCode); if ((directDebitMandateID != null) && (IBAN != null)) { DirectDebit d = new DirectDebit(IBAN, directDebitMandateID); @@ -812,9 +823,11 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx d.setPaymentMeansInformation(paymentMeansInformation); } zpp.getSender().addDebitDetails(d); + bankDetails.forEach(bankDetail -> zpp.getRecipient().addBankDetails(bankDetail)); + } else { + bankDetails.forEach(bankDetail -> zpp.getSender().addBankDetails(bankDetail)); } - bankDetails.forEach(bankDetail -> zpp.getSender().addBankDetails(bankDetail)); if (payeeNodes.getLength() > 0) { zpp.setPayee(new TradeParty(payeeNodes)); @@ -845,15 +858,14 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx Node currentItemNode = nodes.item(i); ReferencedDocument doc = ReferencedDocument.fromNode(currentItemNode); - if (doc != null - && (!Objects.equals(zpp.getInvoiceReferencedDocumentID(), doc.getIssuerAssignedID()) - || !Objects.equals(zpp.getInvoiceReferencedIssueDate(), doc.getFormattedIssueDateTime()))) - { + if (doc != null + && (!Objects.equals(zpp.getInvoiceReferencedDocumentID(), doc.getIssuerAssignedID()) + || !Objects.equals(zpp.getInvoiceReferencedIssueDate(), doc.getFormattedIssueDateTime()))) { zpp.addInvoiceReferencedDocument(doc); } } } - + zpp.setOwnOrganisationName(extractString("//*[local-name()=\"SellerTradeParty\"]/*[local-name()=\"Name\"]|//*[local-name()=\"AccountingSupplierParty\"]/*[local-name()=\"Party\"]/*[local-name()=\"PartyName\"]").trim()); String rounding = extractString("//*[local-name()=\"SpecifiedTradeSettlementHeaderMonetarySummation\"]/*[local-name()=\"RoundingAmount\"]|//*[local-name()=\"LegalMonetaryTotal\"]/*[local-name()=\"Party\"]/*[local-name()=\"PayableRoundingAmount\"]"); @@ -881,7 +893,6 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx Item it = new Item(currentItemNode.getChildNodes(), recalcPrice); zpp.addItem(it); - xpr = xpath.compile(".//*[local-name()=\"SpecifiedTradeAllowanceCharge\"]"); NodeList chargeNodes = (NodeList) xpr.evaluate(currentItemNode, XPathConstants.NODESET); for (int k = 0; k < chargeNodes.getLength(); k++) { @@ -899,7 +910,7 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx xpr = xpath.compile("//*[local-name()=\"AttachmentBinaryObject\"]|//*[local-name()=\"EmbeddedDocumentBinaryObject\"]"); NodeList attachmentNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); for (int i = 0; i < attachmentNodes.getLength(); i++) { - FileAttachment fa = new FileAttachment(attachmentNodes.item(i).getAttributes().getNamedItem("filename").getNodeValue(), attachmentNodes.item(i).getAttributes().getNamedItem("mimeCode").getNodeValue(), "Data", Base64.getDecoder().decode(XMLTools.trimOrNull(attachmentNodes.item(i)))); + FileAttachment fa = new FileAttachment(attachmentNodes.item(i).getAttributes().getNamedItem("filename").getNodeValue(), attachmentNodes.item(i).getAttributes().getNamedItem("mimeCode").getNodeValue(), "Data", Base64.getMimeDecoder().decode(XMLTools.trimOrNull(attachmentNodes.item(i)))); zpp.embedFileInXML(fa); // filename = "Aufmass.png" mimeCode = "image/png" //EmbeddedDocumentBinaryObject cbc:EmbeddedDocumentBinaryObject mimeCode="image/png" filename="Aufmass.png" @@ -921,35 +932,68 @@ public Invoice extractInto(Invoice zpp) throws XPathExpressionException, ParseEx } } - if (zpp.getZFItems() != null && zpp.getZFItems().length > 0) { - TransactionCalculator tc = new TransactionCalculator(zpp); - String calculatedPayableTotal = tc.getDuePayable().toPlainString(); - EStandard whichType; - try { - whichType = getStandard(); - } catch (Exception e) { - throw new StructureException("Could not find out if it's an invoice, order, or delivery advice", 0); + xpr = xpath.compile("//*[local-name()=\"ApplicableHeaderTradeSettlement\"]/*[local-name()=\"SpecifiedLogisticsServiceCharge\"]");// UBL unknown + chargeNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); + for (int i = 0; i < chargeNodes.getLength(); i++) { + NodeList chargeNodeChilds = chargeNodes.item(i).getChildNodes(); + String chargeAmount = null; + String taxPercent = null; + for (int chargeChildIndex = 0; chargeChildIndex < chargeNodeChilds.getLength(); chargeChildIndex++) { + String chargeChildName = chargeNodeChilds.item(chargeChildIndex).getLocalName(); + if (chargeChildName != null) { + if (chargeChildName.equals("AppliedAmount")) { + chargeAmount = XMLTools.trimOrNull(chargeNodeChilds.item(chargeChildIndex)); + } else if (chargeChildName.equals("AppliedTradeTax")) { + NodeList taxChilds = chargeNodeChilds.item(chargeChildIndex).getChildNodes(); + for (int taxChildIndex = 0; taxChildIndex < taxChilds.getLength(); taxChildIndex++) { + String taxItemName = taxChilds.item(taxChildIndex).getLocalName(); + if ((taxItemName != null) && (taxItemName.equals("RateApplicablePercent"))) { + taxPercent = XMLTools.trimOrNull(taxChilds.item(taxChildIndex)); + } + } + } + } + //appliedAmount + //AppliedTradeTax + } + if (chargeAmount != null) { + Charge c = new Charge(new BigDecimal(chargeAmount)); + if (taxPercent != null) { + c.setTaxPercent(new BigDecimal(taxPercent)); + } + zpp.addCharge(c); } + } - if (whichType != EStandard.despatchadvice && !ignoreCalculationErrors) { - // Check calculation if document type allows it and calculation errors should not be ignored - - String payableTotalFromXml = XMLTools.nDigitFormat(Objects.requireNonNullElse(duePayableAmount, expectedGrandTotal), 2); - if (!calculatedPayableTotal.equals(payableTotalFromXml)) { - String moreDetails = ""; - try { - moreDetails = " with tax basis " + tc.getTaxBasis() + " and with positions " + tc.getTotal() + " = " - + Stream.of(tc.trans.getZFItems()) - .map(item -> new LineCalculator(item).getItemTotalNetAmount().toPlainString()) - .collect(Collectors.joining(" + ")); - } catch (Exception ignored) { - } - throw new ArithmetricException("Payable total in XML is " + payableTotalFromXml + ", but calculated total is " + calculatedPayableTotal + moreDetails); + + TransactionCalculator tc = new TransactionCalculator(zpp); + String calculatedPayableTotal = tc.getDuePayable().toPlainString(); + EStandard whichType; + try { + whichType = getStandard(); + } catch (Exception e) { + throw new StructureException("Could not find out if it's an invoice, order, or delivery advice", 0); + } + + if (whichType != EStandard.despatchadvice && !ignoreCalculationErrors) { + // Check calculation if document type allows it and calculation errors should not be ignored + + String payableTotalFromXml = XMLTools.nDigitFormat(Objects.requireNonNullElse(duePayableAmount, expectedGrandTotal), 2); + if (!calculatedPayableTotal.equals(payableTotalFromXml)) { + String moreDetails = ""; + try { + moreDetails = " with tax basis " + tc.getTaxBasis() + " and with positions " + tc.getTotal() + " = " + + Stream.of(tc.trans.getZFItems()) + .map(item -> new LineCalculator(item).getItemTotalNetAmount().toPlainString()) + .collect(Collectors.joining(" + ")); + } catch (Exception ignored) { } + throw new ArithmetricException("Payable total in XML is " + payableTotalFromXml + ", but calculated total is " + calculatedPayableTotal + moreDetails); } } } return zpp; + } /** @@ -1060,7 +1104,7 @@ public EStandard getStandard() throws Exception { } else if (rootNode.equals("Invoice")) { return EStandard.ubl; } else if (rootNode.equals("CreditNote")) { - return EStandard.ubl; + return EStandard.ubl_creditnote; } else if (rootNode.equals("CrossIndustryInvoice")) { return EStandard.facturx; } else if (rootNode.equals("SCRDMCCBDACIDAMessageStructure")) { @@ -1105,11 +1149,17 @@ public String getUTF8() { * * @return the file attachments embedded in XML (using base64) decoded as byte array, * for PDF embedded files in FX use getFileAttachmentsPDF() + * may return empty array * @deprecated use invoice.getAdditionalReferencedDocuments */ @Deprecated public List getFileAttachmentsXML() { - return new ArrayList<>(Arrays.asList(importedInvoice.getAdditionalReferencedDocuments())); + if (importedInvoice.getAdditionalReferencedDocuments()!=null) { + return new ArrayList<>(Arrays.asList(importedInvoice.getAdditionalReferencedDocuments())); + } else { + return new ArrayList<>(); + } + } /*** diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDVisualizer.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDVisualizer.java index 9b5c1e735..8795b478d 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDVisualizer.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDVisualizer.java @@ -45,6 +45,9 @@ import java.io.*; import java.nio.charset.StandardCharsets; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Supplier; public class ZUGFeRDVisualizer { @@ -254,16 +257,61 @@ public void toPDF(String xmlFilename, String pdfFilename) { // the writing part File XMLinputFile = new File(xmlFilename); - String result = null; + String fopInput = null; /* remove file endings so that tests can also pass after checking out from git with arbitrary options (which may include CSRF changes) */ try { - result = this.toFOP(XMLinputFile.getAbsolutePath()); + fopInput = this.toFOP(XMLinputFile.getAbsolutePath()); } catch (TransformerException | IOException e) { LOGGER.error("Failed to apply FOP", e); } + + toPDFfromFOP(fopInput, () -> { + try { + return new FileOutputStream(pdfFilename); + } catch (FileNotFoundException e) { + LOGGER.error("Failed to create PDF", e); + } + return null; + }, (OutputStream out) -> {}); + } + + public byte[] toPDF(String xmlContent) { + + String fopInput = null; + + /* remove file endings so that tests can also pass after checking + out from git with arbitrary options (which may include CSRF changes) + */ + try { + ByteArrayInputStream fis = new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8)); + EStandard theStandard = findOutStandardFromRootNode(fis); + fis = new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8));//rewind :-( + + fopInput = toFOP(fis, theStandard); + } catch (TransformerException | IOException e) { + LOGGER.error("Failed to apply FOP", e); + } + + AtomicReference byteHolder = new AtomicReference<>(); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + toPDFfromFOP(fopInput, () -> new BufferedOutputStream(os), (OutputStream out) -> { + + try { + out.flush(); + } catch (IOException e) { + LOGGER.error("Failed to create PDF", e); + } + byteHolder.set(os.toByteArray()); + }); + + return byteHolder.get(); + } + + private void toPDFfromFOP(String fopInput, Supplier outputStreamDelegate, Consumer consumerDelegate) { + DefaultConfigurationBuilder cfgBuilder = new DefaultConfigurationBuilder(); Configuration cfg = null; @@ -291,7 +339,7 @@ out from git with arbitrary options (which may include CSRF changes) // Step 2: Set up output stream. // Note: Using BufferedOutputStream for performance reasons (helpful with FileOutputStreams). - try (OutputStream out = new BufferedOutputStream(new FileOutputStream(pdfFilename))) { + try (OutputStream out = new BufferedOutputStream(outputStreamDelegate.get())) { // Step 3: Construct fop with desired output format Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, userAgent, out); @@ -302,13 +350,15 @@ out from git with arbitrary options (which may include CSRF changes) // Step 5: Setup input and output for XSLT transformation // Setup input stream - Source src = new StreamSource(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8))); + Source src = new StreamSource(new ByteArrayInputStream(fopInput.getBytes(StandardCharsets.UTF_8))); // Resulting SAX events (the generated FO) must be piped through to FOP Result res = new SAXResult(fop.getDefaultHandler()); // Step 6: Start XSLT transformation and FOP processing transformer.transform(src, res); + + consumerDelegate.accept(out); } catch (FOPException | IOException | TransformerException e) { LOGGER.error("Failed to create PDF", e); diff --git a/library/src/main/resources/stylesheets/result-pdf.xsl b/library/src/main/resources/stylesheets/result-pdf.xsl index 0dfa7c952..f3e2e0d40 100644 --- a/library/src/main/resources/stylesheets/result-pdf.xsl +++ b/library/src/main/resources/stylesheets/result-pdf.xsl @@ -28,6 +28,14 @@ red + + + Das XML ist valide. + + + Das XML ist nicht valide. + + green @@ -37,18 +45,26 @@ - + Das ZUGFeRD-PDF ist valide. - + Das ZUGFeRD-PDF ist nicht valide. + + + green + + + red + + - + Es wird empfohlen, das Dokument anzunehmen und es weiterzuverarbeiten. - + Es wird empfohlen, das Dokument zurückzuweisen. @@ -118,6 +134,7 @@ + + + + + + + + + true + + + urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended + + + + R87654321012345 + WARENRECHNUNG + 380 + + 20180806 + + + ST3 + Es bestehen Rabatt- oder Bonusvereinbarungen. + AAK + + + EEV + Der Verkäufer bleibt Eigentümer der Waren bis zu vollständigen Erfüllung der Kaufpreisforderung. + AAJ + + + MUSTERLIEFERANT GMBH +BAHNHOFSTRASSE 99 +99199 MUSTERHAUSEN +Geschäftsführung: +Max Mustermann +USt-IdNr: DE123456789 +Telefon: +49 932 431 0 +www.musterlieferant.de +HRB Nr. 372876 +Amtsgericht Musterstadt +GLN 4304171000002 +WEEE-Reg-Nr.: DE87654321 + + REG + + + Leergutwert: 46,50 + + + Wichtige Information: Bei Bestellungen bis zum 19.12. ist die Auslieferung bis spätestens 23.12. garantiert. + + + + + + 1 + + + 4123456000014 + ZS997 + Zitronensäure 100ml + + Verpackungsart + BO + + + + + 1.0000 + + + 1.0000 + + + + 100.0000 + 4.0000 + + + + VAT + S + 19.00 + + + 100.00 + + + + + + 2 + + + 4123456000021 + GZ250 + Gelierzucker Extra 250g + + + + 1.5000 + + + false + + 0.0300 + Artikelrabatt 1 + + + + false + + 0.0200 + Artikelrabatt 2 + + + + 1.4500 + + + + 50.0000 + 1.0000 + + + + VAT + S + 7.00 + + + 72.50 + + + + + + 3 + + + 4123456000021 + GZ250 + Gelierzucker Extra 250g + Artikel wie vereinbart ohne Berechnung + + + + 0.0000 + + + 0.0000 + + + + 10.0000 + 1.0000 + + + + VAT + S + 7.00 + + + 0.00 + + + + + + 4 + + + 4100130013294 + 2031 + + Bierbrau Pils 20/0500 + EAN-VKE: 4100130913297 + + Verpackung + Kiste + + + + + 12.0000 + + + 12.0000 + + + + 15.0000 + 20.0000 + + + + VAT + S + 19.00 + + + 180.00 + + + + + + 5 + + + 2001015001325 + 1805 + + Leergutpfand 20 x 0,5l + + Verpackung + unverpackt + + + + + 3.1000 + + + 3.1000 + + + + 15.0000 + 1.0000 + + + + VAT + S + 19.00 + + + 46.50 + + + + + + 6 + + + 4123456000038 + MP107 + Mischpalette Joghurt Karton 3 x 20 + + Verpackung + Karton + + + 4123456001035 + JOG103 + Erdbeer 20 x 150g Becher + 20.0000 + + + 4123456002032 + JOG203 + Banane 20 x 150g Becher + 20.0000 + + + 4123456003039 + JOG303 + Schoko 20 x 150g Becher + 20.0000 + + + + + 30.0000 + + + false + + 0.9000 + Artikelrabatt 1 + + + + 29.1000 + + + + 2.0000 + 1.0000 + + + + VAT + S + 7.00 + + + 58.20 + + + + + + 549910 + 4333741000005 + MUSTERLIEFERANT GMBH + + + +49 932 431 500 + + + max.mustermann@musterlieferant.de + + + + 99199 + BAHNHOFSTRASSE 99 + MUSTERHAUSEN + DE + + + DE123456789 + + + + 009420 + 4304171000002 + MUSTER-KUNDE GMBH + + 40235 + KUNDENWEG 88 + DUESSELDORF + DE + + + + B123456789 + + + A456123 + 130 + + + + + 4304171088093 + MUSTER-MARKT + + 8211 + + + 31157 + HAUPTSTRASSE 44 + SARSTEDT + DE + + + + + 20180805 + + + + L87654321012345 + + + + EUR + + 009420 + 4304171000002 + MUSTER-KUNDE GMBH + + 40235 + KUNDENWEG 88 + DUESSELDORF + DE + + + + 61.07 + VAT + 321.40 + 326.50 + -5.10 + S + 19.00 + + + 8.93 + VAT + 127.59 + 130.70 + -3.11 + S + 7.00 + + + + false + + 2.00 + 280.00 + 5.60 + Rechnungsrabatt 1 + + VAT + S + 19.00 + + + + + false + + 2.00 + 130.70 + 2.61 + Rechnungsrabatt 1 + + VAT + S + 7.00 + + + + + false + + 280.00 + 2.50 + Rechnungsrabatt 2 + + VAT + S + 19.00 + + + + + false + + 130.70 + 0.50 + Rechnungsrabatt 2 + + VAT + S + 7.00 + + + + Transportkosten + 3.00 + + VAT + S + 19.00 + + + + Bei Zahlung innerhalb 14 Tagen gewähren wir 2,0% Skonto. + + 14 + 2.00 + + + + 457.20 + 3.00 + 11.21 + 448.99 + 70.00 + 518.99 + 0.00 + 518.99 + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9dade18f1..7346b012d 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.mustangproject core - 2.15.3-SNAPSHOT pom + 2.16.2-SNAPSHOT pom Mustang diff --git a/validator/pom.xml b/validator/pom.xml index 67d7ca946..6c94fd9ee 100644 --- a/validator/pom.xml +++ b/validator/pom.xml @@ -3,7 +3,7 @@ org.mustangproject core - 2.15.3-SNAPSHOT + 2.16.2-SNAPSHOT 4.0.0 org.mustangproject @@ -11,7 +11,7 @@ Library to validate e-invoices (ZUGFeRD, Factur-X and Xrechnung) jar - 2.15.3-SNAPSHOT + 2.16.2-SNAPSHOT @@ -38,7 +38,7 @@ ${project.groupId} library - 2.15.3-SNAPSHOT + 2.16.2-SNAPSHOT org.dom4j @@ -73,7 +73,7 @@ org.verapdf validation-model-jakarta - 1.26.1 + 1.26.2 diff --git a/validator/src/main/java/org/mustangproject/validator/ValidationContext.java b/validator/src/main/java/org/mustangproject/validator/ValidationContext.java index 73b95f2e7..ce3cde48a 100644 --- a/validator/src/main/java/org/mustangproject/validator/ValidationContext.java +++ b/validator/src/main/java/org/mustangproject/validator/ValidationContext.java @@ -70,7 +70,7 @@ public ValidationContext setGeneration(String version) { } public ValidationContext setProfile(String profile) { - this.profile = profile; + this.profile = profile.trim(); return this; }