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 + + + +