Compare commits

...

9 Commits

18 changed files with 448 additions and 110 deletions

View File

@ -32,7 +32,7 @@ val fetchResult = emailsFetcher.fetchAllEmails(EmailAccount(
)) ))
fetchResult.emails.forEach { email -> fetchResult.emails.forEach { email ->
println("${email.sender}: ${email.attachments.firstNotNullOfOrNull { it.invoice }?.totalAmounts?.duePayableAmount}") println("${email.sender}: ${email.attachments.firstNotNullOfOrNull { it.invoice }?.totals?.duePayableAmount}")
} }
``` ```
@ -68,10 +68,9 @@ fun create() {
} }
private fun createInvoice() = Invoice( private fun createInvoice() = Invoice(
invoiceNumber = "RE-00001", details = InvoiceDetails("RE-00001", LocalDate.now()),
invoicingDate = LocalDate.now(), supplier = Party("codinux GmbH & Co. KG", "Fun Street 1", null, "12345", "Glückstadt"),
sender = Party("codinux GmbH & Co. KG", "Fun Street 1", "12345", "Glückstadt"), customer = Party("Abzock GmbH", "Ausbeutstr.", null, "12345", "Abzockhausen"),
recipient = Party("Abzock GmbH", "Ausbeutstr.", "12345", "Abzockhausen"),
items = listOf(InvoiceItem("Erbrachte Dienstleistungen", BigDecimal(170), "HUR", BigDecimal(105), BigDecimal(19))) // HUR = EN code for hour items = listOf(InvoiceItem("Erbrachte Dienstleistungen", BigDecimal(170), "HUR", BigDecimal(105), BigDecimal(19))) // HUR = EN code for hour
) )
``` ```

160
docs/Translations.md Normal file
View File

