Merge pull request #476 from J-N-K/prodclass

Add DesignatedProductClassification for SpecifiedTradeProduct
This commit is contained in:
Jochen Staerk
2024-09-20 13:27:44 +02:00
committed by GitHub
11 changed files with 565 additions and 297 deletions

View File

@@ -0,0 +1,103 @@
/**
* *********************************************************************
* <p>
* Copyright (c) 2024 Jan N. Klug
* <p>
* Use is subject to license terms.
* <p>
* 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.
* <p>
* 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.
* <p>
* See the License for the specific language governing permissions and
* limitations under the License.
* <p>
* **********************************************************************
*/
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;
}
}

View File

@@ -0,0 +1,71 @@
/**
* *********************************************************************
* <p>
* Copyright (c) 2024 Jan N. Klug
* <p>
* Use is subject to license terms.
* <p>
* 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.
* <p>
* 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.
* <p>
* See the License for the specific language governing permissions and
* limitations under the License.
* <p>
* **********************************************************************
*/
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;
}
}

View File

@@ -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<String> notes = null;
protected ArrayList<ReferencedDocument> referencedDocuments = null;
protected ArrayList<ReferencedDocument> additionalReference = null;
protected ArrayList<IZUGFeRDAllowanceCharge> Allowances = new ArrayList<>(),
Charges = new ArrayList<>();
protected ArrayList<IZUGFeRDAllowanceCharge> Allowances = new ArrayList<>();
protected ArrayList<IZUGFeRDAllowanceCharge> 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,75 @@ 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<ReferencedDocument> rdocs = null;
ArrayList<ReferencedDocument> 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.getAsBigDecimal("Percent"))
.ifPresent(product::setVATPercent);
});
// nodes.item(i).getTextContent())) {
itemMap.getAsNodeMap("Price").ifPresent(icnm -> {
// ubl
// PriceAmount with currencyID and BaseQuantity with unitCode
icnm.getAsBigDecimal("PriceAmount").ifPresent(this::setPrice);
icnm.getAsBigDecimal("BaseQuantity").ifPresent(this::setBasisQuantity);
});
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.getAsNodeMap("BuyerOrderReferencedDocument")
.flatMap(bordNodes -> bordNodes.getAsString("LineID"))
.ifPresent(this::addReferencedLineID);
icnm.getAsNodeMap("NetPriceProductTradePrice").ifPresent(npptpNodes -> {
npptpNodes.getAsBigDecimal("ChargeAmount").ifPresent(this::setPrice);
npptpNodes.getAsBigDecimal("BasisQuantity").ifPresent(this::setBasisQuantity);
});
icnm.getAllNodes("AdditionalReferencedDocument").map(ReferencedDocument::fromNode)
.forEach(this::addReferencedDocument);
});
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")
.flatMap(cnm -> cnm.getAsBigDecimal("RateApplicablePercent", "ApplicablePercent"))
.ifPresent(product::setVATPercent);
if (recalcPrice && !BigDecimal.ZERO.equals(quantity)) {
icnm.getAsNodeMap("SpecifiedTradeSettlementLineMonetarySummation")
.flatMap(cnm -> cnm.getAsBigDecimal("LineTotalAmount"))
.ifPresent(lineTotal -> setPrice(lineTotal.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 +355,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 +365,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;
}

View File

@@ -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
@@ -20,7 +27,8 @@ public class Product implements IZUGFeRDExportableProduct {
protected boolean isIntraCommunitySupply = false;
protected SchemedID globalId = null;
protected String countryOfOrigin = null;
protected HashMap<String, String> attributes = null;
protected HashMap<String, String> attributes = new HashMap<>();
protected List<IDesignatedProductClassification> classifications = new ArrayList<>();
/***
* default constructor
@@ -36,6 +44,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
@@ -242,19 +286,57 @@ public class Product implements IZUGFeRDExportableProduct {
@Override
public HashMap<String, String> getAttributes() {
return this.attributes;
if (attributes.isEmpty()) {
return null;
} else {
return this.attributes;
}
}
public Product setAttributes(HashMap<String, String> attributes) {
this.attributes = attributes;
public Product setAttributes(Map<String, String> 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;
}
}

View File

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

View File

@@ -31,4 +31,9 @@ public class SchemedID {
setId(id);
}
@Override
public String toString() {
return "SchemedID{scheme='" + scheme + "', id='" + id + "'}";
}
}

View File

@@ -4,15 +4,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.AbstractList;
import java.util.Collections;
import java.util.List;
import java.util.RandomAccess;
import org.apache.commons.io.IOUtils;
import org.dom4j.io.XMLWriter;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class XMLTools extends XMLWriter {
@Override
@@ -23,31 +17,6 @@ public class XMLTools extends XMLWriter {
@Override
public String escapeElementEntities(String s) {
return super.escapeElementEntities(s);
}
public static List<Node> asList(NodeList n) {
return n.getLength() == 0 ?
Collections.<Node>emptyList() : new NodeListWrapper(n);
}
static final class NodeListWrapper extends AbstractList<Node>
implements RandomAccess {
private final NodeList list;
NodeListWrapper(NodeList l) {
list = l;
}
@Override
public Node get(int index) {
return list.item(index);
}
@Override
public int size() {
return list.getLength();
}
}
public static String nDigitFormat(BigDecimal value, int scale) {
@@ -70,7 +39,6 @@ public class XMLTools extends XMLWriter {
}
public static String encodeXML(CharSequence s) {
if (s == null) {
return "";

View File

@@ -0,0 +1,42 @@
/**
* *********************************************************************
* <p>
* Copyright (c) 2024 Jan N. Klug
* <p>
* Use is subject to license terms.
* <p>
* 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.
* <p>
* 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.
* <p>
* See the License for the specific language governing permissions and
* limitations under the License.
* <p>
* **********************************************************************
*/
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; }
}

View File

@@ -165,4 +165,11 @@ public interface IZUGFeRDExportableProduct {
default HashMap<String, String> 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; }
}

