diff --git a/imixs-archive-documents/src/main/java/org/imixs/archive/documents/EInvoiceMetaAdapter.java b/imixs-archive-documents/src/main/java/org/imixs/archive/documents/EInvoiceMetaAdapter.java new file mode 100644 index 0000000..1aa60bd --- /dev/null +++ b/imixs-archive-documents/src/main/java/org/imixs/archive/documents/EInvoiceMetaAdapter.java @@ -0,0 +1,84 @@ +package org.imixs.archive.documents; + +import java.util.logging.Logger; + +import org.imixs.workflow.FileData; +import org.imixs.workflow.ItemCollection; +import org.imixs.workflow.exceptions.AdapterException; +import org.imixs.workflow.exceptions.PluginException; + +/** + * The EInvoiceAutoAdapter can detect and extract content from e-invoice + * documents + * in different formats. This Adapter class extends the {@link EInvoiceAdapter} + * and resolves pre defined Items according to the Factur-X/ZUGFeRD 2.0 + * standard. + * + * @author rsoika + * @version 2.0 + * + */ +public class EInvoiceMetaAdapter extends EInvoiceAdapter { + private static Logger logger = Logger.getLogger(EInvoiceAdapter.class.getName()); + + /** + * Executes the e-invoice detection process on the given workitem. + * It attempts to detect the e-invoice format from attached files and + * updates the workitem with the result. + * + * @param workitem The workitem to process + * @param event The event triggering this execution + * @return The updated workitem + * @throws AdapterException If there's an error in the adapter execution + * @throws PluginException If there's an error in plugin processing + */ + @Override + public ItemCollection execute(ItemCollection workitem, ItemCollection event) + throws AdapterException, PluginException { + + // Detect and read E-Invoice Data + FileData eInvoiceFileData = detectEInvoice(workitem); + + if (eInvoiceFileData == null) { + logger.info("No e-invoice type detected."); + return workitem; + } else { + String einvoiceType = detectEInvoiceType(eInvoiceFileData); + workitem.setItemValue(FILE_ATTRIBUTE_EINVOICE_TYPE, einvoiceType); + logger.info("Detected e-invoice type: " + einvoiceType); + + // readEInvoiceContent(eInvoiceFileData, workitem); + } + + return workitem; + } + + /** + * This method resolves the content of a factur-x e-invocie file and extracts + * all invoice and customer fields. + * + * This is the variant without Mustang Project + * + * + * @param xmlData + * @return + * @throws PluginException + */ + private void readEInvoiceContentNativeXML(FileData eInvoiceFileData, + ItemCollection workitem) throws PluginException { + byte[] xmlData = readXMLContent(eInvoiceFileData); + logger.info("Autodetect e-invoice data..."); + + createXMLDoc(xmlData); + + readItem(workitem, "//rsm:CrossIndustryInvoice/rsm:ExchangedDocument/ram:ID", "text", "invoice.number"); + readItem(workitem, "//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString/text()", "date", + "invoice.date"); + readItem(workitem, "//ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount", "double", + "invoice.total"); + readItem(workitem, "//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name/text()", "text", + "cdtr.name"); + + } + +} diff --git a/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/EInvoiceModel.java b/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/EInvoiceModel.java new file mode 100644 index 0000000..3b4aeea --- /dev/null +++ b/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/EInvoiceModel.java @@ -0,0 +1,446 @@ +package org.imixs.archive.documents.einvoice; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.security.SecureRandom; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * A EInvoiceModel represents the dom tree of a e-invoice. + *

+ * The model elements can be read only. + * + * @author rsoika + * + */ +public class EInvoiceModel { + protected static Logger logger = Logger.getLogger(EInvoiceModel.class.getName()); + + private static final SecureRandom random = new SecureRandom(); + private static final Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); + + private Document doc; + private Element crossIndustryInvoice; + private Element exchangedDocumentContext; + private Element exchangedDocument; + private Element supplyChainTradeTransaction; + + // elements + protected String id = null; + protected String buyerReference = null; + protected LocalDate issueDateTime = null; + protected BigDecimal grandTotalAmount = new BigDecimal("0.00"); + protected BigDecimal taxTotalAmount = new BigDecimal("0.00"); + protected BigDecimal netTotalAmount = new BigDecimal("0.00"); + protected Set tradeParties = null; + + private final Map URI_BY_NAMESPACE = new HashMap<>(); + private final Map PREFIX_BY_NAMESPACE = new HashMap<>(); + public static final String FILE_PREFIX = "file://"; + + /** + * This method instantiates a new BPMN model with the default BPMN namespaces + * and prefixes. + * + * @param doc + */ + private EInvoiceModel() { + setUri(EInvoiceNS.A, "urn:un:unece:uncefact:data:standard:QualifiedDataType:100"); + setUri(EInvoiceNS.RSM, "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"); + setUri(EInvoiceNS.QDT, "urn:un:unece:uncefact:data:standard:QualifiedDataType:10"); + setUri(EInvoiceNS.RAM, "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"); + setUri(EInvoiceNS.UDT, "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"); + + setPrefix(EInvoiceNS.A, "a"); + setPrefix(EInvoiceNS.RSM, "rsm"); + setPrefix(EInvoiceNS.QDT, "qdt"); + setPrefix(EInvoiceNS.RAM, "ram"); + setPrefix(EInvoiceNS.UDT, "udt"); + + } + + /** + * This method instantiates a new eInvoice model based on a given + * org.w3c.dom.Document. The method parses the namespaces. + *

+ * + * + * + * @param doc + */ + public EInvoiceModel(Document doc) { + this(); + tradeParties = new LinkedHashSet<>(); + + if (doc != null) { + this.doc = doc; + + crossIndustryInvoice = doc.getDocumentElement(); + + // parse the BPMN namespaces + NamedNodeMap defAttributes = crossIndustryInvoice.getAttributes(); + for (int j = 0; j < defAttributes.getLength(); j++) { + Node node = defAttributes.item(j); + + if (getPrefix(EInvoiceNS.A).equals(node.getLocalName()) + && !getUri(EInvoiceNS.A).equals(node.getNodeValue())) { + logger.fine("...set A namespace URI: " + node.getNodeValue()); + setUri(EInvoiceNS.A, node.getNodeValue()); + } + if (getPrefix(EInvoiceNS.RSM).equals(node.getLocalName()) + && !getUri(EInvoiceNS.RSM).equals(node.getNodeValue())) { + logger.fine("...set RSM namespace URI: " + node.getNodeValue()); + setUri(EInvoiceNS.RSM, node.getNodeValue()); + } + if (getPrefix(EInvoiceNS.QDT).equals(node.getLocalName()) + && !getUri(EInvoiceNS.QDT).equals(node.getNodeValue())) { + logger.fine("...set QDT namespace URI: " + node.getNodeValue()); + setUri(EInvoiceNS.QDT, node.getNodeValue()); + } + if (getPrefix(EInvoiceNS.RAM).equals(node.getLocalName()) + && !getUri(EInvoiceNS.RAM).equals(node.getNodeValue())) { + logger.fine("...set RAM namespace URI: " + node.getNodeValue()); + setUri(EInvoiceNS.RAM, node.getNodeValue()); + } + if (getPrefix(EInvoiceNS.UDT).equals(node.getLocalName()) + && !getUri(EInvoiceNS.UDT).equals(node.getNodeValue())) { + logger.fine("...set UDT namespace URI: " + node.getNodeValue()); + setUri(EInvoiceNS.UDT, node.getNodeValue()); + } + + } + + exchangedDocumentContext = findChildNodeByName(crossIndustryInvoice, EInvoiceNS.RSM, + "ExchangedDocumentContext"); + exchangedDocument = findChildNodeByName(crossIndustryInvoice, EInvoiceNS.RSM, "ExchangedDocument"); + supplyChainTradeTransaction = findChildNodeByName(crossIndustryInvoice, EInvoiceNS.RSM, + "SupplyChainTradeTransaction"); + + // Load e-invoice standard data + loadDocumentCoreData(); + + } + } + + public Document getDoc() { + return doc; + } + + public Element getCrossIndustryInvoice() { + return crossIndustryInvoice; + } + + public Element getExchangedDocumentContext() { + return exchangedDocumentContext; + } + + public Element getExchangedDocument() { + return exchangedDocument; + } + + public String getId() { + return id; + } + + public LocalDate getIssueDateTime() { + return issueDateTime; + } + + public BigDecimal getGrandTotalAmount() { + return grandTotalAmount; + } + + public BigDecimal getTaxTotalAmount() { + return taxTotalAmount; + } + + public BigDecimal getNetTotalAmount() { + return netTotalAmount; + } + + /** + * Returns all trade parties + * + * @return + */ + public Set getTradeParties() { + if (tradeParties == null) { + tradeParties = new LinkedHashSet(); + } + return tradeParties; + } + + /** + * Finds a Trade Party by its type. Method can return null if not trade party of + * the type is defined in the invoice + * + * @param type + * @return + */ + public TradeParty findTradeParty(String type) { + if (type == null || type.isEmpty()) { + return null; + } + Iterator iterParties = tradeParties.iterator(); + while (iterParties.hasNext()) { + TradeParty party = iterParties.next(); + if (type.equals(party.getType())) { + return party; + } + } + // not found + return null; + } + + /** + * This helper method returns a set of child nodes by name from a given parent + * node. If no nodes were found, the method returns an empty list. + *

+ * The method compares the Name including the namespace of the child elements. + *

+ * See also {@link #findChildNodeByName(Element parent, String nodeName) + * findChildNodeByName} + * + * @param parent + * @param nodeName + * @return - list of nodes. If no nodes were found, the method returns an empty + * list + */ + public Set findChildNodesByName(Element parent, EInvoiceNS ns, String nodeName) { + Set result = new LinkedHashSet(); + // resolve the tag name + String tagName = getPrefix(ns) + nodeName; + if (parent != null && nodeName != null) { + NodeList childs = parent.getChildNodes(); + for (int i = 0; i < childs.getLength(); i++) { + Node childNode = childs.item(i); + + if (childNode.getNodeType() == Node.ELEMENT_NODE && tagName.equals(childNode.getNodeName())) { + result.add((Element) childNode); + } + } + } + return result; + } + + /** + * This helper method returns the first child node by name from a given parent + * node. If no nodes were found the method returns null. + * + * See also {@link #findChildNodesByName(Element parent, String nodeName) + * findChildNodesByName} + * + * @param parent + * @param nodeName + * @return - Child Element matching the given node name. If no nodes were found, + * the method returns null + */ + public Element findChildNodeByName(Element parent, EInvoiceNS ns, String nodeName) { + Set elementList = findChildNodesByName(parent, ns, nodeName); + if (elementList.iterator().hasNext()) { + // return first element + return elementList.iterator().next(); + } else { + // no child elements with the given name found! + return null; + } + } + + public TradeParty parseTradeParty(Element tradePartyElement, String type) { + TradeParty tradeParty = new TradeParty(type); + Element element = null; + + // Parse name + element = findChildNodeByName(tradePartyElement, EInvoiceNS.RAM, + "Name"); + if (element != null) { + tradeParty.setName(element.getTextContent()); + } + + Element postalAddress = findChildNodeByName(tradePartyElement, EInvoiceNS.RAM, + "PostalTradeAddress"); + if (postalAddress != null) { + element = findChildNodeByName(postalAddress, EInvoiceNS.RAM, + "PostcodeCode"); + if (element != null) { + tradeParty.setPostcodeCode(element.getTextContent()); + } + element = findChildNodeByName(postalAddress, EInvoiceNS.RAM, + "CityName"); + if (element != null) { + tradeParty.setCityName(element.getTextContent()); + } + element = findChildNodeByName(postalAddress, EInvoiceNS.RAM, + "CountryID"); + if (element != null) { + tradeParty.setCountryId(element.getTextContent()); + } + element = findChildNodeByName(postalAddress, EInvoiceNS.RAM, + "LineOne"); + if (element != null) { + tradeParty.setStreetAddress(element.getTextContent()); + } + } + + Element specifiedTaxRegistration = findChildNodeByName(tradePartyElement, EInvoiceNS.RAM, + "SpecifiedTaxRegistration"); + if (specifiedTaxRegistration != null) { + element = findChildNodeByName(specifiedTaxRegistration, EInvoiceNS.RAM, + "ID"); + if (element != null) { + tradeParty.setVatNumber(element.getTextContent()); + } + } + return tradeParty; + } + + /** + * Returns the central logger instance + * + * @return + */ + public static Logger getLogger() { + return logger; + } + + public static void log(String message) { + logger.info(message); + } + + public static void warning(String message) { + logger.warning(message); + } + + public static void error(String message) { + logger.severe(message); + } + + public static void debug(String message) { + logger.info(message); + } + + private void loadDocumentCoreData() { + Element element = null; + + // read invoice number + element = findChildNodeByName(exchangedDocument, EInvoiceNS.RAM, "ID"); + if (element != null) { + id = element.getTextContent(); + } + + // read Date time + element = findChildNodeByName(exchangedDocument, EInvoiceNS.RAM, "IssueDateTime"); + if (element != null) { + Element dateTimeElement = findChildNodeByName(element, EInvoiceNS.UDT, "DateTimeString"); + if (dateTimeElement != null) { + String dateStr = dateTimeElement.getTextContent(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + issueDateTime = LocalDate.parse(dateStr, formatter); + } + } + + // read Total amount + element = findChildNodeByName(supplyChainTradeTransaction, EInvoiceNS.RAM, "ApplicableHeaderTradeSettlement"); + if (element != null) { + Element tradeSettlementElement = findChildNodeByName(element, EInvoiceNS.RAM, + "SpecifiedTradeSettlementHeaderMonetarySummation"); + if (tradeSettlementElement != null) { + Element child = findChildNodeByName(tradeSettlementElement, EInvoiceNS.RAM, "GrandTotalAmount"); + if (child != null) { + grandTotalAmount = new BigDecimal(child.getTextContent()); + } + child = findChildNodeByName(tradeSettlementElement, EInvoiceNS.RAM, "TaxTotalAmount"); + if (child != null) { + taxTotalAmount = new BigDecimal(child.getTextContent()); + } + netTotalAmount = grandTotalAmount.subtract(taxTotalAmount).setScale(2, RoundingMode.HALF_UP); + + } + + } + + // read ApplicableHeaderTradeAgreement - buyerReference + element = findChildNodeByName(supplyChainTradeTransaction, EInvoiceNS.RAM, "ApplicableHeaderTradeAgreement"); + if (element != null) { + Element buyerReferenceElement = findChildNodeByName(element, EInvoiceNS.RAM, + "BuyerReference"); + if (buyerReferenceElement != null) { + buyerReference = buyerReferenceElement.getTextContent(); + } + Element tradePartyElement = findChildNodeByName(element, EInvoiceNS.RAM, + "SellerTradeParty"); + if (tradePartyElement != null) { + tradeParties.add(parseTradeParty(tradePartyElement, "seller")); + } + tradePartyElement = findChildNodeByName(element, EInvoiceNS.RAM, + "BuyerTradeParty"); + if (tradePartyElement != null) { + tradeParties.add(parseTradeParty(tradePartyElement, "buyer")); + } + } + + } + + /** + * Returns the namespace uri for a given namespace + * + * @param ns + * @return + */ + public String getUri(EInvoiceNS ns) { + return URI_BY_NAMESPACE.get(ns); + } + + public void setUri(EInvoiceNS ns, String uri) { + URI_BY_NAMESPACE.put(ns, uri); + } + + /** + * Returns the namespace prefix for a given namespace - e.g. 'ram' or + * 'rsm'... + *

+ * This is necessary because a model can work with the default namespace + * prefix. The model instance automatically detects the used + * namespace prefix and updates the prefix when loading a model file. + * + * @param ns + * @return + */ + public String getPrefix(EInvoiceNS ns) { + return PREFIX_BY_NAMESPACE.get(ns); + } + + /** + * Updates the namespace prefix for a given BPMN namespace - e.g. 'bpmn2' or + * 'bpmndi' + * The method automatically adds the prefix separator ':' if the prefix is not + * empty. This is necessary to handle default namespaces without a prefix + * correctly + */ + public void setPrefix(EInvoiceNS ns, String prefix) { + if (prefix == null) { + prefix = ""; + } + if (prefix.isEmpty()) { + PREFIX_BY_NAMESPACE.put(ns, prefix); + } else { + PREFIX_BY_NAMESPACE.put(ns, prefix + ":"); + } + + } + +} diff --git a/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/EInvoiceModelFactory.java b/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/EInvoiceModelFactory.java new file mode 100644 index 0000000..19724dc --- /dev/null +++ b/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/EInvoiceModelFactory.java @@ -0,0 +1,106 @@ +package org.imixs.archive.documents.einvoice; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Logger; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * The BPMNModelFactory can be used to load or create a BPMNModel instance. + * + * @author rsoika + * + */ +public class EInvoiceModelFactory { + private static Logger logger = Logger.getLogger(EInvoiceModelFactory.class.getName()); + + /** + * Reads a EInvoiceModel instance from an java.io.File + * + * @param modelFile + * @return a EInvoiceModel instance + * @throws FileNotFoundException + * + */ + public static EInvoiceModel read(File modelFile) throws FileNotFoundException { + + return read(new FileInputStream(modelFile)); + + } + + /** + * Reads a EInvoiceModel instance from an given file path + * + * @param modelFile + * @return a EInvoiceModel instance + * @throws FileNotFoundException + */ + public static EInvoiceModel read(String modelFilePath) throws FileNotFoundException { + return read(EInvoiceModel.class.getResourceAsStream(modelFilePath)); + } + + /** + * Reads a EInvoiceModel instance from an InputStream. + *

+ * + * + * @param modelFile + * @return a EInvoiceModel instance + * @throws FileNotFoundException + */ + public static EInvoiceModel read(InputStream is) throws FileNotFoundException { + logger.fine("read from inputStream..."); + if (is == null) { + throw new NullPointerException( + "Model can not be parsed: InputStream is null"); + } + DocumentBuilderFactory docFactory = DocumentBuilderFactory + .newInstance(); + docFactory.setIgnoringElementContentWhitespace(true); // because of a bug this does not have + // any effect! + docFactory.setNamespaceAware(true); + + try { + if (is.available() == 0) { + logger.warning("Empty BPMN file - creating a default process"); + throw new IOException( + "Model can not be parsed: No Content"); + } + + // parse XML file + DocumentBuilder db = docFactory.newDocumentBuilder(); + // read from a project's resources folder + Document doc = db.parse(is); + Element root = doc.getDocumentElement(); + + // explicit remove whitespace + + EInvoiceModel model = new EInvoiceModel(doc); + + return model; + } catch (ParserConfigurationException | SAXException | IOException e) { + logger.severe(e.getMessage()); + // create a runtimeException to show a error message in the client + throw new RuntimeException(e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + +} diff --git a/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/EInvoiceNS.java b/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/EInvoiceNS.java new file mode 100644 index 0000000..7122c33 --- /dev/null +++ b/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/EInvoiceNS.java @@ -0,0 +1,27 @@ +package org.imixs.archive.documents.einvoice; + +/** + * Defines the valid BPMN namespaces. + *

+ * NOTE: The primary namespace can be either 'bpmn2' or 'bpmn' ! But 'bpmn2' is + * the default. + * + * + * + * // xmlns:a='urn:un:unece:uncefact:data:standard:QualifiedDataType:100' + * // xmlns:rsm='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100' + * // xmlns:qdt='urn:un:unece:uncefact:data:standard:QualifiedDataType:10' + * // + * xmlns:ram='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100' + * // xmlns:xs='http://www.w3.org/2001/XMLSchema' + * // xmlns:udt='urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'> + * + * @author rsoika + */ +public enum EInvoiceNS { + A, // default + RSM, // + QDT, // + RAM, // + UDT; +} diff --git a/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/TradeParty.java b/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/TradeParty.java new file mode 100644 index 0000000..ce5bb72 --- /dev/null +++ b/imixs-archive-documents/src/main/java/org/imixs/archive/documents/einvoice/TradeParty.java @@ -0,0 +1,135 @@ +package org.imixs.archive.documents.einvoice; + +/** + * A TradeParty is a container for a TradeParty element + * + * @author rsoika + * + */ +public class TradeParty { + + private String type; // type of trade party + private String name; + private String postcodeCode; + private String streetAddress; + private String cityName; + private String countryId; + private String vatNumber; + + public TradeParty(String type) { + this.type = type; + } + // public TradeParty(Element sellerElement, String type) { + // this.type = type; + // // Parse name + // Element element=sellerElement.getElementsByTagNameNS( EInvoiceNS.RAM.name(), + // "name"); + // this.name = + // .getElementsByTagName("ram:Name") + // .item(0) + // .getTextContent(); + + // // Get PostalTradeAddress element + // Element postalAddress = (Element) sellerElement + // .getElementsByTagName("ram:PostalTradeAddress") + // .item(0); + + // // Parse address details + // this.postcodeCode = postalAddress + // .getElementsByTagName("ram:PostcodeCode") + // .item(0) + // .getTextContent(); + + // this.streetAddress = postalAddress + // .getElementsByTagName("ram:LineOne") + // .item(0) + // .getTextContent(); + + // this.cityName = postalAddress + // .getElementsByTagName("ram:CityName") + // .item(0) + // .getTextContent(); + + // this.countryId = postalAddress + // .getElementsByTagName("ram:CountryID") + // .item(0) + // .getTextContent(); + + // // // Parse VAT number + // // Element taxRegistration = (Element) sellerElement + // // .getElementsByTagName("ram:SpecifiedTaxRegistration") + // // .item(0); + + // // this.vatNumber = taxRegistration + // // .getElementsByTagName("ram:ID") + // // .item(0) + // // .getTextContent(); + // } + + // Getters + public String getType() { + return type; + } + + public String getName() { + return name; + } + + public String getPostcodeCode() { + return postcodeCode; + } + + public String getStreetAddress() { + return streetAddress; + } + + public String getCityName() { + return cityName; + } + + public String getCountryId() { + return countryId; + } + + public String getVatNumber() { + return vatNumber; + } + + public void setName(String name) { + this.name = name; + } + + public void setPostcodeCode(String postcodeCode) { + this.postcodeCode = postcodeCode; + } + + public void setStreetAddress(String streetAddress) { + this.streetAddress = streetAddress; + } + + public void setCityName(String cityName) { + this.cityName = cityName; + } + + public void setCountryId(String countryId) { + this.countryId = countryId; + } + + public void setVatNumber(String vatNumber) { + this.vatNumber = vatNumber; + } + + // toString method for easy debugging + @Override + public String toString() { + return "SellerTradeParty{" + + "name='" + name + '\'' + + ", postcodeCode='" + postcodeCode + '\'' + + ", streetAddress='" + streetAddress + '\'' + + ", cityName='" + cityName + '\'' + + ", countryId='" + countryId + '\'' + + ", vatNumber='" + vatNumber + '\'' + + '}'; + } + +} diff --git a/imixs-archive-documents/src/test/java/org/imixs/archive/documents/EInvoiceModelTest.java b/imixs-archive-documents/src/test/java/org/imixs/archive/documents/EInvoiceModelTest.java new file mode 100644 index 0000000..252c832 --- /dev/null +++ b/imixs-archive-documents/src/test/java/org/imixs/archive/documents/EInvoiceModelTest.java @@ -0,0 +1,104 @@ +package org.imixs.archive.documents; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.imixs.archive.documents.einvoice.EInvoiceModel; +import org.imixs.archive.documents.einvoice.EInvoiceModelFactory; +import org.imixs.archive.documents.einvoice.TradeParty; +import org.imixs.workflow.FileData; +import org.imixs.workflow.exceptions.AdapterException; +import org.imixs.workflow.exceptions.ModelException; +import org.imixs.workflow.exceptions.PluginException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * This test class is testing the EInvoiceModel and tests different + * kind of files + * + */ +class EInvoiceModelTest { + + @BeforeEach + public void setUp() throws PluginException, ModelException { + + } + + @Test + void testStandaloneXML() throws AdapterException, PluginException, IOException { + // Prepare test data + + EInvoiceModel eInvoiceModel = loadEInvoice("e-invoice/Rechnung_R_00010.xml", "application/xml"); + + // Verify the result + assertNotNull(eInvoiceModel); + assertEquals("R-00010", eInvoiceModel.getId()); + + LocalDate invoiceDate = eInvoiceModel.getIssueDateTime(); + assertEquals(LocalDate.of(2021, 7, 28), invoiceDate); + + assertEquals(new BigDecimal("4380.9"), eInvoiceModel.getGrandTotalAmount()); + assertEquals(new BigDecimal("510.9"), eInvoiceModel.getTaxTotalAmount()); + assertEquals(new BigDecimal("3870.00"), eInvoiceModel.getNetTotalAmount()); + + // Test SellerTradeParty + TradeParty seller = eInvoiceModel.findTradeParty("seller"); + assertNotNull(seller); + assertEquals("Max Mustermann", seller.getName()); + assertEquals("DE111111111", seller.getVatNumber()); + + TradeParty buyer = eInvoiceModel.findTradeParty("buyer"); + assertNotNull(buyer); + assertEquals("Viborg Metall GbR", buyer.getName()); + + } + + /** + * Creates a FileData object from a file stored under /test/resources/ + * + * @param fileName + * @param contentType + * @return + * @throws IOException + */ + private FileData createFileData(String fileName, String contentType) throws IOException { + byte[] content = null; + ClassLoader classLoader = getClass().getClassLoader(); + try (InputStream is = classLoader.getResourceAsStream(fileName)) { + if (is == null) { + throw new IOException("Resource not found: " + fileName); + } + content = is.readAllBytes(); + } + Map> attributes = new HashMap<>(); + return new FileData(fileName, content, contentType, attributes); + } + + /** + * Reads a e-invoice file into a EInvoice object + * + * @param fileName + * @param contentType + * @return + * @throws IOException + */ + private EInvoiceModel loadEInvoice(String fileName, String contentType) throws IOException { + ClassLoader classLoader = getClass().getClassLoader(); + try (InputStream is = classLoader.getResourceAsStream(fileName)) { + if (is == null) { + throw new IOException("Resource not found: " + fileName); + } + return EInvoiceModelFactory.read(is); + } + } + +} \ No newline at end of file