From 3273625f7a13b442fb761c1d96d59a48f0634752 Mon Sep 17 00:00:00 2001 From: dankito Date: Wed, 13 Nov 2024 19:58:06 +0100 Subject: [PATCH] Implemented creating XRechnung --- e-invoicing-domain/build.gradle.kts | 31 ++++++++++ .../invoicing/creation/EInvoiceCreator.kt | 19 +++++++ .../invoicing/creation/MustangMapper.kt | 57 +++++++++++++++++++ .../net/codinux/invoicing/model/Invoice.kt | 14 +++++ .../net/codinux/invoicing/model/LineItem.kt | 14 +++++ .../net/codinux/invoicing/model/Party.kt | 19 +++++++ .../invoicing/creation/EInvoiceCreatorTest.kt | 56 ++++++++++++++++++ gradle.properties | 2 +- settings.gradle.kts | 1 + 9 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 e-invoicing-domain/build.gradle.kts create mode 100644 e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/creation/EInvoiceCreator.kt create mode 100644 e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/creation/MustangMapper.kt create mode 100644 e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/Invoice.kt create mode 100644 e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/LineItem.kt create mode 100644 e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/Party.kt create mode 100644 e-invoicing-domain/src/test/kotlin/net/codinux/invoicing/creation/EInvoiceCreatorTest.kt diff --git a/e-invoicing-domain/build.gradle.kts b/e-invoicing-domain/build.gradle.kts new file mode 100644 index 0000000..4b9ee34 --- /dev/null +++ b/e-invoicing-domain/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + kotlin("jvm") +} + + +kotlin { + jvmToolchain(11) +} + + +val mustangVersion: String by project + +val klfVersion: String by project + +val assertKVersion: String by project + +dependencies { + implementation("org.mustangproject:library:$mustangVersion") + + implementation("net.codinux.log:klf:$klfVersion") + + + testImplementation(kotlin("test")) + + implementation("com.willowtreeapps.assertk:assertk:$assertKVersion") +} + + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/creation/EInvoiceCreator.kt b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/creation/EInvoiceCreator.kt new file mode 100644 index 0000000..dc1bfa4 --- /dev/null +++ b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/creation/EInvoiceCreator.kt @@ -0,0 +1,19 @@ +package net.codinux.invoicing.creation + +import net.codinux.invoicing.model.Invoice +import org.mustangproject.ZUGFeRD.Profiles +import org.mustangproject.ZUGFeRD.ZUGFeRD2PullProvider + +class EInvoiceCreator( + private val mapper: MustangMapper = MustangMapper() +) { + + fun createXRechnungXml(invoice: Invoice): String { + val provider = ZUGFeRD2PullProvider() + provider.profile = Profiles.getByName("XRechnung") + provider.generateXML(mapper.mapToTransaction(invoice)) + + return String(provider.xml, Charsets.UTF_8) + } + +} \ No newline at end of file diff --git a/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/creation/MustangMapper.kt b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/creation/MustangMapper.kt new file mode 100644 index 0000000..0421d38 --- /dev/null +++ b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/creation/MustangMapper.kt @@ -0,0 +1,57 @@ +package net.codinux.invoicing.creation + +import net.codinux.invoicing.model.LineItem +import org.mustangproject.Invoice +import org.mustangproject.Item +import org.mustangproject.Product +import org.mustangproject.TradeParty +import org.mustangproject.ZUGFeRD.IExportableTransaction +import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.util.* + +class MustangMapper { + + fun mapToTransaction(invoice: net.codinux.invoicing.model.Invoice): IExportableTransaction = Invoice().apply { + this.number = invoice.invoiceNumber + this.issueDate = map(invoice.invoicingDate) + this.sender = mapParty(invoice.sender) + this.recipient = mapParty(invoice.recipient) + + this.setZFItems(ArrayList(invoice.items.map { mapLineItem(it) })) + + this.dueDate = map(invoice.dueDate) + } + + fun mapParty(party: net.codinux.invoicing.model.Party): TradeParty = TradeParty( + party.name, "${party.streetName} ${party.houseNumber}", party.postalCode, party.city, party.country + ).apply { + this.taxID = party.taxNumber + // TODO: vatID? + // TODO: ID? + // TODO: description? + + this.email = party.email + } + + fun mapLineItem(item: LineItem): IZUGFeRDExportableItem = Item( + // description has to be an empty string if not set + Product(item.name, item.description ?: "", item.unit, item.vatPercentage), item.price, item.quantity + ).apply { + + } + + + @JvmName("mapNullable") + private fun map(date: LocalDate?) = + date?.let { map(it) } + + private fun map(date: LocalDate): Date = + Date.from(mapToInstant(date)) + + private fun mapToInstant(date: LocalDate): Instant = + date.atStartOfDay(ZoneId.systemDefault()).toInstant() + +} \ No newline at end of file diff --git a/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/Invoice.kt b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/Invoice.kt new file mode 100644 index 0000000..6e262df --- /dev/null +++ b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/Invoice.kt @@ -0,0 +1,14 @@ +package net.codinux.invoicing.model + +import java.time.LocalDate + +class Invoice( + val invoiceNumber: String, + val invoicingDate: LocalDate, + val sender: Party, + val recipient: Party, + val items: List, + val dueDate: LocalDate? = null, +) { + override fun toString() = "$invoicingDate $invoiceNumber to $recipient" +} \ No newline at end of file diff --git a/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/LineItem.kt b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/LineItem.kt new file mode 100644 index 0000000..29f0342 --- /dev/null +++ b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/LineItem.kt @@ -0,0 +1,14 @@ +package net.codinux.invoicing.model + +import java.math.BigDecimal + +class LineItem( + val name: String, + val unit: String, + val quantity: BigDecimal, + val price: BigDecimal, + val vatPercentage: BigDecimal, + val description: String? = null, +) { + override fun toString() = "$name, $quantity x $price, $vatPercentage %" +} \ No newline at end of file diff --git a/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/Party.kt b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/Party.kt new file mode 100644 index 0000000..e260fb5 --- /dev/null +++ b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/model/Party.kt @@ -0,0 +1,19 @@ +package net.codinux.invoicing.model + +class Party( + val name: String, + + val streetName: String, + val houseNumber: String, + var postalCode: String?, + val city: String, + val country: String? = null, + + val taxNumber: String? = null, // better name like vatTaxNumber? + + val email: String? = null, +// var telephoneNumber: String? = null, // simply telephone? +// var website: String? = null, +) { + override fun toString() = "$name, $city" +} \ No newline at end of file diff --git a/e-invoicing-domain/src/test/kotlin/net/codinux/invoicing/creation/EInvoiceCreatorTest.kt b/e-invoicing-domain/src/test/kotlin/net/codinux/invoicing/creation/EInvoiceCreatorTest.kt new file mode 100644 index 0000000..7a75612 --- /dev/null +++ b/e-invoicing-domain/src/test/kotlin/net/codinux/invoicing/creation/EInvoiceCreatorTest.kt @@ -0,0 +1,56 @@ +package net.codinux.invoicing.creation + +import assertk.assertThat +import assertk.assertions.isNotEmpty +import net.codinux.invoicing.model.Invoice +import net.codinux.invoicing.model.LineItem +import net.codinux.invoicing.model.Party +import java.math.BigDecimal +import java.time.LocalDate +import kotlin.test.Test + +class EInvoiceCreatorTest { + + private val underTest = EInvoiceCreator() + + + @Test + fun createXRechnung() { + val invoice = createInvoice() + + val result = underTest.createXRechnungXml(invoice) + + assertThat(result).isNotEmpty() + } + + + private fun createInvoice( + invoiceNumber: String = "12345", + invoicingDate: LocalDate = LocalDate.of(2015, 10, 21), + sender: Party = createParty("Hochwürdiger Leistungserbringer"), + recipient: Party = createParty("Untertänigster Leistungsempfänger"), + items: List = listOf(createItem()), + dueDate: LocalDate? = null + ) = Invoice(invoiceNumber, invoicingDate, sender, recipient, items, dueDate) + + private fun createParty( + name: String, + streetName: String = "Fun Street", + houseNumber: String = "1", + postalCode: String = "12345", + city: String = "Glückstadt", + country: String? = null, + taxNumber: String? = "DE12345678", + email: String? = null, + ) = Party(name, streetName, houseNumber, postalCode, city, country, taxNumber, email) + + private fun createItem( + name: String = "Erbrachte Dienstleistungen", + unit: String = "", + quantity: BigDecimal = BigDecimal(1), + price: BigDecimal = BigDecimal(99), + vatPercentage: BigDecimal = BigDecimal(0.19), + description: String? = null, + ) = LineItem(name, unit, quantity, price, vatPercentage, description) + +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index efc0412..5099174 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.parallel=true kotlinVersion=1.9.25 -coroutinesVersion=1.8.1 +mustangVersion=2.14.2 klfVersion=1.6.2 diff --git a/settings.gradle.kts b/settings.gradle.kts index b89017a..a80867c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,3 +23,4 @@ plugins { rootProject.name = "eInvoicing" +include("e-invoicing-domain")