View File

@@ -445,6 +445,20 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
XMLTools.encodeXML(currentItem.getProduct().getDescription()) +
"</ram:Description>";
}
if (currentItem.getProduct().getClassifications() != null && currentItem.getProduct().getClassifications().length > 0) {
for (IDesignatedProductClassification classification : currentItem.getProduct().getClassifications()) {
xml += "<ram:DesignatedProductClassification>"
+ "<ram:ClassCode listId=\"" + XMLTools.encodeXML(classification.getClassCode().getListID()) + "\"";
if (classification.getClassCode().getListVersionID() != null) {
xml += " listVersionID=\"" + XMLTools.encodeXML(classification.getClassCode().getListVersionID()) + "\"";
}
xml += ">" + classification.getClassCode().getCode() + "</ram:ClassCode>";
if (classification.getClassName() != null) {
xml += "<ram:ClassName>" + XMLTools.encodeXML(classification.getClassName()) + "</ram:ClassName>";
}
xml += "</ram:DesignatedProductClassification>";
}
}
if (currentItem.getProduct().getAttributes() != null) {
for (Entry<String, String> entry : currentItem.getProduct().getAttributes().entrySet()) {
xml += "<ram:ApplicableProductCharacteristic>" +

View File

@@ -0,0 +1,151 @@
/**
* *********************************************************************
* <p>
* Copyright (c) 2024 Jan N. Klug
* <p>
* Use is subject to license terms.
* <p>
* 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.
* <p>
* 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.
* <p>
* See the License for the specific language governing permissions and
* limitations under the License.
* <p>
* **********************************************************************
*/
package org.mustangproject.util;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
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
* <p />
* It can be constructed either from the children of a single {@link Node} or a {@link NodeList}.
*/
public class NodeMap {
private final Map<String, List<Node>> map = new HashMap<>();
/**
* Create a new {@link NodeMap}
* <p />
* 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()) {
mapNodeList(node.getChildNodes());
}
}
public NodeMap(NodeList nodeList) {
if (nodeList == null) {
throw new IllegalArgumentException("nodeList cannot be null");
}
mapNodeList(nodeList);
}
/**
* Get matching node by {@code LocalName}
* <p />
* 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<Node> getNode(String... localNames) {
return getAllNodes(localNames).findAny();
}
/**
* Get a {@link NodeMap} of the child of a matching node by {@code LocalName}
* <p />
* 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<NodeMap> getAsNodeMap(String... localNames) {
return getNode(localNames).filter(Node::hasChildNodes).map(NodeMap::new);
}
/**
* Get the text content of a matching node
* <p>
* 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<String> getAsString(String... localNames) {
return getNode(localNames).map(Node::getTextContent);
}
/**
* Get the text content of a matching node
* <p>
* 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, converted to BigDecimal
*/
public Optional<BigDecimal> getAsBigDecimal(String... localNames) {
return getNode(localNames).map(Node::getTextContent).map(s->new BigDecimal(s.trim()));
}
/**
* Get the text content of a matching node
* <p>
* 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<Node> getAllNodes(String... localNames) {
List<String> localNamesList = Arrays.asList(localNames);
return map.entrySet().stream().filter(e -> localNamesList.contains(e.getKey())).flatMap(e -> e.getValue().stream());
}
private void mapNodeList(NodeList nodeList) {
IntStream.range(0, nodeList.getLength()).mapToObj(nodeList::item)
.filter(node -> node != null && node.getLocalName() != null)
.forEach(node -> map.computeIfAbsent(node.getLocalName(), k -> new ArrayList<>()).add(node));
}
@Override
public String toString() {
return map.toString();
}
}