Compare commits

...

16 Commits

24 changed files with 565 additions and 37 deletions

View File

@ -2,6 +2,9 @@
Tools for working with eInvoicing according to EU standard EU 16931.
As ZUGFeRD 2 and Factur-X unified their specification, these two names are used synonymously and interchangeably
throughout the documentation and code.
## Reading
### Extract eInvoice from a PDF or XML file:
@ -9,11 +12,11 @@ Tools for working with eInvoicing according to EU standard EU 16931.
```kotlin
val reader = EInvoiceReader()
// read a ZUGFeRD or Factor-X PDF that contains eInvoice XML as attachment
// extract invoice data from a ZUGFeRD or Factor-X PDF that contains eInvoice XML as attachment
val invoiceFromPDF = reader.extractFromPdf(File("ZUGFeRD.pdf"))
// read a eInvoice XML file like XRechnung:
val invoiceFromXml = reader.readFromXml(File("XRechnung.xml"))
// extract eInvoice data from a XML file like XRechnung:
val invoiceFromXml = reader.extractFromXml(File("XRechnung.xml"))
```
### Find all invoices of an IMAP email account
@ -51,10 +54,10 @@ fun create() {
val creator = EInvoiceCreator()
// create a PDF that also contains the eInvoice as XML attachment
creator.createZugferdPdf(invoice, pdfResultFile)
creator.createFacturXPdf(invoice, pdfResultFile)
// create only the XML file
val xml = creator.createZugferdXml(invoice)
val xml = creator.createFacturXXml(invoice)
// create a XRechnung
val xRechnung = creator.createXRechnungXml(invoice)
@ -78,10 +81,10 @@ val output = File("Zugferd.pdf")
val creator = EInvoiceCreator()
creator.combinePdfAndInvoiceXml(invoice, existingPdf, output)
creator.attachInvoiceXmlToPdf(invoice, existingPdf, output)
// or if you already have the invoice XML:
val invoiceXml: String = "..." // e.g. creator.createZugferdXml(invoice)
creator.combinePdfAndInvoiceXml(invoiceXml, existingPdf, output)
creator.attachInvoiceXmlToPdf(invoiceXml, existingPdf, output)
```

View File

@ -1,3 +1,15 @@
buildscript {
val kotlinVersion: String by extra
repositories {
mavenCentral()
}
dependencies {
// don't know why but otherwise Quarkus is not able to index the classes of e-invoice-domain
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}
allprojects {
group = "net.codinux.invoicing"

View File

@ -0,0 +1,5 @@
*
!build/*-runner
!build/*-runner.jar
!build/lib/*
!build/quarkus-app/*

View File

@ -0,0 +1,52 @@
plugins {
kotlin("jvm")
kotlin("plugin.allopen")
id("io.quarkus")
}
kotlin {
jvmToolchain(17)
}
val quarkusVersion: String by project
val klfVersion: String by project
val lokiLogAppenderVersion: String by project
dependencies {
implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:$quarkusVersion"))
implementation("io.quarkus:quarkus-kotlin")
implementation("io.quarkus:quarkus-rest")
implementation("io.quarkus:quarkus-rest-jackson")
implementation("io.quarkus:quarkus-smallrye-health")
implementation("io.quarkus:quarkus-smallrye-openapi")
implementation("io.quarkus:quarkus-micrometer")
implementation("io.quarkus:quarkus-micrometer-registry-prometheus")
implementation(project(":e-invoicing-domain"))
implementation("net.codinux.log:klf:$klfVersion")
implementation("net.codinux.log:quarkus-loki-log-appender:$lokiLogAppenderVersion")
implementation("net.codinux.log.kubernetes:codinux-kubernetes-info-retriever:$lokiLogAppenderVersion")
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.rest-assured:rest-assured")
}
tasks.withType<Test> {
systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager")
}
allOpen {
annotation("jakarta.ws.rs.Path")
annotation("jakarta.enterprise.context.ApplicationScoped")
annotation("jakarta.persistence.Entity")
annotation("io.quarkus.test.junit.QuarkusTest")
}

View File

@ -0,0 +1,97 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./gradlew build
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/e-invoice-api-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/e-invoice-api-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/e-invoice-api-jvm
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class")
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
# used to calculate a default maximal heap memory based on a containers restriction.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
# of the container available memory as set here. The default is `50` which means 50%
# of the available memory is used as an upper boundary. You can skip this mechanism by
# setting this value to `0` in which case no `-Xmx` option is added.
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
# is used to calculate a default initial heap memory based on the maximum heap memory.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
# is used as the initial heap size. You can skip this mechanism by setting this value
# to `0` in which case no `-Xms` option is added (example: "25")
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
# This is used to calculate the maximum value of the initial heap memory. If used in
# a container without any memory constraints for the container then this option has
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
# here. The default is 4096MB which means the calculated value of `-Xms` never will
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
# when things are happening. This option, if set to true, will set
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
# true").
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
# (example: "20")
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
# (example: "40")
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
# (example: "4")
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
# previous GC times. (example: "90")
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
# contain the necessary JRE command-line options to specify the required GC, which
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi8/openjdk-17:1.20
ENV LANGUAGE='en_US:en'
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 build/quarkus-app/*.jar /deployments/
COPY --chown=185 build/quarkus-app/app/ /deployments/app/
COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]

View File

@ -0,0 +1,27 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t quarkus/e-invoice-api .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/e-invoice-api
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]

View File

@ -0,0 +1,30 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
# It uses a micro base image, tuned for Quarkus native executables.
# It reduces the size of the resulting container image.
# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image.
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/e-invoice-api .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/e-invoice-api
#
###
FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]

View File

@ -0,0 +1,91 @@
package net.codinux.invoicing.api
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.core.Response
import net.codinux.invoicing.model.Invoice
import net.codinux.invoicing.service.InvoicingService
import org.eclipse.microprofile.openapi.annotations.Operation
import org.eclipse.microprofile.openapi.annotations.tags.Tag
import org.jboss.resteasy.reactive.PartType
import org.jboss.resteasy.reactive.RestForm
import org.jboss.resteasy.reactive.multipart.FileUpload
@Path("")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_XML)
class InvoicingResource(
private val service: InvoicingService
) {
companion object {
private const val MediaTypePdf = "application/pdf"
}
@Path("create/xrechnung")
@POST
@Operation(summary = "Create a XRechnung XML")
@Tag(name = "Create")
fun createXRechnung(invoice: Invoice) =
service.createXRechnung(invoice)
@Path("create/facturx/xml")
@POST
@Operation(summary = "Create a Factur-X / ZUGFeRD XML (ZUGFeRD is a synonym for Factur-X)")
@Tag(name = "Create")
fun createFacturXXml(invoice: Invoice) =
service.createFacturXXml(invoice)
@Path("create/facturx/pdf")
@POST
@Produces(MediaTypePdf)
@Operation(summary = "Create a Factur-X / ZUGFeRD XML, transforms it to PDF and attaches before created XML to it")
@Tag(name = "Create")
fun createFacturXPdf(invoice: Invoice): Response {
val pdfFile = service.createFacturXPdf(invoice)
return createPdfFileResponse(pdfFile, invoice)
}
@Path("attach")
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaTypePdf)
@Operation(summary = "Attaches the invoice data as EN 16931 XML to a PDF file, combining them to a Factur-X / ZUGFeRD hybrid PDF with XML invoice file")
@Tag(name = "Create - Attach")
fun attachInvoiceXmlToPdf(
@RestForm @PartType(MediaType.APPLICATION_JSON) invoice: Invoice,
@RestForm("pdf") @PartType(MediaTypePdf) pdf: FileUpload
): Response {
val pdfFile = service.attachInvoiceXmlToPdf(invoice, pdf.uploadedFile())
return createPdfFileResponse(pdfFile, invoice)
}
@Path("extract")
@POST
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Extract invoice data from a Factur-X / ZUGFeRD or XRechnung file")
@Tag(name = "Extract")
fun extractInvoiceData(invoice: FileUpload) =
service.extractInvoiceData(invoice.uploadedFile())
@Path("validate")
@POST
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
@Operation(summary = "Validate a Factur-X / ZUGFeRD or XRechnung file")
@Tag(name = "Validate")
fun validateInvoiceXml(invoice: FileUpload) =
service.validateInvoice(invoice.uploadedFile()).reportAsXml
private fun createPdfFileResponse(pdfFile: java.nio.file.Path, invoice: Invoice): Response =
Response.ok(pdfFile)
.header("Content-Disposition", "attachment;filename=\"${invoice.invoicingDate.toString().replace('-', '.')} ${invoice.recipient.name} ${invoice.invoiceNumber}.pdf\"")
.build()
}

View File

@ -0,0 +1,63 @@
package net.codinux.invoicing.service
import jakarta.inject.Singleton
import net.codinux.invoicing.creation.EInvoiceCreator
import net.codinux.invoicing.model.Invoice
import net.codinux.invoicing.reader.EInvoiceReader
import net.codinux.invoicing.validation.EInvoiceValidator
import java.io.File
import java.nio.file.Path
import kotlin.io.path.extension
@Singleton
class InvoicingService {
private val creator = EInvoiceCreator()
private val reader = EInvoiceReader()
private val validator = EInvoiceValidator()
fun createXRechnung(invoice: Invoice): String =
creator.createXRechnungXml(invoice)
fun createFacturXXml(invoice: Invoice): String =
creator.createFacturXXml(invoice)
fun createFacturXPdf(invoice: Invoice): Path {
val resultFile = createTempPdfFile()
creator.createFacturXPdf(invoice, resultFile.toFile())
return resultFile
}
fun attachInvoiceXmlToPdf(invoice: Invoice, pdf: Path): Path {
val resultFile = createTempPdfFile()
creator.attachInvoiceXmlToPdf(invoice, pdf.toFile(), resultFile.toFile())
return resultFile
}
fun extractInvoiceData(invoiceFile: Path) = when (invoiceFile.extension.lowercase()) {
"xml" -> reader.extractFromXml(invoiceFile.toFile())
"pdf" -> reader.extractFromPdf(invoiceFile.toFile())
else -> throw IllegalArgumentException("We can only extract eInvoice data from .xml and .pdf files")
}
fun validateInvoice(invoiceFile: Path) =when (invoiceFile.extension.lowercase()) {
"xml", "pdf" -> validator.validate(invoiceFile.toFile())
else -> throw IllegalArgumentException("We can only validate .xml and .pdf eInvoice files")
}
private fun createTempPdfFile(): Path =
File.createTempFile("factur-x", ".pdf")
.also { it.deleteOnExit() }
.toPath()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,99 @@
# HTTP config
%dev.quarkus.http.host=0.0.0.0
%dev.quarkus.http.port=8091
quarkus.http.enable-compression=true
quarkus.http.body.delete-uploaded-files-on-end=true
%dev.quarkus.http.cors=true
quarkus.http.cors.origins=*
quarkus.http.cors.headers=accept, authorization, content-type, x-requested-with
quarkus.http.cors.methods=GET, POST, PUT, DELETE, OPTIONS
# log request response times
quarkus.http.access-log.enabled=true
quarkus.http.record-request-start-time=true
# for all variables see: https://quarkus.io/guides/http-reference#configuring-http-access-logs
# %h Remote host name
# %s HTTP status code of the response
# %D Time taken to process the request, in millis
# %r First line of the request
# %b Bytes sent, excluding HTTP headers, or '-' if no bytes were sent
quarkus.http.access-log.pattern=%h %s %D ms "%r" %b bytes
# Logging
quarkus.log.console.async=true
quarkus.log.loki.host-url=http://loki.monitoring:3100
%dev.quarkus.log.loki.host-url=http://localhost:3100
%dev.quarkus.log.loki.enable=false
%test.quarkus.log.loki.enable=false
quarkus.log.loki.field.app.include=true
quarkus.log.loki.field.kubernetes.include=true
%dev.quarkus.log.loki.field.kubernetes.include=false
quarkus.log.loki.field.kubernetes.prefix=off
quarkus.log.loki.field.kubernetes.field.containername.include=false
# to turn off verbose FOP messages
quarkus.log.category."org.apache.fop.apps".level=ERROR
# to turn off verbose invoice validation logging
quarkus.log.category."org.mustangproject.validator".level=ERROR
# to fix that javax.xml.namespace.QName gets loaded with Quarkus' class loader instead of default class loader which causes an error
quarkus.class-loading.parent-first-artifacts=xpp3:xpp3
# Live reload
quarkus.live-reload.instrumentation=true
# disable this output:
# Press [h] for more options>
# Tests paused
# Press [r] to resume testing, [h] for more options>
# Press [r] to resume testing, [o] Toggle test output, [h] for more options>
quarkus.test.continuous-testing=disabled
quarkus.console.disable-input=true
# disable Analytics
quarkus.analytics.disabled=true
# Quarkus Native settings
quarkus.native.enable-https-url-handler=true
quarkus.native.enable-all-security-services=true
# Metrics and Health
# Kubernetes / Prometheus won't find endpoints under /q/health, /q/metrics, ..., so remove /q (= Quarkus default sub path) from path
quarkus.micrometer.export.prometheus.path=/metrics
quarkus.smallrye-health.root-path=/health
quarkus.smallrye-health.ui.always-include=true
# OpenAPI and Swagger-UI
# so that in Kubernetes Swagger-UI, Health UI, ... can be reached under /party-events (under /q/ it's not reachable via ingress)
quarkus.http.root-path=/invoicing
quarkus.http.non-application-root-path=${quarkus.http.root-path}
quarkus.swagger-ui.always-include=true
quarkus.swagger-ui.theme=flattop
quarkus.swagger-ui.display-request-duration=true
quarkus.smallrye-openapi.info-title=eInvoicing
quarkus.smallrye-openapi.info-version=1.0.0
quarkus.smallrye-openapi.info-description=API to create an read eInvoices according to EU standard EN 16931
quarkus.smallrye-openapi.info-contact-email=dev@codinux.net

View File

@ -0,0 +1,6 @@
package net.codinux.invoicing.api
import io.quarkus.test.junit.QuarkusIntegrationTest
@QuarkusIntegrationTest
class InvoicingResourceIT : InvoicingResourceTest()

View File

@ -0,0 +1,20 @@
package net.codinux.invoicing.api
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured.given
import org.hamcrest.CoreMatchers.`is`
import org.junit.jupiter.api.Test
@QuarkusTest
class InvoicingResourceTest {
@Test
fun testHelloEndpoint() {
given()
.`when`().get("/hello")
.then()
.statusCode(200)
.body(`is`("Hello from Quarkus REST"))
}
}

View File

@ -30,12 +30,12 @@ class EInvoiceConverter {
/**
* Converts a CII (Cross Industry Invoice) invoice, e.g. a Zugferd or Factur-X invoice, to UBL (Universal Business Language).
* Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language).
*/
fun convertCiiToUbl(invoice: Invoice) = convertCiiToUbl(createXRechnungXml(invoice))
/**
* Converts a CII (Cross Industry Invoice) invoice, e.g. a Zugferd or Factur-X invoice, to UBL (Universal Business Language).
* Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language).
*/
fun convertCiiToUbl(invoiceXml: String): String {
// TODO: extract a common method for this
@ -54,7 +54,7 @@ class EInvoiceConverter {
}
/**
* Converts a CII (Cross Industry Invoice) invoice, e.g. a Zugferd or Factur-X invoice, to UBL (Universal Business Language).
* Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language).
*/
fun convertCiiToUbl(xmlFile: File, outputFile: File) {
val cii2Ubl = CIIToUBL()

View File

@ -16,15 +16,26 @@ class EInvoiceCreator(
return createXml(provider, invoice)
}
fun createZugferdXml(invoice: Invoice): String {
/**
* Synonym for [createFacturXXml] (ZUGFeRD 2 is a synonym for Factur-X).
*/
fun createZugferdXml(invoice: Invoice) = createFacturXXml(invoice)
fun createFacturXXml(invoice: Invoice): String {
val exporter = ZUGFeRDExporterFromA3()
.setProfile("EN16931") // required for XML?
return createXml(exporter.provider, invoice)
}
fun createZugferdPdf(invoice: Invoice, outputFile: File) {
val xml = createZugferdXml(invoice)
/**
* Synonym for [createFacturXPdf] (ZUGFeRD 2 is a synonym for Factur-X).
*/
fun createZugferdPdf(invoice: Invoice, outputFile: File) = createFacturXPdf(invoice, outputFile)
fun createFacturXPdf(invoice: Invoice, outputFile: File) {
val xml = createFacturXXml(invoice)
val xmlFile = File.createTempFile(outputFile.nameWithoutExtension, ".xml")
.also { it.writeText(xml) }
val pdfFile = File(xmlFile.parentFile, xmlFile.nameWithoutExtension + ".pdf")
@ -32,16 +43,17 @@ class EInvoiceCreator(
val visualizer = ZUGFeRDVisualizer()
visualizer.toPDF(xmlFile.absolutePath, pdfFile.absolutePath)
combinePdfAndInvoiceXml(xml, pdfFile, outputFile)
attachInvoiceXmlToPdf(xml, pdfFile, outputFile)
xmlFile.delete()
pdfFile.delete()
}
fun combinePdfAndInvoiceXml(invoice: Invoice, pdfFile: File, outputFile: File) =
combinePdfAndInvoiceXml(createZugferdXml(invoice), pdfFile, outputFile)
fun combinePdfAndInvoiceXml(invoiceXml: String, pdfFile: File, outputFile: File) {
fun attachInvoiceXmlToPdf(invoice: Invoice, pdfFile: File, outputFile: File) =
attachInvoiceXmlToPdf(createFacturXXml(invoice), pdfFile, outputFile)
fun attachInvoiceXmlToPdf(invoiceXml: String, pdfFile: File, outputFile: File) {
val exporter = ZUGFeRDExporterFromA3()
.setZUGFeRDVersion(2)
.setProfile("EN16931") // available values: MINIMUM, BASICWL, BASIC, CIUS, EN16931, EXTENDED, XRECHNUNG

View File

@ -37,7 +37,7 @@ class FilesystemInvoiceReader(
if (extension == "pdf") {
eInvoiceReader.extractFromPdf(file.inputStream())
} else if (extension == "xml") {
eInvoiceReader.readFromXml(file.inputStream())
eInvoiceReader.extractFromXml(file.inputStream())
} else {
null
}

View File

@ -108,7 +108,7 @@ class MailReader(
if (filename.endsWith(".pdf") || contentType.startsWith("application/pdf") || contentType.startsWith("application/octet-stream")) {
eInvoiceReader.extractFromPdf(part.inputStream)
} else if (filename.endsWith(".xml") || contentType.startsWith("application/xml") || contentType.startsWith("text/xml")) {
eInvoiceReader.readFromXml(part.inputStream)
eInvoiceReader.extractFromXml(part.inputStream)
} else {
null
}

View File

@ -10,11 +10,11 @@ class EInvoiceReader(
private val mapper: MustangMapper = MustangMapper()
) {
fun readFromXml(xmlFile: File) = readFromXml(xmlFile.inputStream())
fun extractFromXml(xmlFile: File) = extractFromXml(xmlFile.inputStream())
fun readFromXml(stream: InputStream) = readFromXml(stream.reader().readText())
fun extractFromXml(stream: InputStream) = extractFromXml(stream.reader().readText())
fun readFromXml(xml: String): Invoice {
fun extractFromXml(xml: String): Invoice {
val importer = ZUGFeRDInvoiceImporter() // XRechnungImporter only reads properties but not to a Invoice object
importer.fromXML(xml)

View File

@ -17,11 +17,11 @@ class Demonstration {
fun read() {
val reader = EInvoiceReader()
// read a ZUGFeRD or Factor-X PDF that contains eInvoice XML as attachment
// extract invoice data from a ZUGFeRD or Factor-X PDF that contains eInvoice XML as attachment
val invoiceFromPDF = reader.extractFromPdf(File("ZUGFeRD.pdf"))
// read a eInvoice XML file like XRechnung:
val invoiceFromXml = reader.readFromXml(File("XRechnung.xml"))
// extract eInvoice data from a XML file like XRechnung:
val invoiceFromXml = reader.extractFromXml(File("XRechnung.xml"))
}
fun fromMail() {
@ -53,28 +53,28 @@ class Demonstration {
val creator = EInvoiceCreator()
// create a PDF that also contains the eInvoice as XML attachment
creator.createZugferdPdf(invoice, pdfResultFile)
creator.createFacturXPdf(invoice, pdfResultFile)
// create only the XML file
val xml = creator.createZugferdXml(invoice)
val xml = creator.createFacturXXml(invoice)
// create a XRechnung
val xRechnung = creator.createXRechnungXml(invoice)
}
fun combinePdfAndInvoiceXml() {
fun attachInvoiceXmlToPdf() {
val invoice: Invoice = createInvoice()
val existingPdf = File("Invoice.pdf")
val output = File("Zugferd.pdf")
val creator = EInvoiceCreator()
creator.combinePdfAndInvoiceXml(invoice, existingPdf, output)
creator.attachInvoiceXmlToPdf(invoice, existingPdf, output)
// or if you already have the invoice XML:
val invoiceXml: String = "..." // e.g. creator.createZugferdXml(invoice)
creator.combinePdfAndInvoiceXml(invoiceXml, existingPdf, output)
creator.attachInvoiceXmlToPdf(invoiceXml, existingPdf, output)
}

View File

@ -21,20 +21,20 @@ class EInvoiceCreatorTest {
}
@Test
fun createZugferdXml() {
fun createFacturXXml() {
val invoice = createInvoice()
val result = underTest.createZugferdXml(invoice)
val result = underTest.createFacturXXml(invoice)
assertInvoiceXml(result)
}
@Test
fun createZugferdPdf() {
fun createFacturXPdf() {
val invoice = createInvoice()
val testFile = File.createTempFile("Zugferd", ".pdf")
underTest.createZugferdPdf(invoice, testFile)
underTest.createFacturXPdf(invoice, testFile)
val importer = ZUGFeRDInvoiceImporter(testFile.inputStream())
val xml = String(importer.rawXML, Charsets.UTF_8)

View File

@ -11,8 +11,8 @@ class EInvoiceReaderTest {
@Test
fun readFromXml() {
val result = underTest.readFromXml(getTestFile("XRechnung.xml"))
fun extractFromXml() {
val result = underTest.extractFromXml(getTestFile("XRechnung.xml"))
assertInvoice(result)
}

View File

@ -3,13 +3,19 @@ kotlin.code.style=official
org.gradle.parallel=true
kotlinVersion=1.9.25
# Quarkus 3.12 requires Kotlin 2.0
#kotlinVersion=1.9.25
kotlinVersion=2.0.20
quarkusVersion=3.16.3
mustangVersion=2.14.2
angusMailVersion=2.0.3
klfVersion=1.6.2
lokiLogAppenderVersion=0.5.5
# only used for tests
logbackVersion=1.5.12

View File

@ -1,5 +1,6 @@
pluginManagement {
val kotlinVersion: String by settings
val quarkusVersion: String by settings
repositories {
@ -12,6 +13,8 @@ pluginManagement {
kotlin("plugin.allopen") version kotlinVersion
kotlin("plugin.noarg") version kotlinVersion
id("io.quarkus") version quarkusVersion
}
}
@ -24,3 +27,5 @@ plugins {
rootProject.name = "eInvoicing"
include("e-invoicing-domain")
include("e-invoice-api")