Compare commits
9 Commits
ed5f272c07
...
b9d75a6423
Author | SHA1 | Date |
---|---|---|
dankito | b9d75a6423 | |
dankito | 55427c68ff | |
dankito | ad23dc3fc3 | |
dankito | e76a130a75 | |
dankito | d8f707e5eb | |
dankito | 68b1f56cfa | |
dankito | 6a9ed9078e | |
dankito | 8c4614fe9c | |
dankito | a944b4b69c |
|
@ -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
|
||||
)
|
||||
```
|
||||
|
|
|
@ -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.
|
|
@ -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()
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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()})" } ?: ""}"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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 %"
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -28,4 +28,6 @@ rootProject.name = "eInvoicing"
|
|||
|
||||
include("e-invoice-domain")
|
||||
|
||||
include("invoice-creator")
|
||||
|
||||
include("e-invoice-api")
|
||||
|
|
Loading…
Reference in New Issue