diff --git a/library/src/main/java/org/mustangproject/ClassCode.java b/library/src/main/java/org/mustangproject/ClassCode.java
new file mode 100644
index 00000000..b47cbdf6
--- /dev/null
+++ b/library/src/main/java/org/mustangproject/ClassCode.java
@@ -0,0 +1,103 @@
+/**
+ * *********************************************************************
+ *
+ * Copyright (c) 2024 Jan N. Klug
+ *
+ * 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;
+
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+/**
+ * A schemed classification for products. The scheme can be anything defined in UNTDID 7143.
+ */
+public class ClassCode {
+ private final String listID;
+ private final String code;
+ private String listVersionID;
+
+ /**
+ * A UNTDID 7143 schemed classification code
+ *
+ * @param listID the scheme from UNTDID 7143
+ * @param code the classification code
+ * @param listVersionID an (optional) version of the scheme
+ */
+ public ClassCode(String listID, String code, String listVersionID) {
+ this.listID = listID;
+ this.code = code;
+ this.listVersionID = listVersionID;
+ }
+
+ /**
+ * A UNTDID 7143 schemed classification code
+ *
+ * @param listID the scheme from UNTDID 7143
+ * @param code the classification code
+ */
+ public ClassCode(String listID, String code) {
+ this(listID, code, null);
+ }
+
+ /**
+ * Set the version for the scheme returned by {@link #getListID()}
+ */
+ public void setListVersionID(String listVersionID) {
+ this.listVersionID = listVersionID;
+ }
+
+ /**
+ * Get the scheme (according to UNTDID 7143) that describes the value returned by {@link #getCode()},
+ * potentially versioned by {@link #getListVersionID()}
+ *
+ * @return the scheme
+ */
+ public String getListID() {
+ return listID;
+ }
+
+ /**
+ * Get the code that (following the scheme returned by {@link #getListID()}) describes the product
+ *
+ * @return the classification code itself
+ */
+ public String getCode() {
+ return code;
+ }
+
+ /**
+ * Get the (optional) version for the scheme returned by {@link #getListID()}
+ *
+ * @return the version or {@code null} if not set
+ */
+ public String getListVersionID() {
+ return listVersionID;
+ }
+
+ public static ClassCode fromNode(Node node) {
+ NamedNodeMap attrs = node.getAttributes();
+ if (attrs != null && attrs.getNamedItem("listID") != null) {
+ ClassCode classCode = new ClassCode(attrs.getNamedItem("listID").getNodeValue(), node.getTextContent());
+ if (attrs.getNamedItem("listVersionID") != null) {
+ classCode.setListVersionID(attrs.getNamedItem("listVersionID").getNodeValue());
+ }
+ return classCode;
+ }
+ return null;
+ }
+}
diff --git a/library/src/main/java/org/mustangproject/DesignatedProductClassification.java b/library/src/main/java/org/mustangproject/DesignatedProductClassification.java
new file mode 100644
index 00000000..ac74bf36
--- /dev/null
+++ b/library/src/main/java/org/mustangproject/DesignatedProductClassification.java
@@ -0,0 +1,71 @@
+/**
+ * *********************************************************************
+ *
+ * Copyright (c) 2024 Jan N. Klug
+ *
+ * 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;
+
+import org.mustangproject.ZUGFeRD.IDesignatedProductClassification;
+
+/**
+ * An implementation of {@link IDesignatedProductClassification} for describing a {@link org.mustangproject.Product}
+ *
+ */
+public class DesignatedProductClassification implements IDesignatedProductClassification {
+ private final ClassCode classCode;
+ private String className;
+
+ /**
+ * A schemed product descriptor
+ *
+ * @param classCode an UNTDID 7143 schemed class code
+ * @param className a verbal description of the class code
+ */
+ public DesignatedProductClassification(ClassCode classCode, String className) {
+ this.classCode = classCode;
+ this.className = className;
+ }
+
+ /**
+ * A schemed product descriptor
+ *
+ * @param classCode an UNTDID 7143 schemed class code
+ */
+ public DesignatedProductClassification(ClassCode classCode) {
+ this(classCode, null);
+ }
+
+ @Override
+ public ClassCode getClassCode() {
+ return classCode;
+ }
+
+ @Override
+ public String getClassName() {
+ return className;
+ }
+
+ /**
+ * Set the human-readable name of the class code
+ *
+ * @param className the name of the class code (can be {@code null})
+ */
+ public void setClassName(String className) {
+ this.className = className;
+ }
+}
diff --git a/library/src/main/java/org/mustangproject/Item.java b/library/src/main/java/org/mustangproject/Item.java
index d4d417c6..1057f617 100644
--- a/library/src/main/java/org/mustangproject/Item.java
+++ b/library/src/main/java/org/mustangproject/Item.java
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.mustangproject.ZUGFeRD.IReferencedDocument;
import org.mustangproject.ZUGFeRD.IZUGFeRDAllowanceCharge;
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem;
+import org.mustangproject.util.NodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@@ -18,17 +19,22 @@ import java.util.Date;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Item implements IZUGFeRDExportableItem {
- protected BigDecimal price, quantity, tax, grossPrice, lineTotalAmount;
+ protected BigDecimal price = BigDecimal.ZERO;
+ protected BigDecimal quantity;
+ protected BigDecimal tax;
+ protected BigDecimal grossPrice;
+ protected BigDecimal lineTotalAmount;
protected BigDecimal basisQuantity = BigDecimal.ONE;
- protected Date detailedDeliveryPeriodFrom = null, detailedDeliveryPeriodTo = null;
+ protected Date detailedDeliveryPeriodFrom = null;
+ protected Date detailedDeliveryPeriodTo = null;
protected String id;
protected String referencedLineID = null;
protected Product product;
protected ArrayList notes = null;
protected ArrayList referencedDocuments = null;
protected ArrayList additionalReference = null;
- protected ArrayList Allowances = new ArrayList<>(),
- Charges = new ArrayList<>();
+ protected ArrayList Allowances = new ArrayList<>();
+ protected ArrayList Charges = new ArrayList<>();
/***
* default constructor
@@ -42,7 +48,6 @@ public class Item implements IZUGFeRDExportableItem {
this.product = product;
}
-
/***
* empty constructor
* do not use, but might be used e.g. by jackson
@@ -51,269 +56,77 @@ public class Item implements IZUGFeRDExportableItem {
}
public Item(NodeList itemChilds, boolean recalcPrice) {
- String price = "0";
- String basisQuantity = "1";
- String name = "";
- String sellerAssignedID = null;
- String description = "";
- SchemedID gid = null;
- String quantity = "0";
- String vatPercent = null;
- String lineTotal = "0";
- String unitCode = "0";
- String referencedLineID = null;
+ NodeMap itemMap = new NodeMap(itemChilds);
- ArrayList rdocs = null;
- ArrayList addRefs = null;
+ itemMap.getAsNodeMap("Item").ifPresent(icnm -> {
+ // ubl
+ //we need: name description unitcode
+ //and we additionally have vat%
+ setProduct(new Product());
+ icnm.getAsString("Name").ifPresent(product::setName);
+ icnm.getAsNodeMap("ClassifiedTaxCategory").flatMap(m -> m.getAsString("Percent"))
+ .ifPresent(vatPercent -> product.setVATPercent(new BigDecimal(vatPercent.trim())));
+ });
- // nodes.item(i).getTextContent())) {
+ itemMap.getAsNodeMap("Price").ifPresent(icnm -> {
+ // ubl
+ // PriceAmount with currencyID and BaseQuantity with unitCode
+ icnm.getAsString("PriceAmount").ifPresent(amount -> setPrice(new BigDecimal(amount.trim())));
+ icnm.getAsString("BaseQuantity").ifPresent(baseQuantity -> setQuantity(new BigDecimal(baseQuantity.trim())));
+ });
- for (int itemChildIndex = 0; itemChildIndex < itemChilds.getLength(); itemChildIndex++) {
- String lineTrade = itemChilds.item(itemChildIndex).getLocalName();
- if ((lineTrade != null) && (lineTrade.equals("Item"))) {
- // ubl
- //we need: name description unitcode
- //and we additionally have vat%
- NodeList UBLitemChilds = itemChilds.item(itemChildIndex).getChildNodes();
- for (Node currentUBLItemChildNode : XMLTools.asList(UBLitemChilds)) {
+ itemMap.getNode("InvoicedQuantity").ifPresent(icn -> {
+ // ubl
+ setQuantity(new BigDecimal(icn.getTextContent().trim()));
+ product.setUnit(icn.getAttributes().getNamedItem("unitCode").getNodeValue());
+ });
- if ((currentUBLItemChildNode.getLocalName() != null) && (currentUBLItemChildNode.getLocalName().equals("Name"))) {
- name = currentUBLItemChildNode.getTextContent();
- }
- if ((currentUBLItemChildNode.getLocalName() != null) && (currentUBLItemChildNode.getLocalName().equals("ClassifiedTaxCategory"))) {
- for (Node currentUBLTaxChildNode : XMLTools.asList(currentUBLItemChildNode.getChildNodes())) {
- if ((currentUBLTaxChildNode.getLocalName() != null) && (currentUBLTaxChildNode.getLocalName().equals("Percent"))) {
- vatPercent = currentUBLTaxChildNode.getTextContent();
- }
- }
+ itemMap.getAsNodeMap("SpecifiedLineTradeAgreement", "SpecifiedSupplyChainTradeAgreement").ifPresent(icnm -> {
+ icnm.getAllNodes("AdditionalReferencedDocument").map(ReferencedDocument::fromNode)
+ .forEach(this::addReferencedDocument);
+
+ icnm.getAsNodeMap("BuyerOrderReferencedDocument")
+ .flatMap(bordNodes -> bordNodes.getAsString("LineID"))
+ .ifPresent(this::addReferencedLineID);
+
+ icnm.getAsNodeMap("NetPriceProductTradePrice").ifPresent(npptpNodes -> {
+ npptpNodes.getAsString("ChargeAmount")
+ .ifPresent(amount -> setPrice(new BigDecimal(amount.trim())));
+ npptpNodes.getAsString("BasisQuantity")
+ .ifPresent(bq -> setBasisQuantity(new BigDecimal(bq.trim())));
+ });
+ });
+
+ itemMap.getNode("SpecifiedTradeProduct").map(Product::new).ifPresent(this::setProduct);
+
+ // RequestedQuantity is for Order-X, BilledQuantity for FX and ZF
+ itemMap.getAsNodeMap("SpecifiedLineTradeDelivery", "SpecifiedSupplyChainTradeDelivery")
+ .flatMap(icnm -> icnm.getNode("BilledQuantity", "RequestedQuantity", "DespatchedQuantity"))
+ .ifPresent(bq -> {
+ setQuantity(new BigDecimal(bq.getTextContent().trim()));
+ if (bq.hasAttributes()) {
+ Node unitAttr = bq.getAttributes().getNamedItem("unitCode");
+ if (unitAttr != null) {
+ product.setUnit(unitAttr.getNodeValue());
}
}
+ });
+
+ itemMap.getAsNodeMap("SpecifiedLineTradeSettlement", "SpecifiedSupplyChainTradeSettlement").ifPresent(icnm -> {
+ icnm.getAsNodeMap("ApplicableTradeTax")
+ .map(cnm -> cnm.getAsStringOrNull("RateApplicablePercent", "ApplicablePercent"))
+ .ifPresent(vatPercent -> product.setVATPercent(new BigDecimal(vatPercent.trim())));
+
+ if (recalcPrice && !BigDecimal.ZERO.equals(quantity)) {
+ icnm.getAsNodeMap("SpecifiedTradeSettlementLineMonetarySummation")
+ .map(cnm -> cnm.getAsStringOrNull("LineTotalAmount"))
+ .ifPresent(lineTotal -> setPrice(new BigDecimal(lineTotal.trim()).divide(quantity, 4, RoundingMode.HALF_UP)));
}
- if ((lineTrade != null) && (lineTrade.equals("Price"))) {
- // ubl
- // PriceAmount with currencyID and BaseQuantity with unitCode
- NodeList UBLpriceChilds = itemChilds.item(itemChildIndex).getChildNodes();
- for (Node currentUBLPriceChildNode : XMLTools.asList(UBLpriceChilds)) {
- if ((currentUBLPriceChildNode.getLocalName() != null) && (currentUBLPriceChildNode.getLocalName().equals("PriceAmount"))) {
- price = currentUBLPriceChildNode.getTextContent();
- }
- if ((currentUBLPriceChildNode.getLocalName() != null) && (currentUBLPriceChildNode.getLocalName().equals("BaseQuantity"))) {
- basisQuantity = currentUBLPriceChildNode.getTextContent();
- }
- }
-
- }
- if ((lineTrade != null) && (lineTrade.equals("InvoicedQuantity"))) {
- // ubl
- quantity = itemChilds.item(itemChildIndex).getTextContent();
- unitCode = itemChilds.item(itemChildIndex).getAttributes()
- .getNamedItem("unitCode").getNodeValue();
- }
- if ((lineTrade != null) && (lineTrade.equals("SpecifiedLineTradeAgreement")
- || lineTrade.equals("SpecifiedSupplyChainTradeAgreement"))) {
- NodeList tradeLineChilds = itemChilds.item(itemChildIndex).getChildNodes();
- for (int tradeLineChildIndex = 0; tradeLineChildIndex < tradeLineChilds
- .getLength(); tradeLineChildIndex++) {
-
- if ((tradeLineChilds.item(tradeLineChildIndex).getLocalName() != null) && tradeLineChilds
- .item(tradeLineChildIndex).getLocalName().equals("AdditionalReferencedDocument")) {
- String IssuerAssignedID = "";
- String TypeCode = "";
- String ReferenceTypeCode = "";
-
- NodeList refDocChilds = tradeLineChilds.item(tradeLineChildIndex).getChildNodes();
- for (int refDocIndex = 0; refDocIndex < refDocChilds.getLength(); refDocIndex++) {
- String localName = refDocChilds.item(refDocIndex).getLocalName();
- if ((localName != null) && (localName.equals("IssuerAssignedID"))) {
- IssuerAssignedID = refDocChilds.item(refDocIndex).getTextContent();
- }
- if ((localName != null) && (localName.equals("TypeCode"))) {
- TypeCode = refDocChilds.item(refDocIndex).getTextContent();
- }
- if ((localName != null) && (localName.equals("ReferenceTypeCode"))) {
- ReferenceTypeCode = refDocChilds.item(refDocIndex).getTextContent();
- }
- }
-
- ReferencedDocument rd = new ReferencedDocument(IssuerAssignedID, TypeCode,
- ReferenceTypeCode);
- if (rdocs == null) {
- rdocs = new ArrayList<>();
- }
- rdocs.add(rd);
-
- }
-
- if ((tradeLineChilds.item(tradeLineChildIndex).getLocalName() != null) && tradeLineChilds.item(tradeLineChildIndex).getLocalName().equals("BuyerOrderReferencedDocument")) {
- NodeList docChilds = tradeLineChilds.item(tradeLineChildIndex).getChildNodes();
- for (int docIndex = 0; docIndex < docChilds.getLength(); docIndex++) {
- String localName = docChilds.item(docIndex).getLocalName();
- if ((localName != null) && (localName.equals("LineID"))) {
- referencedLineID = docChilds.item(docIndex).getTextContent();
- }
- }
- }
-
- if ((tradeLineChilds.item(tradeLineChildIndex).getLocalName() != null) && tradeLineChilds
- .item(tradeLineChildIndex).getLocalName().equals("NetPriceProductTradePrice")) {
- NodeList netChilds = tradeLineChilds.item(tradeLineChildIndex).getChildNodes();
- for (int netIndex = 0; netIndex < netChilds.getLength(); netIndex++) {
- if ((netChilds.item(netIndex).getLocalName() != null)
- && (netChilds.item(netIndex).getLocalName().equals("ChargeAmount"))) {
- price = netChilds.item(netIndex).getTextContent();// ChargeAmount
-
- }
- if ((netChilds.item(netIndex).getLocalName() != null)
- && ((netChilds.item(netIndex).getLocalName().equals("BasisQuantity")) || (netChilds.item(netIndex).getLocalName().equals("InvoicedQuantity")))) {
- basisQuantity = netChilds.item(netIndex).getTextContent();// ChargeAmount
-
- }
- }
- }
- }
- }
- if ((lineTrade != null) && (lineTrade.equals("SpecifiedLineTradeDelivery")
- || lineTrade.equals("SpecifiedSupplyChainTradeDelivery"))) {
- NodeList tradeLineChilds = itemChilds.item(itemChildIndex).getChildNodes();
- for (int tradeLineChildIndex = 0; tradeLineChildIndex < tradeLineChilds
- .getLength(); tradeLineChildIndex++) {
- String tradeName = tradeLineChilds.item(tradeLineChildIndex).getLocalName();
- if ((tradeName != null)
- && (tradeName.equals("BilledQuantity") || tradeName.equals("RequestedQuantity")
- || tradeName.equals("DespatchedQuantity"))) {
- // RequestedQuantity is for Order-X, BilledQuantity for FX and ZF
- quantity = tradeLineChilds.item(tradeLineChildIndex).getTextContent();
- unitCode = tradeLineChilds.item(tradeLineChildIndex).getAttributes()
- .getNamedItem("unitCode").getNodeValue();
- }
- }
- }
- if ((lineTrade != null) && (lineTrade.equals("SpecifiedTradeProduct"))) {
- NodeList tradeProductChilds = itemChilds.item(itemChildIndex).getChildNodes();
- for (int tradeProductChildIndex = 0; tradeProductChildIndex < tradeProductChilds
- .getLength(); tradeProductChildIndex++) {
- if ((tradeProductChilds.item(tradeProductChildIndex).getLocalName() != null)
- && (tradeProductChilds.item(tradeProductChildIndex).getLocalName()
- .equals("Name"))) {
- name = tradeProductChilds.item(tradeProductChildIndex).getTextContent();
- }
- if ((tradeProductChilds.item(tradeProductChildIndex).getLocalName() != null)
- && (tradeProductChilds.item(tradeProductChildIndex).getLocalName()
- .equals("SellerAssignedID"))) {
- sellerAssignedID = tradeProductChilds.item(tradeProductChildIndex).getTextContent();
- }
- if ((tradeProductChilds.item(tradeProductChildIndex).getLocalName() != null)
- && (tradeProductChilds.item(tradeProductChildIndex).getLocalName()
- .equals("GlobalID"))) {
- if (tradeProductChilds.item(tradeProductChildIndex).getAttributes()
- .getNamedItem("schemeID") != null) {
- gid = new SchemedID()
- .setScheme(tradeProductChilds.item(tradeProductChildIndex).getAttributes()
- .getNamedItem("schemeID").getNodeValue())
- .setId(tradeProductChilds.item(tradeProductChildIndex).getTextContent());
- }
-
- }
- }
- }
- if ((lineTrade != null) && (lineTrade.equals("SpecifiedLineTradeSettlement")
- || lineTrade.equals("SpecifiedSupplyChainTradeSettlement"))) {
- NodeList tradeSettlementChilds = itemChilds.item(itemChildIndex).getChildNodes();
- for (int tradeSettlementChildIndex = 0; tradeSettlementChildIndex < tradeSettlementChilds
- .getLength(); tradeSettlementChildIndex++) {
-
- String tradeSettlementName = tradeSettlementChilds.item(tradeSettlementChildIndex)
- .getLocalName();
- if (tradeSettlementName != null) {
- if (tradeSettlementName.equals("ApplicableTradeTax")) {
- NodeList taxChilds = tradeSettlementChilds.item(tradeSettlementChildIndex)
- .getChildNodes();
- for (int taxChildIndex = 0; taxChildIndex < taxChilds
- .getLength(); taxChildIndex++) {
- String taxChildName = taxChilds.item(taxChildIndex).getLocalName();
- if ((taxChildName != null) && (taxChildName.equals("RateApplicablePercent")
- || taxChildName.equals("ApplicablePercent"))) {
- vatPercent = taxChilds.item(taxChildIndex).getTextContent();
- }
- }
- }
-
- if (tradeSettlementName.equals("SpecifiedTradeSettlementLineMonetarySummation")) {
- NodeList totalChilds = tradeSettlementChilds.item(tradeSettlementChildIndex)
- .getChildNodes();
- for (int totalChildIndex = 0; totalChildIndex < totalChilds
- .getLength(); totalChildIndex++) {
- if ((totalChilds.item(totalChildIndex).getLocalName() != null) && (totalChilds
- .item(totalChildIndex).getLocalName().equals("LineTotalAmount"))) {
- lineTotal = totalChilds.item(totalChildIndex).getTextContent();
- }
- }
- }
-
- if (tradeSettlementName.equals("AdditionalReferencedDocument")) {
- String IssuerAssignedID = "";
- String TypeCode = "";
- String ReferenceTypeCode = "";
-
- NodeList refDocChilds = tradeSettlementChilds.item(tradeSettlementChildIndex).getChildNodes();
- for (int refDocIndex = 0; refDocIndex < refDocChilds.getLength(); refDocIndex++) {
- String localName = refDocChilds.item(refDocIndex).getLocalName();
- if ((localName != null) && (localName.equals("IssuerAssignedID"))) {
- IssuerAssignedID = refDocChilds.item(refDocIndex).getTextContent();
- }
- if ((localName != null) && (localName.equals("TypeCode"))) {
- TypeCode = refDocChilds.item(refDocIndex).getTextContent();
- }
- if ((localName != null) && (localName.equals("ReferenceTypeCode"))) {
- ReferenceTypeCode = refDocChilds.item(refDocIndex).getTextContent();
- }
- }
-
- ReferencedDocument rd = new ReferencedDocument(IssuerAssignedID, TypeCode,
- ReferenceTypeCode);
- if (addRefs == null) {
- addRefs = new ArrayList<>();
- }
- addRefs.add(rd);
-
- }
-
- }
- }
- }
- }
- BigDecimal prc = new BigDecimal(price.trim());
- BigDecimal qty = new BigDecimal(quantity.trim());
- if ((recalcPrice) && (!qty.equals(BigDecimal.ZERO))) {
- prc = new BigDecimal(lineTotal.trim()).divide(qty, 4, RoundingMode.HALF_UP);
- }
- Product p = new Product(name, description, unitCode,
- vatPercent == null ? null : new BigDecimal(vatPercent.trim()));
- if (gid != null) {
- p.addGlobalID(gid);
- }
- if (sellerAssignedID != null) {
- p.setSellerAssignedID(sellerAssignedID);
- }
- setProduct(p);
- setPrice(prc);
- setQuantity(qty);
- setBasisQuantity(new BigDecimal(basisQuantity));
- if (rdocs != null) {
- for (ReferencedDocument rdoc : rdocs) {
- addReferencedDocument(rdoc);
- }
- }
- if (addRefs != null) {
- for (ReferencedDocument rdoc : addRefs) {
- addAdditionalReference(rdoc);
- }
- }
- addReferencedLineID( referencedLineID );
+ icnm.getAllNodes("AdditionalReferencedDocument").map(ReferencedDocument::fromNode).forEach(this::addAdditionalReference);
+ });
}
-
public Item addReferencedLineID(String s) {
referencedLineID = s;
return this;
@@ -544,6 +357,7 @@ public class Item implements IZUGFeRDExportableItem {
* this will be included in a BillingSpecifiedPeriod element
* @return the beginning of the delivery period
*/
+ @Override
public Date getDetailedDeliveryPeriodFrom() {
return detailedDeliveryPeriodFrom;
}
@@ -553,6 +367,7 @@ public class Item implements IZUGFeRDExportableItem {
* this will be included in a BillingSpecifiedPeriod element
* @return the end of the delivery period
*/
+ @Override
public Date getDetailedDeliveryPeriodTo() {
return detailedDeliveryPeriodTo;
}
diff --git a/library/src/main/java/org/mustangproject/Product.java b/library/src/main/java/org/mustangproject/Product.java
index df5e329f..1e92fbfd 100644
--- a/library/src/main/java/org/mustangproject/Product.java
+++ b/library/src/main/java/org/mustangproject/Product.java
@@ -1,10 +1,17 @@
package org.mustangproject;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import org.mustangproject.ZUGFeRD.IDesignatedProductClassification;
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableProduct;
+import org.mustangproject.util.NodeMap;
+import org.w3c.dom.Node;
import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
/***
* describes a product, good or service used in an invoice item line
@@ -18,7 +25,8 @@ public class Product implements IZUGFeRDExportableProduct {
protected boolean isIntraCommunitySupply = false;
protected SchemedID globalId = null;
protected String countryOfOrigin = null;
- protected HashMap attributes = null;
+ protected HashMap attributes = new HashMap<>();
+ protected List classifications = new ArrayList<>();
/***
* default constructor
@@ -34,6 +42,42 @@ public class Product implements IZUGFeRDExportableProduct {
this.VATPercent = VATPercent;
}
+ public Product(Node node) {
+ NodeMap nodeMap = new NodeMap(node);
+
+ nodeMap.getNode("GlobalID").ifPresent(idNode -> {
+ if (idNode.hasAttributes()
+ && idNode.getAttributes().getNamedItem("schemeID") != null) {
+ globalId = new SchemedID()
+ .setScheme(idNode.getAttributes().getNamedItem("schemeID").getNodeValue())
+ .setId(idNode.getTextContent());
+ }
+ });
+
+ nodeMap.getAsString("SellerAssignedID").ifPresent(this::setSellerAssignedID);
+ nodeMap.getAsString("BuyerAssignedID").ifPresent(this::setBuyerAssignedID);
+ nodeMap.getAsString("Name").ifPresent(this::setName);
+ nodeMap.getAsString("Description").ifPresent(this::setDescription);
+
+ nodeMap.getAsNodeMap("ApplicableProductCharacteristic").ifPresent(apcNodes -> {
+ String key = apcNodes.getAsStringOrNull("Description");
+ String value = apcNodes.getAsStringOrNull("Value");
+ if (key != null && value != null) {
+ if (attributes == null) {
+ attributes = new HashMap<>();
+ }
+ attributes.put(key, value);
+ }
+ });
+
+ nodeMap.getAsNodeMap("DesignatedProductClassification").ifPresent(dpcNodes -> {
+ String className = dpcNodes.getAsStringOrNull("ClassName");
+ dpcNodes.getNode("ClassCode").map(ClassCode::fromNode).ifPresent(classCode ->
+ classifications.add(new DesignatedProductClassification(classCode, className)));
+ });
+
+ nodeMap.getAsString("OriginTradeCounty").ifPresent(this::setCountryOfOrigin);
+ }
/***
* empty constructor
@@ -202,19 +246,57 @@ public class Product implements IZUGFeRDExportableProduct {
@Override
public HashMap getAttributes() {
- return this.attributes;
+ if (attributes.isEmpty()) {
+ return null;
+ } else {
+ return this.attributes;
+ }
}
- public Product setAttributes(HashMap attributes) {
- this.attributes = attributes;
+ public Product setAttributes(Map attributes) {
+ this.attributes.clear();
+ if (attributes != null) {
+ this.attributes.putAll(attributes);
+ }
return this;
}
public Product addAttribute(String name, String value ) {
- if ( this.attributes == null ) {
- this.attributes = new HashMap<>();
- }
this.attributes.put(name, value);
return this;
}
+
+ @Override
+ public IDesignatedProductClassification[] getClassifications() {
+ if (classifications.isEmpty()) {
+ return null;
+ } else {
+ return classifications.toArray(new IDesignatedProductClassification[0]);
+ }
+ }
+
+ /**
+ * Replace the current set of {@link IDesignatedProductClassification}s with a new set
+ *
+ * @param classifications the new set of classifications
+ * @return the modified object
+ */
+ public Product setClassifications(IDesignatedProductClassification[] classifications) {
+ this.classifications.clear();
+ if (classifications != null) {
+ this.classifications.addAll(Arrays.asList(classifications));
+ }
+ return this;
+ }
+
+ /**
+ * Add a {@link IDesignatedProductClassification} classification
+ *
+ * @param classification the classification
+ * @return the modified object
+ */
+ public Product addClassification(IDesignatedProductClassification classification) {
+ this.classifications.add(classification);
+ return this;
+ }
}
diff --git a/library/src/main/java/org/mustangproject/ReferencedDocument.java b/library/src/main/java/org/mustangproject/ReferencedDocument.java
index 423c1bc5..4d2151ca 100644
--- a/library/src/main/java/org/mustangproject/ReferencedDocument.java
+++ b/library/src/main/java/org/mustangproject/ReferencedDocument.java
@@ -1,6 +1,8 @@
package org.mustangproject;
import org.mustangproject.ZUGFeRD.IReferencedDocument;
+import org.mustangproject.util.NodeMap;
+import org.w3c.dom.Node;
public class ReferencedDocument implements IReferencedDocument {
@@ -58,4 +60,14 @@ public class ReferencedDocument implements IReferencedDocument {
public String getReferenceTypeCode() {
return referenceTypeCode;
}
+
+ public static ReferencedDocument fromNode(Node node) {
+ if (!node.hasChildNodes()) {
+ return null;
+ }
+ NodeMap nodes = new NodeMap(node);
+ return new ReferencedDocument(nodes.getAsStringOrNull("IssuerAssignedID"),
+ nodes.getAsStringOrNull("TypeCode"),
+ nodes.getAsStringOrNull("ReferenceTypeCode"));
+ }
}
diff --git a/library/src/main/java/org/mustangproject/SchemedID.java b/library/src/main/java/org/mustangproject/SchemedID.java
index d3071850..9df8a2d9 100644
--- a/library/src/main/java/org/mustangproject/SchemedID.java
+++ b/library/src/main/java/org/mustangproject/SchemedID.java
@@ -31,4 +31,9 @@ public class SchemedID {
setId(id);
}
+ @Override
+ public String toString() {
+ return "SchemedID{scheme='" + scheme + "', id='" + id + "'}";
+ }
+
}
diff --git a/library/src/main/java/org/mustangproject/XMLTools.java b/library/src/main/java/org/mustangproject/XMLTools.java
index af44d904..c882e573 100644
--- a/library/src/main/java/org/mustangproject/XMLTools.java
+++ b/library/src/main/java/org/mustangproject/XMLTools.java
@@ -5,9 +5,15 @@ import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.AbstractList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.RandomAccess;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
import org.apache.commons.io.IOUtils;
import org.dom4j.io.XMLWriter;
@@ -23,7 +29,6 @@ public class XMLTools extends XMLWriter {
@Override
public String escapeElementEntities(String s) {
return super.escapeElementEntities(s);
-
}
public static List asList(NodeList n) {
diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IDesignatedProductClassification.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IDesignatedProductClassification.java
new file mode 100644
index 00000000..327fb085
--- /dev/null
+++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IDesignatedProductClassification.java
@@ -0,0 +1,42 @@
+/**
+ * *********************************************************************
+ *
+ * Copyright (c) 2024 Jan N. Klug
+ *
+ * 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;
+
+import org.mustangproject.ClassCode;
+
+/**
+ * A product classification that allows to describe a product. Classification codes are schemed by the available systems in UNTDID 7143.
+ */
+public interface IDesignatedProductClassification {
+ /**
+ * Classification code
+ *
+ * @return the classification code
+ */
+ ClassCode getClassCode();
+
+ /**
+ * an optional, human-readable description of the classifcation code
+ *
+ * @return the name or {@code null} if not set
+ */
+ default String getClassName() { return null; }
+}
diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableProduct.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableProduct.java
index 049bb546..ae9eb5b8 100644
--- a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableProduct.java
+++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableProduct.java
@@ -165,4 +165,11 @@ public interface IZUGFeRDExportableProduct {
default HashMap getAttributes() {
return null;
}
+
+ /**
+ * Detailed information about the product
+ *
+ * @return an array containing the product classifications or {@code null} if not set
+ */
+ default IDesignatedProductClassification[] getClassifications() { 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 f98674c4..16e3906d 100644
--- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java
+++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java
@@ -423,6 +423,20 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
XMLTools.encodeXML(currentItem.getProduct().getDescription()) +
"";
}
+ if (currentItem.getProduct().getClassifications() != null && currentItem.getProduct().getClassifications().length > 0) {
+ for (IDesignatedProductClassification classification : currentItem.getProduct().getClassifications()) {
+ xml += ""
+ + "" + classification.getClassCode().getCode() + "";
+ if (classification.getClassName() != null) {
+ xml += "" + XMLTools.encodeXML(classification.getClassName()) + "";
+ }
+ xml += "";
+ }
+ }
if (currentItem.getProduct().getAttributes() != null) {
for ( Entry entry : currentItem.getProduct().getAttributes().entrySet() ) {
xml += "" +
diff --git a/library/src/main/java/org/mustangproject/util/NodeMap.java b/library/src/main/java/org/mustangproject/util/NodeMap.java
new file mode 100644
index 00000000..abe73d12
--- /dev/null
+++ b/library/src/main/java/org/mustangproject/util/NodeMap.java
@@ -0,0 +1,138 @@
+/**
+ * *********************************************************************
+ *
+ * Copyright (c) 2024 Jan N. Klug
+ *
+ * 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.util;
+
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+/**
+ * The {@link NodeMap} contains a {@link Map} representation of DOM object
+ *
+ * It can be constructed either from the children of a single {@link Node} or a {@link NodeList}.
+ */
+public class NodeMap {
+ private final Map map;
+
+ /**
+ * Create a new {@link NodeMap}
+ *
+ * The {@link Node} that is passed to the constructor must not be null.
+ * The {@link NodeMap} will be empty when no child nodes are present.
+ *
+ * @param node the node that shall be represented by this {@link NodeMap}
+ * @throws IllegalArgumentException when argument is null
+ */
+ public NodeMap(Node node) {
+ if (node == null) {
+ throw new IllegalArgumentException("node cannot be null");
+ }
+ if (!node.hasChildNodes()) {
+ map = Map.of();
+ } else {
+ map = mapNodeList(node.getChildNodes());
+ }
+ }
+
+ public NodeMap(NodeList nodeList) {
+ if (nodeList == null) {
+ throw new IllegalArgumentException("nodeList cannot be null");
+ }
+ map = mapNodeList(nodeList);
+ }
+
+ /**
+ * Get matching node by {@code LocalName}
+ *
+ * In case more than one node matches, it is not guaranteed that the first match is selected
+ *
+ * @param localNames one or more {@code LocalName}s
+ * @return the matching node
+ */
+ public Optional getNode(String... localNames) {
+ return getAllNodes(localNames).findAny();
+ }
+
+ /**
+ * Get a {@link NodeMap} of the child of a matching node by {@code LocalName}
+ *
+ * In case more than one node matches, it is not guaranteed that the first match is selected
+ *
+ * @param localNames one or more {@code LocalName}s
+ * @return the {@link NodeMap} representation of the matching node
+ */
+ public Optional getAsNodeMap(String... localNames) {
+ return getNode(localNames).filter(Node::hasChildNodes).map(NodeMap::new);
+ }
+
+ /**
+ * Get the text content of a matching node
+ *
+ * In case more than one node matches, it is not guaranteed that the first match is selected
+ *
+ * @param localNames one or more {@code LocalName}s
+ * @return the text content of the matching node
+ */
+ public Optional getAsString(String... localNames) {
+ return getNode(localNames).map(Node::getTextContent);
+ }
+
+ /**
+ * Get the text content of a matching node
+ *
+ * In case more than one node matches, it is not guaranteed that the first match is selected
+ *
+ * @param localNames one or more {@code LocalName}s
+ * @return the text content of the matching node or {@code null} when no matching node could be found
+ */
+ public String getAsStringOrNull(String... localNames) {
+ return getAsString(localNames).orElse(null);
+ }
+
+ /**
+ * Get all matching nodes
+ *
+ * @param localNames one or more {@code LocalName}s
+ * @return a {@code Stream} that contains all matching nodes (can be empty if nothing matches)
+ */
+ public Stream getAllNodes(String... localNames) {
+ List localNamesList = Arrays.asList(localNames);
+ return map.entrySet().stream().filter(e -> localNamesList.contains(e.getKey())).map(Map.Entry::getValue);
+ }
+
+ private Map mapNodeList(NodeList nodeList) {
+ return IntStream.range(0, nodeList.getLength()).mapToObj(nodeList::item)
+ .filter(node -> node != null && node.getLocalName() != null)
+ .collect(Collectors.toMap(Node::getLocalName, node -> node));
+ }
+
+ @Override
+ public String toString() {
+ return map.toString();
+ }
+}