@ -0,0 +1,160 @@
- **Rechnungsaussteller / Rechnungsersteller / Leistungserbringer** Invoice issuer / Invoice creator / Service provider
- **Empfänger der Rechnung / Auftraggeber / Leistungsempfänger** Invoice recipient / Client / Service recipient
- **Rechnungsnummer** Invoice number
- **Rechnungsdatum** Invoice date
- **Fälligkeitsdatum** Due date
- **Auftragsnummer** Order number
- **Auftragsdatum** Order date
- **Kundennummer** Customer number
- **Kontaktperson / Kontaktadresse** Contact person / Contact address
- **Lieferschein** Delivery note
- **Lieferdatum** Delivery date
**Leitweg-ID**: Routing ID
**Regierungsstellen / Regierungsbehörden**: government agencies (/ government authorities)
JetBrains Rechnung:
```text
Invoice Details:
Reference number: INVCZ7424912
Order reference: R20162736
Tax point date: 8.3.2024
Issue date: 8.3.2024
Due date: 8.3.2024
Paid via: Credit Card
Payment Date: 8.3.2024
Transaction: VZ6HHRG7V2KJFXB2
Bill To:
Christian Dankl
Kürnbergstr. 36
81369 München
Germany
VAT ID: DE306521719
Row 1:
Customer Id
1983447
Order Date
8.3.2024
Shipped Electronically To
mammon@dankito.de
Row 2:
Part Number
P-S.ALL-Y-40C
<Invoice item name>
All Products Pack
Product Description
Personal annual subscription with
40% continuity discount
Valid from 15.3.2024 through
14.3.2025
Price
173.00
Qty
1
Extended Price
173.00 EUR
Subtotal: 173.00 EUR
VAT Rate*: 0.00%
VAT Amount: 0.00 EUR
Total: 173.00 EUR
PAID: 173.00 EUR
```
Rechnungsaussteller / Leistungserbringer:
- **CII**: Seller / Supplier
- **UBL**: Supplier
Rechnungsempfänger / Auftraggeber / Leistungsempfänger:
- **CII**: Buyer / Customer
- **UBL**: Buyer / Customer
1. Rechnungsaussteller (die Partei, die die Rechnung stellt):
Seller Das ist der häufigste Begriff und bezieht sich auf die Partei, die die Waren oder Produkte verkauft und die Rechnung ausstellt.
Supplier Wird ebenfalls oft verwendet, besonders wenn der Fokus auf der Lieferung von Waren liegt.
Vendor Ein weiterer sehr gebräuchlicher Begriff, vor allem im Einzelhandel und im E-Commerce.
2. Rechnungsempfänger (die Partei, die die Rechnung erhält und bezahlt):
Buyer Der Käufer, die Partei, die die Ware oder Dienstleistung erworben hat und die Rechnung erhält.
Customer Ein sehr allgemeiner Begriff, der häufig für alle Arten von Käufern verwendet wird, sowohl in B2B als auch in B2C.
Purchaser Wird ebenfalls verwendet, besonders in formelleren oder vertraglichen Kontexten.
Die am häufigsten verwendeten Begriffe in Rechnungs- und Geschäftsprozessen sind:
### Für denjenigen, der die Rechnung ausstellt bzw. die Leistung erbracht hat:
- **Service provider** Dies ist der am häufigsten verwendete Begriff, insbesondere in Dienstleistungsbranchen. Der „Service provider“ ist derjenige, der eine Dienstleistung erbringt und die Rechnung stellt.
- **Invoice issuer** oder **Invoice creator** sind zwar korrekt, aber weniger gebräuchlich und klingen formeller oder spezifischer, insbesondere im Kontext von Rechnungsstellung und Buchhaltung. **Service provider** ist der allgemeinere und häufigste Begriff, wenn man von der Leistungserbringung spricht.
### Für denjenigen, der die Rechnung erhält:
- **Client** Dies ist der am häufigsten verwendete Begriff, besonders in geschäftlichen und dienstleistungsorientierten Kontexten. Der „Client“ ist die Person oder Organisation, die die Leistung in Anspruch nimmt und die Rechnung bezahlt.
- **Invoice recipient** ist ebenfalls korrekt, wird aber weniger häufig verwendet, da es eine sehr formale Ausdrucksweise ist.
- **Service recipient** ist auch korrekt, wird aber eher in formelleren oder spezialisierten Kontexten verwendet, z.B. in Verträgen, bei denen die Erbringung einer spezifischen Dienstleistung betont wird.
### Zusammengefasst:
- **Für denjenigen, der die Rechnung ausstellt:** **Service provider**
- **Für denjenigen, der die Rechnung erhält:** **Client**
## Client vs. Customer
Der Unterschied zwischen **Customer** und **Client** im Kontext von CII/UBL und allgemeiner Geschäftsbeziehung ist subtil, aber dennoch wichtig. Die Begriffe haben unterschiedliche Konnotationen, abhängig von der Art der Transaktion und dem zugrunde liegenden Geschäftsmodell.
### **1. Customer (CII / UBL)**
- **Kontext**: Der Begriff **Customer** ist weit verbreitet und bezieht sich auf eine Person oder Organisation, die **Waren** oder **Dienstleistungen** kauft. In den **CII** und **UBL** XML-Formaten wird der Begriff "Customer" verwendet, um den Empfänger einer Rechnung zu bezeichnen also die Partei, die das Produkt oder die Dienstleistung **erwirbt** und **bezahlt**.
- **Verwendung**:
- **B2C (Business-to-Consumer)**: Der Begriff **Customer** wird insbesondere in B2C-Transaktionen verwendet, also bei der Interaktion zwischen einem Unternehmen und einem Endverbraucher.
- **B2B (Business-to-Business)**: Auch im B2B-Kontext wird der Begriff oft verwendet, wenn eine Firma Waren oder Dienstleistungen an eine andere Firma verkauft.
- **Beispiel**: Ein Online-Shop verkauft ein Produkt an einen Endverbraucher. Der Endverbraucher ist der **Customer** des Shops.
### **2. Client (Allgemein)**
- **Kontext**: Der Begriff **Client** ist etwas **formeller** und wird häufiger in bestimmten **Dienstleistungsbranchen** oder in längerfristigen **geschäftlichen Beziehungen** verwendet, wo eine kontinuierliche Betreuung oder ein fortlaufender Service erforderlich ist. Er bezeichnet häufig eine Person oder Organisation, die **eine Dienstleistung in Anspruch nimmt** und häufig eine engere, individuellere Geschäftsbeziehung pflegt.
- **Verwendung**:
- **Dienstleistungsbranche**: **Client** wird vor allem in Bereichen wie **Beratung, Finanzdienstleistungen, Recht, IT-Dienstleistungen** und anderen beratungsintensiven Branchen verwendet.
- **Längerfristige Beziehungen**: Der Begriff wird auch verwendet, wenn es eine **langfristige Geschäftsbeziehung** gibt, wie z.B. bei regelmäßigen Verträgen oder individuellen Dienstleistungen.
- **Beispiel**: Ein Beratungsunternehmen stellt einem Unternehmen eine Rechnung für eine langfristige Dienstleistung. Das Unternehmen ist der **Client** des Beratungsunternehmens.
### **Wichtige Unterschiede**:
- **Customer** ist eher allgemein und bezieht sich auf **jeden Käufer** von Waren oder Dienstleistungen, egal ob in einem **einmaligen Kauf** oder einer **langfristigen Beziehung**. Der Begriff ist besonders in **B2C** und **Einzelhandelskontexten** verbreitet.
- **Client** wird eher in **dienstleistungsorientierten** und **langfristigen** Geschäftsbeziehungen verwendet und suggeriert eine **individuellere, oft persönlichere Beziehung**. Es geht um eine **Dienstleistung** oder **Beratung**, die über einen längeren Zeitraum erfolgt und typischerweise mit einem **Vertrag** oder einer regelmäßigen Interaktion verbunden ist.
### Fazit:
- **Customer** (CII/UBL) ist der häufigere und allgemeinere Begriff für den Empfänger einer Rechnung in den meisten **Warenkauf**-Szenarien und auch in **B2C**-Transaktionen.
- **Client** wird häufig in einem **Dienstleistungszusammenhang** verwendet, besonders wenn es um langfristige Beziehungen oder maßgeschneiderte Dienstleistungen geht.
Im Kontext von **CII** und **UBL**, die häufig für **Produktkäufe** und **Warenlieferungen** verwendet werden, ist **Customer** also der passendere Begriff, während **Client** in einem Geschäfts- oder Beratungsdienstleistungsumfeld gebräuchlicher wäre.

View File

@ -94,7 +94,7 @@ class InvoicingResource(
private fun createPdfFileResponse(pdfFile: java.nio.file.Path, invoice: Invoice): Response = private fun createPdfFileResponse(pdfFile: java.nio.file.Path, invoice: Invoice): Response =
Response.ok(pdfFile) Response.ok(pdfFile)
.header("Content-Disposition", "attachment;filename=\"${invoice.invoicingDate.toString().replace('-', '.')} ${invoice.recipient.name} ${invoice.invoiceNumber}.pdf\"") .header("Content-Disposition", "attachment;filename=\"${invoice.details.invoiceDate.toString().replace('-', '.')} ${invoice.customer.name} ${invoice.details.invoiceNumber}.pdf\"")
.build() .build()
} }

