diff --git a/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/mapper/MustangMapper.kt b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/mapper/MustangMapper.kt
index 29670fc..8a85f2a 100644
--- a/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/mapper/MustangMapper.kt
+++ b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/mapper/MustangMapper.kt
@@ -53,6 +53,30 @@ class MustangMapper {
}
+ fun mapToInvoice(invoice: Invoice) = net.codinux.invoicing.model.Invoice(
+ invoiceNumber = invoice.number,
+ invoicingDate = map(invoice.issueDate),
+ sender = mapParty(invoice.sender),
+ recipient = 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
+ )
+
+ fun mapParty(party: TradeParty) = Party(
+ party.name, party.street, 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) }
+ )
+
+ fun mapLineItem(item: IZUGFeRDExportableItem) = LineItem(
+ item.product.name, item.product.unit, item.quantity, item.price, item.product.vatPercent, item.product.description.takeUnless { it.isBlank() }
+ )
+
+
@JvmName("mapNullable")
private fun map(date: LocalDate?) =
date?.let { map(it) }
@@ -63,4 +87,11 @@ class MustangMapper {
private fun mapToInstant(date: LocalDate): Instant =
date.atStartOfDay(ZoneId.systemDefault()).toInstant()
+ @JvmName("mapNullable")
+ private fun map(date: Date?) =
+ date?.let { map(it) }
+
+ private fun map(date: Date): LocalDate =
+ date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
+
}
\ No newline at end of file
diff --git a/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/reader/EInvoiceReader.kt b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/reader/EInvoiceReader.kt
new file mode 100644
index 0000000..498fecd
--- /dev/null
+++ b/e-invoicing-domain/src/main/kotlin/net/codinux/invoicing/reader/EInvoiceReader.kt
@@ -0,0 +1,43 @@
+package net.codinux.invoicing.reader
+
+import net.codinux.invoicing.mapper.MustangMapper
+import net.codinux.invoicing.model.Invoice
+import org.mustangproject.ZUGFeRD.ZUGFeRDInvoiceImporter
+import java.io.File
+import java.io.InputStream
+
+class EInvoiceReader(
+ private val mapper: MustangMapper = MustangMapper()
+) {
+
+ fun readFromXml(file: File) = readFromXml(file.inputStream())
+
+ fun readFromXml(stream: InputStream): Invoice? {
+ val importer = ZUGFeRDInvoiceImporter() // XRechnungImporter only reads properties but not to a Invoice object
+ importer.fromXML(stream.reader().readText())
+
+ return extractInvoice(importer)
+ }
+
+ fun extractFromPdf(file: File) = extractFromPdf(file.inputStream())
+
+ fun extractFromPdf(stream: InputStream): Invoice? {
+ val importer = ZUGFeRDInvoiceImporter(stream)
+
+ return extractInvoice(importer)
+ }
+
+ private fun extractInvoice(importer: ZUGFeRDInvoiceImporter): Invoice? {
+ val invoice = importer.extractInvoice()
+
+ // TODO: the values LineTotalAmount, ChargeTotalAmount, AllowanceTotalAmount, TaxBasisTotalAmount, TaxTotalAmount,
+ // GrandTotalAmount, TotalPrepaidAmount adn DuePayableAmount are not extracted from XML document
+ // we could use TransactionCalculator to manually calculate these values - Importer also does this and asserts
+ // that its calculated value matches XML doc's GrandTotalAmount value. But then we would have to make some
+ // methods of TransactionCalculator public
+ // Another option would be to manually extract these values from XML document.
+
+ return mapper.mapToInvoice(invoice)
+ }
+
+}
\ No newline at end of file
diff --git a/e-invoicing-domain/src/test/kotlin/net/codinux/invoicing/reader/EInvoiceReaderTest.kt b/e-invoicing-domain/src/test/kotlin/net/codinux/invoicing/reader/EInvoiceReaderTest.kt
new file mode 100644
index 0000000..7d599d1
--- /dev/null
+++ b/e-invoicing-domain/src/test/kotlin/net/codinux/invoicing/reader/EInvoiceReaderTest.kt
@@ -0,0 +1,89 @@
+package net.codinux.invoicing.reader
+
+import assertk.assertThat
+import assertk.assertions.hasSize
+import assertk.assertions.isEqualTo
+import assertk.assertions.isNotNull
+import assertk.assertions.isNull
+import net.codinux.invoicing.model.BankDetails
+import net.codinux.invoicing.model.Invoice
+import net.codinux.invoicing.model.LineItem
+import net.codinux.invoicing.model.Party
+import net.codinux.invoicing.test.DataGenerator
+import java.io.InputStream
+import java.math.BigDecimal
+import kotlin.test.Test
+
+class EInvoiceReaderTest {
+
+ private val underTest = EInvoiceReader()
+
+
+ @Test
+ fun readFromXml() {
+ val result = underTest.readFromXml(getTestFile("XRechnung.xml"))
+
+ assertInvoice(result)
+ }
+
+ @Test
+ fun extractFromPdf() {
+ val result = underTest.extractFromPdf(getTestFile("ZUGFeRD.pdf"))
+
+ assertInvoice(result)
+ }
+
+
+ private fun getTestFile(filename: String): InputStream =
+ this.javaClass.classLoader.getResourceAsStream("files/$filename")!!
+
+ private fun assertInvoice(invoice: Invoice?) {
+ assertThat(invoice).isNotNull()
+
+ assertThat(invoice!!.invoiceNumber).isEqualTo(DataGenerator.InvoiceNumber)
+ assertThat(invoice.invoicingDate).isEqualTo(DataGenerator.InvoicingDate)
+
+ assertParty(invoice.sender, DataGenerator.SenderName, DataGenerator.SenderStreet, DataGenerator.SenderPostalCode, DataGenerator.SenderCity, DataGenerator.SenderCountry, DataGenerator.SenderVatId, DataGenerator.SenderEmail, DataGenerator.SenderPhone, DataGenerator.SenderBankDetails)
+
+ assertParty(invoice.recipient, DataGenerator.RecipientName, DataGenerator.RecipientStreet, DataGenerator.RecipientPostalCode, DataGenerator.RecipientCity, DataGenerator.RecipientCountry, DataGenerator.RecipientVatId, DataGenerator.RecipientEmail, DataGenerator.RecipientPhone, DataGenerator.RecipientBankDetails)
+
+ assertThat(invoice.items).hasSize(1)
+ assertLineItem(invoice.items.first(), DataGenerator.ItemName, DataGenerator.ItemUnit, DataGenerator.ItemQuantity, DataGenerator.ItemPrice, DataGenerator.ItemVatPercentage, 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?) {
+ assertThat(party.name).isEqualTo(name)
+
+ assertThat(party.street).isEqualTo(street)
+ assertThat(party.postalCode).isEqualTo(postalCode)
+ assertThat(party.city).isEqualTo(city)
+ assertThat(party.countryIsoCode).isEqualTo(country)
+
+ assertThat(party.vatId).isEqualTo(vatId)
+
+ assertThat(party.email).isEqualTo(email)
+ assertThat(party.phone).isEqualTo(phone)
+
+ if (bankDetails == null) {
+ assertThat(party.bankDetails).isNull()
+ } else {
+ assertThat(party.bankDetails!!.accountNumber).isEqualTo(bankDetails.accountNumber)
+ assertThat(party.bankDetails!!.bankCode).isEqualTo(bankDetails.bankCode)
+ // due to a bug in Mustang accountName doesn't get extracted from XML, see https://github.com/ZUGFeRD/mustangproject/issues/558
+// assertThat(party.bankDetails!!.accountHolderName).isEqualTo(bankDetails.accountHolderName)
+ }
+ }
+
+ private fun assertLineItem(item: LineItem, name: String, unit: String, quantity: BigDecimal, price: BigDecimal, vatPercentage: BigDecimal, description: String?) {
+ assertThat(item.name).isEqualTo(name)
+
+ assertThat(item.unit).isEqualTo(unit)
+ assertThat(item.quantity).isEqualTo(quantity.setScale(4))
+
+ assertThat(item.price).isEqualTo(price.setScale(4))
+ assertThat(item.vatPercentage).isEqualTo(vatPercentage.setScale(2))
+
+// assertThat(item.description).isEqualTo(description)
+ }
+
+}
\ No newline at end of file
diff --git a/e-invoicing-domain/src/test/resources/files/XRechnung.xml b/e-invoicing-domain/src/test/resources/files/XRechnung.xml
new file mode 100644
index 0000000..2947203
--- /dev/null
+++ b/e-invoicing-domain/src/test/resources/files/XRechnung.xml
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+ urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
+
+
+
+
+ urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0
+
+
+
+ 12345
+ 380
+
+ 20151021
+
+
+
+
+
+ 1
+
+
+ Erbrachte Dienstleistungen
+
+
+
+ 99.0000
+ 1.0000
+
+
+
+ 1.0000
+
+
+
+ VAT
+ S
+ 19.00
+
+
+ 99.00
+
+
+
+
+
+ Hochwürdiger Leistungserbringer
+
+
+ +4917012345678
+
+
+ working-class-hero@rock.me
+
+
+
+ 12345
+ Fun Street 1
+ Glückstadt
+ DE
+
+
+ working-class-hero@rock.me
+
+
+ DE123456789
+
+
+
+ Untertänigster Leistungsempfänger
+
+
+ +491234567890
+
+
+ exploiter@your.boss
+
+
+
+ 12345
+ Party Street 1
+ Glückstadt
+ DE
+
+
+ exploiter@your.boss
+
+
+ DE987654321
+
+
+
+
+
+ 12345
+ EUR
+
+ 58
+ SEPA credit transfer
+
+ DE00123456780987654321
+ Manuela Musterfrau
+
+
+ ABZODEFFXXX
+
+
+
+ 18.81
+ VAT
+ 99.00
+ S
+ 19.00
+
+
+ Zahlbar ohne Abzug bis 15.06.2016
+
+ 20160615
+
+
+
+ 99.00
+ 0.00
+ 0.00
+ 99.00
+ 18.81
+ 117.81
+ 0.00
+ 117.81
+
+
+
+
diff --git a/e-invoicing-domain/src/test/resources/files/ZUGFeRD.pdf b/e-invoicing-domain/src/test/resources/files/ZUGFeRD.pdf
new file mode 100644
index 0000000..8a54b45
Binary files /dev/null and b/e-invoicing-domain/src/test/resources/files/ZUGFeRD.pdf differ
diff --git a/e-invoicing-domain/src/test/resources/files/ZUGFeRD.xml b/e-invoicing-domain/src/test/resources/files/ZUGFeRD.xml
new file mode 100644
index 0000000..be9696a
--- /dev/null
+++ b/e-invoicing-domain/src/test/resources/files/ZUGFeRD.xml
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+ urn:cen.eu:en16931:2017
+
+
+
+ 12345
+ 380
+
+ 20151021
+
+
+
+
+
+ 1
+
+
+ Erbrachte Dienstleistungen
+
+
+
+ 99.0000
+ 1.0000
+
+
+
+ 1.0000
+
+
+
+ VAT
+ S
+ 19.00
+
+
+ 99.00
+
+
+
+
+
+ Hochwürdiger Leistungserbringer
+
+
+ +4917012345678
+
+
+ working-class-hero@rock.me
+
+
+
+ 12345
+ Fun Street 1
+ Glückstadt
+ DE
+
+
+ working-class-hero@rock.me
+
+
+ DE123456789
+
+
+
+ Untertänigster Leistungsempfänger
+
+
+ +491234567890
+
+
+ exploiter@your.boss
+
+
+
+ 12345
+ Party Street 1
+ Glückstadt
+ DE
+
+
+ exploiter@your.boss
+
+
+ DE987654321
+
+
+
+
+
+ 12345
+ EUR
+
+ 58
+ SEPA credit transfer
+
+ DE00123456780987654321
+ Manuela Musterfrau
+
+
+ ABZODEFFXXX
+
+
+
+ 18.81
+ VAT
+ 99.00
+ S
+ 19.00
+
+
+ Zahlbar ohne Abzug bis 15.06.2016
+
+ 20160615
+
+
+
+ 99.00
+ 0.00
+ 0.00
+ 99.00
+ 18.81
+ 117.81
+ 0.00
+ 117.81
+
+
+
+