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 ->
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(
invoiceNumber = "RE-00001",
invoicingDate = LocalDate.now(),
sender = Party("codinux GmbH & Co. KG", "Fun Street 1", "12345", "Glückstadt"),
recipient = Party("Abzock GmbH", "Ausbeutstr.", "12345", "Abzockhausen"),
details = InvoiceDetails("RE-00001", LocalDate.now()),
supplier = Party("codinux GmbH & Co. KG", "Fun Street 1", null, "12345", "Glückstadt"),
customer = Party("Abzock GmbH", "Ausbeutstr.", null, "12345", "Abzockhausen"),
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 =
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()
}

View File

@ -50,7 +50,7 @@ open class EmailsFetcher(
open fun checkCredentials(account: EmailAccount): CheckCredentialsResult {
try {
val status = connect(account, FetchEmailsOptions(showDebugOutputOnConsole = true))
val status = connect(account, FetchEmailsOptions())
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
folder.fetch(messages, getFetchProfile(status))
messages.mapNotNull { message ->
messages.reversed().mapNotNull { message ->
async(coroutineDispatcher) {
try {
getEmail(message, status)

View File

@ -1,11 +1,10 @@
package net.codinux.invoicing.mapper
import net.codinux.invoicing.calculator.AmountsCalculator
import net.codinux.invoicing.model.AmountAdjustments
import net.codinux.invoicing.model.ChargeOrAllowance
import net.codinux.invoicing.model.InvoiceItem
import net.codinux.invoicing.model.Party
import net.codinux.invoicing.model.*
import org.mustangproject.*
import org.mustangproject.BankDetails
import org.mustangproject.Invoice
import org.mustangproject.ZUGFeRD.IExportableTransaction
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem
import java.math.BigDecimal
@ -19,17 +18,17 @@ open class MustangMapper(
) {
open 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.number = invoice.details.invoiceNumber
this.issueDate = map(invoice.details.invoiceDate)
this.sender = mapParty(invoice.supplier)
this.recipient = mapParty(invoice.customer)
this.setZFItems(ArrayList(invoice.items.map { mapLineItem(it) }))
this.dueDate = map(invoice.dueDate)
this.paymentTermDescription = invoice.paymentDescription
this.dueDate = map(invoice.details.dueDate)
this.paymentTermDescription = invoice.details.paymentDescription
this.referenceNumber = invoice.buyerReference
this.referenceNumber = invoice.customerReferenceNumber
invoice.amountAdjustments?.let { adjustments ->
this.totalPrepaidAmount = adjustments.prepaidAmounts
@ -37,14 +36,16 @@ open class MustangMapper(
adjustments.allowances.forEach { this.addAllowance(mapAllowance(it)) }
}
if (invoice.totalAmounts == null) {
invoice.totalAmounts = calculator.calculateTotalAmounts(this)
if (invoice.totals == null) {
invoice.totals = calculator.calculateTotalAmounts(this)
}
}
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 {
this.setAdditionalAddress(party.additionalAddressLine)
this.setVATID(party.vatId)
// TODO: description?
@ -63,7 +64,10 @@ open class MustangMapper(
open fun mapLineItem(item: InvoiceItem): IZUGFeRDExportableItem = Item(
// 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 {
}
@ -90,30 +94,27 @@ open class MustangMapper(
open fun mapToInvoice(invoice: Invoice) = net.codinux.invoicing.model.Invoice(
invoiceNumber = invoice.number,
invoicingDate = map(invoice.issueDate),
sender = mapParty(invoice.sender),
recipient = mapParty(invoice.recipient),
details = InvoiceDetails(invoice.number, map(invoice.issueDate), map(invoice.dueDate ?: invoice.paymentTerms?.dueDate), invoice.paymentTermDescription ?: invoice.paymentTerms?.description),
supplier = mapParty(invoice.sender),
customer = mapParty(invoice.recipient),
items = invoice.zfItems.map { mapLineItem(it) },
dueDate = map(invoice.dueDate ?: invoice.paymentTerms?.dueDate),
paymentDescription = invoice.paymentTermDescription ?: invoice.paymentTerms?.description,
buyerReference = invoice.referenceNumber,
customerReferenceNumber = invoice.referenceNumber,
amountAdjustments = mapAmountAdjustments(invoice),
totalAmounts = calculator.calculateTotalAmounts(invoice)
totals = calculator.calculateTotalAmounts(invoice)
)
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.bankDetails?.firstOrNull()?.let { net.codinux.invoicing.model.BankDetails(it.iban, it.bic, it.accountName) }
)
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? {

View File

@ -1,21 +1,26 @@
package net.codinux.invoicing.model
import java.time.LocalDate
class Invoice(
val invoiceNumber: String,
val invoicingDate: LocalDate,
val sender: Party,
val recipient: Party,
val details: InvoiceDetails,
val supplier: Party,
val customer: Party,
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,
@ -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
* 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 unitPrice: BigDecimal,
val vatRate: BigDecimal,
val articleNumber: String? = null,
val description: String? = null,
) {
override fun toString() = "$name, $quantity x $unitPrice, $vatRate %"

View File

@ -6,7 +6,8 @@ class Party(
/**
* Party's street and house number.
*/
val street: String,
val address: String,
val additionalAddressLine: String? = null,
var postalCode: String?,
val city: String,
/**
@ -24,6 +25,12 @@ class Party(
// actually there can be multiple bankDetails in eInvoice data model
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"
}

View File

@ -3,10 +3,7 @@ package net.codinux.invoicing
import net.codinux.invoicing.creation.EInvoiceCreator
import net.codinux.invoicing.email.model.EmailAccount
import net.codinux.invoicing.email.EmailsFetcher
import net.codinux.invoicing.model.EInvoiceXmlFormat
import net.codinux.invoicing.model.Invoice
import net.codinux.invoicing.model.InvoiceItem
import net.codinux.invoicing.model.Party
import net.codinux.invoicing.model.*
import net.codinux.invoicing.reader.EInvoiceReader
import net.codinux.invoicing.validation.EInvoiceValidator
import java.io.File
@ -36,7 +33,7 @@ class Demonstration {
))
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(
invoiceNumber = "RE-00001",
invoicingDate = LocalDate.now(),
sender = Party("codinux GmbH & Co. KG", "Fun Street 1", "12345", "Glückstadt"),
recipient = Party("Abzock GmbH", "Ausbeutstr.", "12345", "Abzockhausen"),
details = InvoiceDetails("RE-00001", LocalDate.now()),
supplier = Party("codinux GmbH & Co. KG", "Fun Street 1", null, "12345", "Glückstadt"),
customer = Party("Abzock GmbH", "Ausbeutstr.", null, "12345", "Abzockhausen"),
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
import net.codinux.invoicing.model.BankDetails
import net.codinux.invoicing.model.Invoice
import net.codinux.invoicing.model.InvoiceItem
import net.codinux.invoicing.model.Party
import net.codinux.invoicing.calculator.AmountsCalculator
import net.codinux.invoicing.model.*
import java.math.BigDecimal
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@ -11,63 +9,70 @@ import java.time.format.DateTimeFormatter
object DataGenerator {
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)
const val SenderName = "Hochwürdiger Leistungserbringer"
const val SenderStreet = "Fun Street 1"
const val SenderPostalCode = "12345"
const val SenderCity = "Glückstadt"
const val SenderCountry = "DE"
const val SenderVatId = "DE123456789"
const val SenderEmail = "working-class-hero@rock.me"
const val SenderPhone = "+4917012345678"
val SenderBankDetails = BankDetails("DE00123456780987654321", "ABZODEFFXXX", "Manuela Musterfrau")
const val SupplierName = "Hochwürdiger Leistungserbringer"
const val SupplierAddress = "Fun Street 1"
val SupplierAdditionalAddressLine: String? = null
const val SupplierPostalCode = "12345"
const val SupplierCity = "Glückstadt"
const val SupplierCountry = "DE"
const val SupplierVatId = "DE123456789"
const val SupplierEmail = "working-class-hero@rock.me"
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 RecipientStreet = "Party Street 1"
const val RecipientPostalCode = SenderPostalCode
const val RecipientCity = SenderCity
const val RecipientCountry = "DE"
const val RecipientVatId = "DE987654321"
const val RecipientEmail = "exploiter@your.boss"
const val RecipientPhone = "+491234567890"
val RecipientBankDetails: BankDetails? = null
const val CustomerName = "Untertänigster Leistungsempfänger"
const val CustomerAddress = "Party Street 1"
val CustomerAdditionalAddressLine: String? = null
const val CustomerPostalCode = SupplierPostalCode
const val CustomerCity = SupplierCity
const val CustomerCountry = "DE"
const val CustomerVatId = "DE987654321"
const val CustomerEmail = "exploiter@your.boss"
const val CustomerPhone = "+491234567890"
val CustomerFax: String? = null
val CustomerBankDetails: BankDetails? = null
const val ItemName = "Erbrachte Dienstleistungen"
val ItemQuantity = BigDecimal(1)
const val ItemUnit = "HUR" // EN code for 'hour'
val ItemUnitPrice = BigDecimal(99)
val ItemVatRate = BigDecimal(19)
val ItemArticleNumber: String? = null
val ItemDescription: String? = null
fun createInvoice(
invoiceNumber: String = InvoiceNumber,
invoicingDate: LocalDate = InvoicingDate,
sender: Party = createParty(SenderName, SenderStreet, SenderPostalCode, SenderCity, SenderCountry, SenderVatId, SenderEmail, SenderPhone,
bankDetails = SenderBankDetails),
recipient: Party = createParty(RecipientName, RecipientStreet, RecipientPostalCode, RecipientCity, RecipientCountry, RecipientVatId, RecipientEmail, RecipientPhone,
bankDetails = RecipientBankDetails),
invoiceDate: LocalDate = InvoiceDate,
supplier: Party = createParty(SupplierName, SupplierAddress, SupplierAdditionalAddressLine, SupplierPostalCode, SupplierCity, SupplierCountry,
SupplierVatId, SupplierEmail, SupplierPhone, SupplierFax, bankDetails = SupplierBankDetails),
customer: Party = createParty(CustomerName, CustomerAddress, CustomerAdditionalAddressLine, CustomerPostalCode, CustomerCity, CustomerCountry,
CustomerVatId, CustomerEmail, CustomerPhone, CustomerFax, bankDetails = CustomerBankDetails),
items: List<InvoiceItem> = listOf(createItem()),
dueDate: LocalDate? = DueDate,
paymentDescription: String? = dueDate?.let { "Zahlbar ohne Abzug bis ${DateTimeFormatter.ofPattern("dd.MM.yyyy").format(dueDate)}" },
buyerReference: String? = null
) = Invoice(invoiceNumber, invoicingDate, sender, recipient, items, dueDate, paymentDescription, buyerReference)
) = Invoice(InvoiceDetails(invoiceNumber, invoiceDate, dueDate, paymentDescription), supplier, customer, items).apply {
this.totals = AmountsCalculator().calculateTotalAmounts(this)
}
fun createParty(
name: String,
streetName: String = SenderStreet,
postalCode: String = SenderPostalCode,
city: String = SenderCity,
country: String? = SenderCountry,
vatId: String? = SenderVatId,
email: String? = SenderEmail,
phone: String? = SenderPhone,
fax: String? = null,
address: String = SupplierAddress,
additionalAddressLine: String? = SupplierAdditionalAddressLine,
postalCode: String = SupplierPostalCode,
city: String = SupplierCity,
country: String? = SupplierCountry,
vatId: String? = SupplierVatId,
email: String? = SupplierEmail,
phone: String? = SupplierPhone,
fax: String? = SupplierFax,
contactName: String? = 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(
name: String = ItemName,
@ -75,7 +80,8 @@ object DataGenerator {
unit: String = ItemUnit,
unitPrice: BigDecimal = ItemUnitPrice,
vatRate: BigDecimal = ItemVatRate,
articleNumber: String? = ItemArticleNumber,
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)
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"
assertParty(asserter, senderXPath, DataGenerator.SenderName, DataGenerator.SenderStreet, DataGenerator.SenderPostalCode, DataGenerator.SenderCity, DataGenerator.SenderVatId, DataGenerator.SenderEmail, DataGenerator.SenderPhone)
val supplierXPath = "//rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty"
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"
assertParty(asserter, receiverXPath, DataGenerator.RecipientName, DataGenerator.RecipientStreet, DataGenerator.RecipientPostalCode, DataGenerator.RecipientCity, DataGenerator.RecipientVatId, DataGenerator.RecipientEmail, DataGenerator.RecipientPhone)
val customerXPath = "//rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty"
assertParty(asserter, customerXPath, DataGenerator.CustomerName, DataGenerator.CustomerAddress, DataGenerator.CustomerPostalCode, DataGenerator.CustomerCity, DataGenerator.CustomerVatId, DataGenerator.CustomerEmail, DataGenerator.CustomerPhone)
val lineItemXPath = "//rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem"
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: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:CityName", city)
@ -59,21 +59,21 @@ object InvoiceAsserter {
fun assertInvoice(invoice: Invoice?) {
assertThat(invoice).isNotNull()
assertThat(invoice!!.invoiceNumber).isEqualTo(DataGenerator.InvoiceNumber)
assertThat(invoice.invoicingDate).isEqualTo(DataGenerator.InvoicingDate)
assertThat(invoice!!.details.invoiceNumber).isEqualTo(DataGenerator.InvoiceNumber)
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)
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.street).isEqualTo(street)
assertThat(party.address).isEqualTo(address)
assertThat(party.postalCode).isEqualTo(postalCode)
assertThat(party.city).isEqualTo(city)
assertThat(party.countryIsoCode).isEqualTo(country)

View File

@ -23,6 +23,9 @@ pdfboxTextExtractor=0.6.1
angusMailVersion=2.0.3
openHtmlToPdfVersion=1.1.22
jsoupVersion=1.18.1
klfVersion=1.6.2
lokiLogAppenderVersion=0.5.5
# 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("invoice-creator")
include("e-invoice-api")