View File

@ -50,7 +50,7 @@ open class EmailsFetcher(
open fun checkCredentials(account: EmailAccount): CheckCredentialsResult { open fun checkCredentials(account: EmailAccount): CheckCredentialsResult {
try { try {
val status = connect(account, FetchEmailsOptions(showDebugOutputOnConsole = true)) val status = connect(account, FetchEmailsOptions())
close(status) close(status)
@ -154,7 +154,7 @@ open class EmailsFetcher(
// executed, making the overall process very slow -> use FetchProfile to prefetch requested data with a single request // executed, making the overall process very slow -> use FetchProfile to prefetch requested data with a single request
folder.fetch(messages, getFetchProfile(status)) folder.fetch(messages, getFetchProfile(status))
messages.mapNotNull { message -> messages.reversed().mapNotNull { message ->
async(coroutineDispatcher) { async(coroutineDispatcher) {
try { try {
getEmail(message, status) getEmail(message, status)

View File

@ -1,11 +1,10 @@
package net.codinux.invoicing.mapper package net.codinux.invoicing.mapper
import net.codinux.invoicing.calculator.AmountsCalculator import net.codinux.invoicing.calculator.AmountsCalculator
import net.codinux.invoicing.model.AmountAdjustments import net.codinux.invoicing.model.*
import net.codinux.invoicing.model.ChargeOrAllowance
import net.codinux.invoicing.model.InvoiceItem
import net.codinux.invoicing.model.Party
import org.mustangproject.* import org.mustangproject.*
import org.mustangproject.BankDetails
import org.mustangproject.Invoice
import org.mustangproject.ZUGFeRD.IExportableTransaction import org.mustangproject.ZUGFeRD.IExportableTransaction
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem
import java.math.BigDecimal import java.math.BigDecimal
@ -19,17 +18,17 @@ open class MustangMapper(
) { ) {
open fun mapToTransaction(invoice: net.codinux.invoicing.model.Invoice): IExportableTransaction = Invoice().apply { open fun mapToTransaction(invoice: net.codinux.invoicing.model.Invoice): IExportableTransaction = Invoice().apply {
this.number = invoice.invoiceNumber this.number = invoice.details.invoiceNumber
this.issueDate = map(invoice.invoicingDate) this.issueDate = map(invoice.details.invoiceDate)
this.sender = mapParty(invoice.sender) this.sender = mapParty(invoice.supplier)
this.recipient = mapParty(invoice.recipient) this.recipient = mapParty(invoice.customer)
this.setZFItems(ArrayList(invoice.items.map { mapLineItem(it) })) this.setZFItems(ArrayList(invoice.items.map { mapLineItem(it) }))
this.dueDate = map(invoice.dueDate) this.dueDate = map(invoice.details.dueDate)
this.paymentTermDescription = invoice.paymentDescription this.paymentTermDescription = invoice.details.paymentDescription
this.referenceNumber = invoice.buyerReference this.referenceNumber = invoice.customerReferenceNumber
invoice.amountAdjustments?.let { adjustments -> invoice.amountAdjustments?.let { adjustments ->
this.totalPrepaidAmount = adjustments.prepaidAmounts this.totalPrepaidAmount = adjustments.prepaidAmounts
@ -37,14 +36,16 @@ open class MustangMapper(
adjustments.allowances.forEach { this.addAllowance(mapAllowance(it)) } adjustments.allowances.forEach { this.addAllowance(mapAllowance(it)) }
} }
if (invoice.totalAmounts == null) { if (invoice.totals == null) {
invoice.totalAmounts = calculator.calculateTotalAmounts(this) invoice.totals = calculator.calculateTotalAmounts(this)
} }
} }
open fun mapParty(party: Party): TradeParty = TradeParty( open fun mapParty(party: Party): TradeParty = TradeParty(
party.name, party.street, party.postalCode, party.city, party.countryIsoCode party.name, party.address, party.postalCode, party.city, party.countryIsoCode
).apply { ).apply {
this.setAdditionalAddress(party.additionalAddressLine)
this.setVATID(party.vatId) this.setVATID(party.vatId)
// TODO: description? // TODO: description?
@ -63,7 +64,10 @@ open class MustangMapper(
open fun mapLineItem(item: InvoiceItem): IZUGFeRDExportableItem = Item( open fun mapLineItem(item: InvoiceItem): IZUGFeRDExportableItem = Item(
// description has to be an empty string if not set // description has to be an empty string if not set
Product(item.name, item.description ?: "", item.unit, item.vatRate), item.unitPrice, item.quantity Product(item.name, item.description ?: "", item.unit, item.vatRate).apply {
this.sellerAssignedID = item.articleNumber // TODO: what is the articleNumber? sellerAssignedId, globalId, ...?
},
item.unitPrice, item.quantity
).apply { ).apply {
} }
@ -90,30 +94,27 @@ open class MustangMapper(
open fun mapToInvoice(invoice: Invoice) = net.codinux.invoicing.model.Invoice( open fun mapToInvoice(invoice: Invoice) = net.codinux.invoicing.model.Invoice(
invoiceNumber = invoice.number, details = InvoiceDetails(invoice.number, map(invoice.issueDate), map(invoice.dueDate ?: invoice.paymentTerms?.dueDate), invoice.paymentTermDescription ?: invoice.paymentTerms?.description),
invoicingDate = map(invoice.issueDate),
sender = mapParty(invoice.sender), supplier = mapParty(invoice.sender),
recipient = mapParty(invoice.recipient), customer = mapParty(invoice.recipient),
items = invoice.zfItems.map { mapLineItem(it) }, items = invoice.zfItems.map { mapLineItem(it) },
dueDate = map(invoice.dueDate ?: invoice.paymentTerms?.dueDate), customerReferenceNumber = invoice.referenceNumber,
paymentDescription = invoice.paymentTermDescription ?: invoice.paymentTerms?.description,
buyerReference = invoice.referenceNumber,
amountAdjustments = mapAmountAdjustments(invoice), amountAdjustments = mapAmountAdjustments(invoice),
totalAmounts = calculator.calculateTotalAmounts(invoice) totals = calculator.calculateTotalAmounts(invoice)
) )
open fun mapParty(party: TradeParty) = Party( open fun mapParty(party: TradeParty) = Party(
party.name, party.street, party.zip, party.location, party.country, party.vatID, party.name, party.street, party.additionalAddress, party.zip, party.location, party.country, party.vatID,
party.email ?: party.contact?.eMail, party.contact?.phone, party.contact?.fax, party.contact?.name, party.email ?: party.contact?.eMail, party.contact?.phone, party.contact?.fax, party.contact?.name,
party.bankDetails?.firstOrNull()?.let { net.codinux.invoicing.model.BankDetails(it.iban, it.bic, it.accountName) } party.bankDetails?.firstOrNull()?.let { net.codinux.invoicing.model.BankDetails(it.iban, it.bic, it.accountName) }
) )
open fun mapLineItem(item: IZUGFeRDExportableItem) = InvoiceItem( open fun mapLineItem(item: IZUGFeRDExportableItem) = InvoiceItem(
item.product.name, item.quantity, item.product.unit, item.price, item.product.vatPercent, item.product.description.takeUnless { it.isBlank() } item.product.name, item.quantity, item.product.unit, item.price, item.product.vatPercent, item.product.sellerAssignedID, item.product.description.takeUnless { it.isBlank() }
) )
protected open fun mapAmountAdjustments(invoice: Invoice): AmountAdjustments? { protected open fun mapAmountAdjustments(invoice: Invoice): AmountAdjustments? {

View File

@ -1,21 +1,26 @@
package net.codinux.invoicing.model package net.codinux.invoicing.model
import java.time.LocalDate
class Invoice( class Invoice(
val invoiceNumber: String, val details: InvoiceDetails,
val invoicingDate: LocalDate, val supplier: Party,
val sender: Party, val customer: Party,
val recipient: Party,
val items: List<InvoiceItem>, val items: List<InvoiceItem>,
val dueDate: LocalDate? = null,
val paymentDescription: String? = null,
/** /**
* Unique reference number of the buyer, e.g. the Leitweg-ID required by German authorities (Behörden) * An identifier assigned by the Buyer used for internal routing purposes.
*
* The identifier is defined by the Buyer (e.g. contact ID, department, office id, project code), but provided by the
* Seller in the Invoice.
*
* In XRechnung mandatory for invoices to government agencies in Germany (B2G and G2G) to set the Leitweg-ID here.
*
* From XRechnung specification:
* "Anmerkung: Im Rahmen des Steuerungsprojekts eRechnung ist mit der so genannten Leitweg-ID eine Zuord-
* nungsmöglichkeit entwickelt worden, deren verbindliche Nutzung von Bund und mehreren Ländern vorgegeben
* wird. Die Leitweg-ID ist prinzipiell für Bund, Länder und Kommunen einsetzbar (B2G, G2G). Für die Darstellung
* der Leitweg-ID wird das in XRechnung verpflichtende Feld Buyer Reference benutzt."
*/ */
val buyerReference: String? = null, val customerReferenceNumber: String? = null,
val amountAdjustments: AmountAdjustments? = null, val amountAdjustments: AmountAdjustments? = null,
@ -25,7 +30,7 @@ class Invoice(
* For outgoing invoices: You don't have to calculate them, we do this for you. This ensures that all total amounts * For outgoing invoices: You don't have to calculate them, we do this for you. This ensures that all total amounts
* are in accordance to other data of the invoice like the invoice item amounts and amount adjustments. * are in accordance to other data of the invoice like the invoice item amounts and amount adjustments.
*/ */
var totalAmounts: TotalAmounts? = null var totals: TotalAmounts? = null
) { ) {
override fun toString() = "$invoicingDate $invoiceNumber to $recipient ${totalAmounts?.duePayableAmount?.let { " (${it.toPlainString()})" } ?: ""}" override fun toString() = "$details to $customer ${totals?.duePayableAmount?.let { " (${it.toPlainString()})" } ?: ""}"
} }

View File

@ -0,0 +1,13 @@
package net.codinux.invoicing.model
import java.time.LocalDate
class InvoiceDetails(
val invoiceNumber: String,
val invoiceDate: LocalDate,
val dueDate: LocalDate? = null,
val paymentDescription: String? = null,
) {
override fun toString() = "$invoiceDate $invoiceNumber"
}

View File

@ -8,6 +8,7 @@ class InvoiceItem(
val unit: String, val unit: String,
val unitPrice: BigDecimal, val unitPrice: BigDecimal,
val vatRate: BigDecimal, val vatRate: BigDecimal,
val articleNumber: String? = null,
val description: String? = null, val description: String? = null,
) { ) {
override fun toString() = "$name, $quantity x $unitPrice, $vatRate %" override fun toString() = "$name, $quantity x $unitPrice, $vatRate %"

View File

@ -6,7 +6,8 @@ class Party(
/** /**
* Party's street and house number. * Party's street and house number.
*/ */
val street: String, val address: String,
val additionalAddressLine: String? = null,
var postalCode: String?, var postalCode: String?,
val city: String, val city: String,
/** /**
@ -24,6 +25,12 @@ class Party(
// actually there can be multiple bankDetails in eInvoice data model // actually there can be multiple bankDetails in eInvoice data model
val bankDetails: BankDetails? = null, val bankDetails: BankDetails? = null,
/**
* Currently only used to display the logo of the supplier in generated PDF. There is an element for it in Factur-X
* and XRechnung, but the underlying library doesn't map it.
*/
val logoUrl: String? = null,
) { ) {
override fun toString() = "$name, $city" override fun toString() = "$name, $city"
} }

View File

@ -3,10 +3,7 @@ package net.codinux.invoicing
import net.codinux.invoicing.creation.EInvoiceCreator import net.codinux.invoicing.creation.EInvoiceCreator
import net.codinux.invoicing.email.model.EmailAccount import net.codinux.invoicing.email.model.EmailAccount
import net.codinux.invoicing.email.EmailsFetcher import net.codinux.invoicing.email.EmailsFetcher
import net.codinux.invoicing.model.EInvoiceXmlFormat import net.codinux.invoicing.model.*
import net.codinux.invoicing.model.Invoice
import net.codinux.invoicing.model.InvoiceItem
import net.codinux.invoicing.model.Party
import net.codinux.invoicing.reader.EInvoiceReader import net.codinux.invoicing.reader.EInvoiceReader
import net.codinux.invoicing.validation.EInvoiceValidator import net.codinux.invoicing.validation.EInvoiceValidator
import java.io.File import java.io.File
@ -36,7 +33,7 @@ class Demonstration {
)) ))
fetchResult.emails.forEach { email -> fetchResult.emails.forEach { email ->
println("${email.sender}: ${email.attachments.firstNotNullOfOrNull { it.invoice }?.totalAmounts?.duePayableAmount}") println("${email.sender}: ${email.attachments.firstNotNullOfOrNull { it.invoice }?.totals?.duePayableAmount}")
} }
} }
@ -84,10 +81,9 @@ class Demonstration {
private fun createInvoice() = Invoice( private fun createInvoice() = Invoice(
invoiceNumber = "RE-00001", details = InvoiceDetails("RE-00001", LocalDate.now()),
invoicingDate = LocalDate.now(), supplier = Party("codinux GmbH & Co. KG", "Fun Street 1", null, "12345", "Glückstadt"),
sender = Party("codinux GmbH & Co. KG", "Fun Street 1", "12345", "Glückstadt"), customer = Party("Abzock GmbH", "Ausbeutstr.", null, "12345", "Abzockhausen"),
recipient = Party("Abzock GmbH", "Ausbeutstr.", "12345", "Abzockhausen"),
items = listOf(InvoiceItem("Erbrachte Dienstleistungen", BigDecimal(170), "HUR", BigDecimal(1_000_000), BigDecimal(19))) // HUR = EN code for hour items = listOf(InvoiceItem("Erbrachte Dienstleistungen", BigDecimal(170), "HUR", BigDecimal(1_000_000), BigDecimal(19))) // HUR = EN code for hour
) )
} }

View File

@ -1,9 +1,7 @@
package net.codinux.invoicing.test package net.codinux.invoicing.test
import net.codinux.invoicing.model.BankDetails import net.codinux.invoicing.calculator.AmountsCalculator
import net.codinux.invoicing.model.Invoice import net.codinux.invoicing.model.*
import net.codinux.invoicing.model.InvoiceItem
import net.codinux.invoicing.model.Party
import java.math.BigDecimal import java.math.BigDecimal
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -11,63 +9,70 @@ import java.time.format.DateTimeFormatter
object DataGenerator { object DataGenerator {
const val InvoiceNumber = "12345" const val InvoiceNumber = "12345"
val InvoicingDate = LocalDate.of(2015, 10, 21) val InvoiceDate = LocalDate.of(2015, 10, 21)
val DueDate = LocalDate.of(2016, 6, 15) val DueDate = LocalDate.of(2016, 6, 15)
const val SenderName = "Hochwürdiger Leistungserbringer" const val SupplierName = "Hochwürdiger Leistungserbringer"
const val SenderStreet = "Fun Street 1" const val SupplierAddress = "Fun Street 1"
const val SenderPostalCode = "12345" val SupplierAdditionalAddressLine: String? = null
const val SenderCity = "Glückstadt" const val SupplierPostalCode = "12345"
const val SenderCountry = "DE" const val SupplierCity = "Glückstadt"
const val SenderVatId = "DE123456789" const val SupplierCountry = "DE"
const val SenderEmail = "working-class-hero@rock.me" const val SupplierVatId = "DE123456789"
const val SenderPhone = "+4917012345678" const val SupplierEmail = "working-class-hero@rock.me"
val SenderBankDetails = BankDetails("DE00123456780987654321", "ABZODEFFXXX", "Manuela Musterfrau") const val SupplierPhone = "+4917012345678"
val SupplierFax: String? = null
val SupplierBankDetails = BankDetails("DE00123456780987654321", "ABZODEFFXXX", "Manuela Musterfrau", "Abzock-Bank")
const val RecipientName = "Untertänigster Leistungsempfänger" const val CustomerName = "Untertänigster Leistungsempfänger"
const val RecipientStreet = "Party Street 1" const val CustomerAddress = "Party Street 1"
const val RecipientPostalCode = SenderPostalCode val CustomerAdditionalAddressLine: String? = null
const val RecipientCity = SenderCity const val CustomerPostalCode = SupplierPostalCode
const val RecipientCountry = "DE" const val CustomerCity = SupplierCity
const val RecipientVatId = "DE987654321" const val CustomerCountry = "DE"
const val RecipientEmail = "exploiter@your.boss" const val CustomerVatId = "DE987654321"
const val RecipientPhone = "+491234567890" const val CustomerEmail = "exploiter@your.boss"
val RecipientBankDetails: BankDetails? = null const val CustomerPhone = "+491234567890"
val CustomerFax: String? = null
val CustomerBankDetails: BankDetails? = null
const val ItemName = "Erbrachte Dienstleistungen" const val ItemName = "Erbrachte Dienstleistungen"
val ItemQuantity = BigDecimal(1) val ItemQuantity = BigDecimal(1)
const val ItemUnit = "HUR" // EN code for 'hour' const val ItemUnit = "HUR" // EN code for 'hour'
val ItemUnitPrice = BigDecimal(99) val ItemUnitPrice = BigDecimal(99)
val ItemVatRate = BigDecimal(19) val ItemVatRate = BigDecimal(19)
val ItemArticleNumber: String? = null
val ItemDescription: String? = null val ItemDescription: String? = null
fun createInvoice( fun createInvoice(
invoiceNumber: String = InvoiceNumber, invoiceNumber: String = InvoiceNumber,
invoicingDate: LocalDate = InvoicingDate, invoiceDate: LocalDate = InvoiceDate,
sender: Party = createParty(SenderName, SenderStreet, SenderPostalCode, SenderCity, SenderCountry, SenderVatId, SenderEmail, SenderPhone, supplier: Party = createParty(SupplierName, SupplierAddress, SupplierAdditionalAddressLine, SupplierPostalCode, SupplierCity, SupplierCountry,
bankDetails = SenderBankDetails), SupplierVatId, SupplierEmail, SupplierPhone, SupplierFax, bankDetails = SupplierBankDetails),
recipient: Party = createParty(RecipientName, RecipientStreet, RecipientPostalCode, RecipientCity, RecipientCountry, RecipientVatId, RecipientEmail, RecipientPhone, customer: Party = createParty(CustomerName, CustomerAddress, CustomerAdditionalAddressLine, CustomerPostalCode, CustomerCity, CustomerCountry,
bankDetails = RecipientBankDetails), CustomerVatId, CustomerEmail, CustomerPhone, CustomerFax, bankDetails = CustomerBankDetails),
items: List<InvoiceItem> = listOf(createItem()), items: List<InvoiceItem> = listOf(createItem()),
dueDate: LocalDate? = DueDate, dueDate: LocalDate? = DueDate,
paymentDescription: String? = dueDate?.let { "Zahlbar ohne Abzug bis ${DateTimeFormatter.ofPattern("dd.MM.yyyy").format(dueDate)}" }, paymentDescription: String? = dueDate?.let { "Zahlbar ohne Abzug bis ${DateTimeFormatter.ofPattern("dd.MM.yyyy").format(dueDate)}" },
buyerReference: String? = null ) = Invoice(InvoiceDetails(invoiceNumber, invoiceDate, dueDate, paymentDescription), supplier, customer, items).apply {
) = Invoice(invoiceNumber, invoicingDate, sender, recipient, items, dueDate, paymentDescription, buyerReference) this.totals = AmountsCalculator().calculateTotalAmounts(this)
}
fun createParty( fun createParty(
name: String, name: String,
streetName: String = SenderStreet, address: String = SupplierAddress,
postalCode: String = SenderPostalCode, additionalAddressLine: String? = SupplierAdditionalAddressLine,
city: String = SenderCity, postalCode: String = SupplierPostalCode,
country: String? = SenderCountry, city: String = SupplierCity,
vatId: String? = SenderVatId, country: String? = SupplierCountry,
email: String? = SenderEmail, vatId: String? = SupplierVatId,
phone: String? = SenderPhone, email: String? = SupplierEmail,
fax: String? = null, phone: String? = SupplierPhone,
fax: String? = SupplierFax,
contactName: String? = null, contactName: String? = null,
bankDetails: BankDetails? = null bankDetails: BankDetails? = null
) = Party(name, streetName, postalCode, city, country, vatId, email, phone, fax, contactName, bankDetails) ) = Party(name, address, additionalAddressLine, postalCode, city, country, vatId, email, phone, fax, contactName, bankDetails)
fun createItem( fun createItem(
name: String = ItemName, name: String = ItemName,
@ -75,7 +80,8 @@ object DataGenerator {
unit: String = ItemUnit, unit: String = ItemUnit,
unitPrice: BigDecimal = ItemUnitPrice, unitPrice: BigDecimal = ItemUnitPrice,
vatRate: BigDecimal = ItemVatRate, vatRate: BigDecimal = ItemVatRate,
articleNumber: String? = ItemArticleNumber,
description: String? = ItemDescription, description: String? = ItemDescription,
) = InvoiceItem(name, quantity, unit, unitPrice, vatRate, description) ) = InvoiceItem(name, quantity, unit, unitPrice, vatRate, articleNumber, description)
} }

View File

@ -16,22 +16,22 @@ object InvoiceAsserter {
val asserter = XPathAsserter(xml) val asserter = XPathAsserter(xml)
asserter.xpathHasValue("//rsm:ExchangedDocument/ram:ID", DataGenerator.InvoiceNumber) asserter.xpathHasValue("//rsm:ExchangedDocument/ram:ID", DataGenerator.InvoiceNumber)
asserter.xpathHasValue("//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString", DataGenerator.InvoicingDate.toString().replace("-", "")) asserter.xpathHasValue("//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString", DataGenerator.InvoiceDate.toString().replace("-", ""))
val senderXPath = "//rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty" val supplierXPath = "//rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty"
assertParty(asserter, senderXPath, DataGenerator.SenderName, DataGenerator.SenderStreet, DataGenerator.SenderPostalCode, DataGenerator.SenderCity, DataGenerator.SenderVatId, DataGenerator.SenderEmail, DataGenerator.SenderPhone) assertParty(asserter, supplierXPath, DataGenerator.SupplierName, DataGenerator.SupplierAddress, DataGenerator.SupplierPostalCode, DataGenerator.SupplierCity, DataGenerator.SupplierVatId, DataGenerator.SupplierEmail, DataGenerator.SupplierPhone)
val receiverXPath = "//rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty" val customerXPath = "//rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty"
assertParty(asserter, receiverXPath, DataGenerator.RecipientName, DataGenerator.RecipientStreet, DataGenerator.RecipientPostalCode, DataGenerator.RecipientCity, DataGenerator.RecipientVatId, DataGenerator.RecipientEmail, DataGenerator.RecipientPhone) assertParty(asserter, customerXPath, DataGenerator.CustomerName, DataGenerator.CustomerAddress, DataGenerator.CustomerPostalCode, DataGenerator.CustomerCity, DataGenerator.CustomerVatId, DataGenerator.CustomerEmail, DataGenerator.CustomerPhone)
val lineItemXPath = "//rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem" val lineItemXPath = "//rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem"
assertLineItem(asserter, lineItemXPath, DataGenerator.ItemName, DataGenerator.ItemQuantity, DataGenerator.ItemUnit, DataGenerator.ItemUnitPrice, DataGenerator.ItemVatRate, DataGenerator.ItemDescription) assertLineItem(asserter, lineItemXPath, DataGenerator.ItemName, DataGenerator.ItemQuantity, DataGenerator.ItemUnit, DataGenerator.ItemUnitPrice, DataGenerator.ItemVatRate, DataGenerator.ItemDescription)
} }
private fun assertParty(asserter: XPathAsserter, partyXPath: String, name: String, street: String, postalCode: String, city: String, vatId: String, email: String, phone: String) { private fun assertParty(asserter: XPathAsserter, partyXPath: String, name: String, address: String, postalCode: String, city: String, vatId: String, email: String, phone: String) {
asserter.xpathHasValue("$partyXPath/ram:Name", name) asserter.xpathHasValue("$partyXPath/ram:Name", name)
asserter.xpathHasValue("$partyXPath/ram:PostalTradeAddress/ram:LineOne", street) asserter.xpathHasValue("$partyXPath/ram:PostalTradeAddress/ram:LineOne", address)
asserter.xpathHasValue("$partyXPath/ram:PostalTradeAddress/ram:PostcodeCode", postalCode) asserter.xpathHasValue("$partyXPath/ram:PostalTradeAddress/ram:PostcodeCode", postalCode)
asserter.xpathHasValue("$partyXPath/ram:PostalTradeAddress/ram:CityName", city) asserter.xpathHasValue("$partyXPath/ram:PostalTradeAddress/ram:CityName", city)
@ -59,21 +59,21 @@ object InvoiceAsserter {
fun assertInvoice(invoice: Invoice?) { fun assertInvoice(invoice: Invoice?) {
assertThat(invoice).isNotNull() assertThat(invoice).isNotNull()
assertThat(invoice!!.invoiceNumber).isEqualTo(DataGenerator.InvoiceNumber) assertThat(invoice!!.details.invoiceNumber).isEqualTo(DataGenerator.InvoiceNumber)
assertThat(invoice.invoicingDate).isEqualTo(DataGenerator.InvoicingDate) assertThat(invoice.details.invoiceDate).isEqualTo(DataGenerator.InvoiceDate)
assertParty(invoice.sender, DataGenerator.SenderName, DataGenerator.SenderStreet, DataGenerator.SenderPostalCode, DataGenerator.SenderCity, DataGenerator.SenderCountry, DataGenerator.SenderVatId, DataGenerator.SenderEmail, DataGenerator.SenderPhone, DataGenerator.SenderBankDetails) assertParty(invoice.supplier, DataGenerator.SupplierName, DataGenerator.SupplierAddress, DataGenerator.SupplierPostalCode, DataGenerator.SupplierCity, DataGenerator.SupplierCountry, DataGenerator.SupplierVatId, DataGenerator.SupplierEmail, DataGenerator.SupplierPhone, DataGenerator.SupplierBankDetails)
assertParty(invoice.recipient, DataGenerator.RecipientName, DataGenerator.RecipientStreet, DataGenerator.RecipientPostalCode, DataGenerator.RecipientCity, DataGenerator.RecipientCountry, DataGenerator.RecipientVatId, DataGenerator.RecipientEmail, DataGenerator.RecipientPhone, DataGenerator.RecipientBankDetails) assertParty(invoice.customer, DataGenerator.CustomerName, DataGenerator.CustomerAddress, DataGenerator.CustomerPostalCode, DataGenerator.CustomerCity, DataGenerator.CustomerCountry, DataGenerator.CustomerVatId, DataGenerator.CustomerEmail, DataGenerator.CustomerPhone, DataGenerator.CustomerBankDetails)
assertThat(invoice.items).hasSize(1) assertThat(invoice.items).hasSize(1)
assertLineItem(invoice.items.first(), DataGenerator.ItemName, DataGenerator.ItemQuantity, DataGenerator.ItemUnit, DataGenerator.ItemUnitPrice, DataGenerator.ItemVatRate, DataGenerator.ItemDescription) assertLineItem(invoice.items.first(), DataGenerator.ItemName, DataGenerator.ItemQuantity, DataGenerator.ItemUnit, DataGenerator.ItemUnitPrice, DataGenerator.ItemVatRate, DataGenerator.ItemDescription)
} }
private fun assertParty(party: Party, name: String, street: String, postalCode: String, city: String, country: String?, vatId: String, email: String, phone: String, bankDetails: BankDetails?) { private fun assertParty(party: Party, name: String, address: String, postalCode: String, city: String, country: String?, vatId: String, email: String, phone: String, bankDetails: BankDetails?) {
assertThat(party.name).isEqualTo(name) assertThat(party.name).isEqualTo(name)
assertThat(party.street).isEqualTo(street) assertThat(party.address).isEqualTo(address)
assertThat(party.postalCode).isEqualTo(postalCode) assertThat(party.postalCode).isEqualTo(postalCode)
assertThat(party.city).isEqualTo(city) assertThat(party.city).isEqualTo(city)
assertThat(party.countryIsoCode).isEqualTo(country) assertThat(party.countryIsoCode).isEqualTo(country)

View File

@ -23,6 +23,9 @@ pdfboxTextExtractor=0.6.1
angusMailVersion=2.0.3 angusMailVersion=2.0.3
openHtmlToPdfVersion=1.1.22
jsoupVersion=1.18.1
klfVersion=1.6.2 klfVersion=1.6.2
lokiLogAppenderVersion=0.5.5 lokiLogAppenderVersion=0.5.5
# only used for tests # only used for tests

View File

@ -0,0 +1,49 @@
plugins {
kotlin("jvm")
}
kotlin {
jvmToolchain(11)
}
java {
withSourcesJar()
}
val openHtmlToPdfVersion: String by project
val jsoupVersion: String by project
val klfVersion: String by project
val assertKVersion: String by project
val logbackVersion: String by project
dependencies {
implementation("io.github.openhtmltopdf:openhtmltopdf-pdfbox:$openHtmlToPdfVersion")
implementation("io.github.openhtmltopdf:openhtmltopdf-slf4j:$openHtmlToPdfVersion")
implementation("org.jsoup:jsoup:$jsoupVersion")
implementation("net.codinux.log:klf:$klfVersion")
testImplementation(kotlin("test"))
testImplementation("com.willowtreeapps.assertk:assertk:$assertKVersion")
testImplementation("ch.qos.logback:logback-classic:$logbackVersion")
}
tasks.test {
useJUnitPlatform()
}
ext["customArtifactId"] = "invoice-creator"
apply(from = "../gradle/scripts/publish-codinux-repo.gradle.kts")

View File

@ -0,0 +1,40 @@
package net.codinux.invoicing.pdf
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder
import org.jsoup.Jsoup
import org.jsoup.helper.W3CDom
import org.jsoup.nodes.Document
import java.io.File
import java.nio.file.FileSystems
open class OpenHtmlToPdfHtmlToPdfConverter {
open fun renderHtml(htmlFile: File, outputFile: File) =
renderHtml(htmlFile.readText(), outputFile)
open fun renderHtml(html: String, outputFile: File) {
val doc = createWellFormedHtml(html)
xhtmlToPdf(doc, outputFile)
}
protected open fun xhtmlToPdf(doc: Document, outputPdf: File) {
outputPdf.outputStream().use { output ->
val baseUri = FileSystems.getDefault().getPath("/src/main/resources")
.toUri().toString()
val builder = PdfRendererBuilder()
builder.useFastMode()
builder.toStream(output)
builder.withW3cDocument(W3CDom().fromJsoup(doc), baseUri)
builder.run()
}
}
protected open fun createWellFormedHtml(html: String): Document =
Jsoup.parse(html, Charsets.UTF_8.name()).apply {
this.outputSettings().syntax(Document.OutputSettings.Syntax.xml)
}
}

View File

@ -0,0 +1,24 @@
package net.codinux.invoicing.pdf
import java.io.InputStream
import java.net.URL
object ResourceUtil {
fun getResourceUrl(resourcePath: String): URL? =
javaClass.classLoader.getResource(resourcePath)
fun getResourceAsStream(resourcePath: String): InputStream =
javaClass.classLoader.getResourceAsStream(resourcePath)!!
fun getResourceBytes(resourcePath: String): ByteArray =
getResourceAsStream(resourcePath).use {
it.readBytes()
}
fun getResourceAsText(resourcePath: String): String =
getResourceAsStream(resourcePath).use { inputStream ->
inputStream.reader().use { it.readText() }
}
}

View File

@ -0,0 +1,32 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<!-- Insert the current time formatted as "yyyyMMdd'T'HHmmss" under
the key "bySecond" into the logger context. This value will be
available to all subsequent configuration elements. -->
<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
<root level="ALL">
<appender-ref ref="STDOUT"/>
</root>
<!-- Apache FOP will flood otherwise the log so that test run crashes -->
<logger name="org.apache.fop" level="INFO">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="org.apache.xmlgraphics.image.loader.spi.ImageImplRegistry" level="INFO">
<appender-ref ref="STDOUT"/>
</logger>
</configuration>

View File

@ -28,4 +28,6 @@ rootProject.name = "eInvoicing"
include("e-invoice-domain") include("e-invoice-domain")
include("invoice-creator")
include("e-invoice-api") include("e-invoice-api")