Compare commits
16 Commits
a29f3ce498
...
f9db232fbc
Author | SHA1 | Date |
---|---|---|
dankito | f9db232fbc | |
dankito | cd144e1f9d | |
dankito | 06f5da1544 | |
dankito | af1b49859a | |
dankito | c5f7e04fed | |
dankito | 67c58db9d7 | |
dankito | 2b39bb8682 | |
dankito | 9e3753f9c6 | |
dankito | e8e773c59b | |
dankito | f37ee036f0 | |
dankito | 2d42e58d0f | |
dankito | 5395880429 | |
dankito | d879ba74d8 | |
dankito | 07007ad619 | |
dankito | 8aae8fc3d2 | |
dankito | 6a9f5d7e2e |
17
README.md
17
README.md
|
@ -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)
|
||||
```
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
*
|
||||
!build/*-runner
|
||||
!build/*-runner.jar
|
||||
!build/lib/*
|
||||
!build/quarkus-app/*
|
|
@ -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")
|
||||
}
|
|
@ -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" ]
|
||||
|
|
@ -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"]
|
|
@ -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"]
|
|
@ -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()
|
||||
|
||||
}
|
|
@ -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 |
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
package net.codinux.invoicing.api
|
||||
|
||||
import io.quarkus.test.junit.QuarkusIntegrationTest
|
||||
|
||||
@QuarkusIntegrationTest
|
||||
class InvoicingResourceIT : InvoicingResourceTest()
|
|
@ -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"))
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue