Compare commits
7 Commits
35a2a6c518
...
6aaecd1656
Author | SHA1 | Date |
---|---|---|
dankito | 6aaecd1656 | |
dankito | 2514e96b16 | |
dankito | 23ad5c6945 | |
dankito | f1a364866c | |
dankito | 556c59ba3a | |
dankito | 3418ab9b0f | |
dankito | 0f97b8dae9 |
|
@ -68,7 +68,7 @@ private fun createInvoice() = Invoice(
|
||||||
invoicingDate = LocalDate.now(),
|
invoicingDate = LocalDate.now(),
|
||||||
sender = Party("codinux GmbH & Co. KG", "Fun Street 1", "12345", "Glückstadt"),
|
sender = Party("codinux GmbH & Co. KG", "Fun Street 1", "12345", "Glückstadt"),
|
||||||
recipient = Party("Abzock GmbH", "Ausbeutstr.", "12345", "Abzockhausen"),
|
recipient = Party("Abzock GmbH", "Ausbeutstr.", "12345", "Abzockhausen"),
|
||||||
items = listOf(LineItem("Erbrachte Dienstleistungen", "HUR", BigDecimal(170), BigDecimal(1_000_000), BigDecimal(19))) // HUR = EN code for hour
|
items = listOf(InvoiceItem("Erbrachte Dienstleistungen", BigDecimal(170), "HUR", BigDecimal(105), BigDecimal(19))) // HUR = EN code for hour
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package net.codinux.invoicing.calculator
|
||||||
|
|
||||||
|
import net.codinux.invoicing.mapper.MustangMapper
|
||||||
|
import net.codinux.invoicing.model.Invoice
|
||||||
|
import net.codinux.invoicing.model.TotalAmounts
|
||||||
|
import org.mustangproject.ZUGFeRD.IExportableTransaction
|
||||||
|
import org.mustangproject.ZUGFeRD.TransactionCalculator
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
open class AmountsCalculator {
|
||||||
|
|
||||||
|
protected open val mapper by lazy { MustangMapper() } // lazy to avoid circular dependency creation with MustangMapper
|
||||||
|
|
||||||
|
|
||||||
|
open fun calculateTotalAmounts(invoice: Invoice) =
|
||||||
|
calculateTotalAmounts(mapper.mapToTransaction(invoice))
|
||||||
|
|
||||||
|
open fun calculateTotalAmounts(invoice: IExportableTransaction): TotalAmounts {
|
||||||
|
val calculator = MustangTransactionCalculator(invoice)
|
||||||
|
|
||||||
|
return TotalAmounts(
|
||||||
|
lineTotalAmount = calculator.lineTotalAmount,
|
||||||
|
chargeTotalAmount = calculator.chargeTotal,
|
||||||
|
allowanceTotalAmount = calculator.allowanceTotal,
|
||||||
|
taxBasisTotalAmount = calculator.taxBasisTotalAmount,
|
||||||
|
taxTotalAmount = calculator.taxTotalAmount,
|
||||||
|
grandTotalAmount = calculator.grandTotal,
|
||||||
|
totalPrepaidAmount = calculator.totalPrepaidAmount,
|
||||||
|
duePayableAmount = calculator.duePayableAmount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected open class MustangTransactionCalculator(invoice: IExportableTransaction): TransactionCalculator(invoice) {
|
||||||
|
val lineTotalAmount: BigDecimal = this.total
|
||||||
|
|
||||||
|
val taxBasisTotalAmount: BigDecimal = this.taxBasis
|
||||||
|
val taxTotalAmount: BigDecimal by lazy { this.getGrandTotal().subtract(this.taxBasis) }
|
||||||
|
|
||||||
|
val totalPrepaidAmount: BigDecimal = this.totalPrepaid
|
||||||
|
val duePayableAmount: BigDecimal by lazy { this.getGrandTotal().subtract(this.totalPrepaid) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,12 +6,12 @@ import org.mustangproject.CII.CIIToUBL
|
||||||
import org.mustangproject.ZUGFeRD.ZUGFeRDVisualizer
|
import org.mustangproject.ZUGFeRD.ZUGFeRDVisualizer
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class EInvoiceConverter {
|
open class EInvoiceConverter {
|
||||||
|
|
||||||
fun convertInvoiceToHtml(invoice: Invoice, outputFile: File, language: ZUGFeRDVisualizer.Language = ZUGFeRDVisualizer.Language.DE) =
|
open fun convertInvoiceToHtml(invoice: Invoice, outputFile: File, language: ZUGFeRDVisualizer.Language = ZUGFeRDVisualizer.Language.DE) =
|
||||||
convertInvoiceToHtml(createXRechnungXml(invoice), outputFile, language)
|
convertInvoiceToHtml(createXRechnungXml(invoice), outputFile, language)
|
||||||
|
|
||||||
fun convertInvoiceToHtml(invoiceXml: String, outputFile: File, language: ZUGFeRDVisualizer.Language = ZUGFeRDVisualizer.Language.DE): String {
|
open fun convertInvoiceToHtml(invoiceXml: String, outputFile: File, language: ZUGFeRDVisualizer.Language = ZUGFeRDVisualizer.Language.DE): String {
|
||||||
val xmlFile = File.createTempFile("Zugferd", ".xml")
|
val xmlFile = File.createTempFile("Zugferd", ".xml")
|
||||||
.also { it.writeText(invoiceXml) }
|
.also { it.writeText(invoiceXml) }
|
||||||
|
|
||||||
|
@ -32,12 +32,12 @@ class EInvoiceConverter {
|
||||||
/**
|
/**
|
||||||
* Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language).
|
* Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language).
|
||||||
*/
|
*/
|
||||||
fun convertCiiToUbl(invoice: Invoice) = convertCiiToUbl(createXRechnungXml(invoice))
|
open fun convertCiiToUbl(invoice: Invoice) = convertCiiToUbl(createXRechnungXml(invoice))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language).
|
* Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language).
|
||||||
*/
|
*/
|
||||||
fun convertCiiToUbl(invoiceXml: String): String {
|
open fun convertCiiToUbl(invoiceXml: String): String {
|
||||||
// TODO: extract a common method for this
|
// TODO: extract a common method for this
|
||||||
val xmlFile = File.createTempFile("Zugferd", ".xml")
|
val xmlFile = File.createTempFile("Zugferd", ".xml")
|
||||||
.also { it.writeText(invoiceXml) }
|
.also { it.writeText(invoiceXml) }
|
||||||
|
@ -56,15 +56,15 @@ class EInvoiceConverter {
|
||||||
/**
|
/**
|
||||||
* Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language).
|
* Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language).
|
||||||
*/
|
*/
|
||||||
fun convertCiiToUbl(xmlFile: File, outputFile: File) {
|
open fun convertCiiToUbl(xmlFile: File, outputFile: File) {
|
||||||
val cii2Ubl = CIIToUBL()
|
val cii2Ubl = CIIToUBL()
|
||||||
cii2Ubl.convert(xmlFile, outputFile)
|
cii2Ubl.convert(xmlFile, outputFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun createXRechnungXml(invoice: Invoice): String = EInvoiceCreator().createXRechnungXml(invoice)
|
protected open fun createXRechnungXml(invoice: Invoice): String = EInvoiceCreator().createXRechnungXml(invoice)
|
||||||
|
|
||||||
private fun copyResource(resourceName: String, outputFile: File, outputFileExtension: String) {
|
protected open fun copyResource(resourceName: String, outputFile: File, outputFileExtension: String) {
|
||||||
javaClass.classLoader.getResourceAsStream(resourceName).use {
|
javaClass.classLoader.getResourceAsStream(resourceName).use {
|
||||||
it?.copyTo(File(outputFile.parentFile, outputFile.nameWithoutExtension + outputFileExtension).outputStream())
|
it?.copyTo(File(outputFile.parentFile, outputFile.nameWithoutExtension + outputFileExtension).outputStream())
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,11 @@ import net.codinux.invoicing.model.Invoice
|
||||||
import org.mustangproject.ZUGFeRD.*
|
import org.mustangproject.ZUGFeRD.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class EInvoiceCreator(
|
open class EInvoiceCreator(
|
||||||
private val mapper: MustangMapper = MustangMapper()
|
protected open val mapper: MustangMapper = MustangMapper()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createXRechnungXml(invoice: Invoice): String {
|
open fun createXRechnungXml(invoice: Invoice): String {
|
||||||
val provider = ZUGFeRD2PullProvider()
|
val provider = ZUGFeRD2PullProvider()
|
||||||
provider.profile = Profiles.getByName("XRechnung")
|
provider.profile = Profiles.getByName("XRechnung")
|
||||||
|
|
||||||
|
@ -20,9 +20,9 @@ class EInvoiceCreator(
|
||||||
/**
|
/**
|
||||||
* Synonym for [createFacturXXml] (ZUGFeRD 2 is a synonym for Factur-X).
|
* Synonym for [createFacturXXml] (ZUGFeRD 2 is a synonym for Factur-X).
|
||||||
*/
|
*/
|
||||||
fun createZugferdXml(invoice: Invoice) = createFacturXXml(invoice)
|
open fun createZugferdXml(invoice: Invoice) = createFacturXXml(invoice)
|
||||||
|
|
||||||
fun createFacturXXml(invoice: Invoice): String {
|
open fun createFacturXXml(invoice: Invoice): String {
|
||||||
val exporter = ZUGFeRDExporterFromA3()
|
val exporter = ZUGFeRDExporterFromA3()
|
||||||
.setProfile("EN16931") // required for XML?
|
.setProfile("EN16931") // required for XML?
|
||||||
|
|
||||||
|
@ -32,9 +32,9 @@ class EInvoiceCreator(
|
||||||
/**
|
/**
|
||||||
* Synonym for [createFacturXPdf] (ZUGFeRD 2 is a synonym for Factur-X).
|
* Synonym for [createFacturXPdf] (ZUGFeRD 2 is a synonym for Factur-X).
|
||||||
*/
|
*/
|
||||||
fun createZugferdPdf(invoice: Invoice, outputFile: File) = createFacturXPdf(invoice, outputFile)
|
open fun createZugferdPdf(invoice: Invoice, outputFile: File) = createFacturXPdf(invoice, outputFile)
|
||||||
|
|
||||||
fun createFacturXPdf(invoice: Invoice, outputFile: File) {
|
open fun createFacturXPdf(invoice: Invoice, outputFile: File) {
|
||||||
val xml = createFacturXXml(invoice)
|
val xml = createFacturXXml(invoice)
|
||||||
val xmlFile = File.createTempFile(outputFile.nameWithoutExtension, ".xml")
|
val xmlFile = File.createTempFile(outputFile.nameWithoutExtension, ".xml")
|
||||||
.also { it.writeText(xml) }
|
.also { it.writeText(xml) }
|
||||||
|
@ -50,10 +50,10 @@ class EInvoiceCreator(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun attachInvoiceXmlToPdf(invoice: Invoice, pdfFile: File, outputFile: File) =
|
open fun attachInvoiceXmlToPdf(invoice: Invoice, pdfFile: File, outputFile: File) =
|
||||||
attachInvoiceXmlToPdf(createFacturXXml(invoice), pdfFile, outputFile)
|
attachInvoiceXmlToPdf(createFacturXXml(invoice), pdfFile, outputFile)
|
||||||
|
|
||||||
fun attachInvoiceXmlToPdf(invoiceXml: String, pdfFile: File, outputFile: File) {
|
open fun attachInvoiceXmlToPdf(invoiceXml: String, pdfFile: File, outputFile: File) {
|
||||||
val exporter = ZUGFeRDExporterFromA3()
|
val exporter = ZUGFeRDExporterFromA3()
|
||||||
.setZUGFeRDVersion(2)
|
.setZUGFeRDVersion(2)
|
||||||
.setProfile("EN16931") // available values: MINIMUM, BASICWL, BASIC, CIUS, EN16931, EXTENDED, XRECHNUNG
|
.setProfile("EN16931") // available values: MINIMUM, BASICWL, BASIC, CIUS, EN16931, EXTENDED, XRECHNUNG
|
||||||
|
@ -68,7 +68,7 @@ class EInvoiceCreator(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun createXml(provider: IXMLProvider, invoice: Invoice): String {
|
protected open fun createXml(provider: IXMLProvider, invoice: Invoice): String {
|
||||||
val transaction = mapper.mapToTransaction(invoice)
|
val transaction = mapper.mapToTransaction(invoice)
|
||||||
|
|
||||||
provider.generateXML(transaction)
|
provider.generateXML(transaction)
|
||||||
|
|
|
@ -6,16 +6,16 @@ import net.codinux.log.logger
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.io.path.*
|
import kotlin.io.path.*
|
||||||
|
|
||||||
class FilesystemInvoiceReader(
|
open class FilesystemInvoiceReader(
|
||||||
private val eInvoiceReader: EInvoiceReader = EInvoiceReader()
|
protected open val eInvoiceReader: EInvoiceReader = EInvoiceReader()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val log by logger()
|
private val log by logger()
|
||||||
|
|
||||||
fun readAllInvoicesOfDirectory(directory: Path, recursive: Boolean = false) =
|
open fun readAllInvoicesOfDirectory(directory: Path, recursive: Boolean = false) =
|
||||||
readInvoicesFromFiles(collectFiles(directory, recursive))
|
readInvoicesFromFiles(collectFiles(directory, recursive))
|
||||||
|
|
||||||
private fun collectFiles(directory: Path, recursive: Boolean): List<Path> = buildList {
|
protected open fun collectFiles(directory: Path, recursive: Boolean): List<Path> = buildList {
|
||||||
directory.listDirectoryEntries().forEach { child ->
|
directory.listDirectoryEntries().forEach { child ->
|
||||||
if (child.isRegularFile()) {
|
if (child.isRegularFile()) {
|
||||||
add(child)
|
add(child)
|
||||||
|
@ -25,13 +25,13 @@ class FilesystemInvoiceReader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readInvoicesFromFiles(vararg files: Path) =
|
open fun readInvoicesFromFiles(vararg files: Path) =
|
||||||
readInvoicesFromFiles(files.toList())
|
readInvoicesFromFiles(files.toList())
|
||||||
|
|
||||||
fun readInvoicesFromFiles(files: List<Path>): List<InvoiceOnFilesystem> =
|
open fun readInvoicesFromFiles(files: List<Path>): List<InvoiceOnFilesystem> =
|
||||||
files.mapNotNull { file -> readInvoiceFromFile(file)?.let { InvoiceOnFilesystem(file, it) } }
|
files.mapNotNull { file -> readInvoiceFromFile(file)?.let { InvoiceOnFilesystem(file, it) } }
|
||||||
|
|
||||||
fun readInvoiceFromFile(file: Path): Invoice? = try {
|
open fun readInvoiceFromFile(file: Path): Invoice? = try {
|
||||||
val extension = file.extension.lowercase()
|
val extension = file.extension.lowercase()
|
||||||
|
|
||||||
if (extension == "pdf") {
|
if (extension == "pdf") {
|
||||||
|
|
|
@ -19,22 +19,22 @@ import java.util.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class MailReader(
|
open class MailReader(
|
||||||
private val eInvoiceReader: EInvoiceReader = EInvoiceReader()
|
protected open val eInvoiceReader: EInvoiceReader = EInvoiceReader()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private data class MessagePart(
|
protected data class MessagePart(
|
||||||
val mediaType: String,
|
val mediaType: String,
|
||||||
val part: Part
|
val part: Part
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
private val mailDispatcher = Executors.newFixedThreadPool(max(24, Runtime.getRuntime().availableProcessors() * 4)).asCoroutineDispatcher()
|
protected open val mailDispatcher = Executors.newFixedThreadPool(max(24, Runtime.getRuntime().availableProcessors() * 4)).asCoroutineDispatcher()
|
||||||
|
|
||||||
private val log by logger()
|
protected val log by logger()
|
||||||
|
|
||||||
|
|
||||||
fun listenForNewReceivedEInvoices(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX", eInvoiceReceived: (MailWithInvoice) -> Unit) = runBlocking {
|
open fun listenForNewReceivedEInvoices(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX", eInvoiceReceived: (MailWithInvoice) -> Unit) = runBlocking {
|
||||||
try {
|
try {
|
||||||
connect(account) { store ->
|
connect(account) { store ->
|
||||||
val folder = store.getFolder(emailFolderName)
|
val folder = store.getFolder(emailFolderName)
|
||||||
|
@ -61,7 +61,7 @@ class MailReader(
|
||||||
log.info { "Stopped listening to new received eInvoices of '${account.username}'" }
|
log.info { "Stopped listening to new received eInvoices of '${account.username}'" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun keepConnectionOpen(account: MailAccount, folder: Folder) {
|
protected open suspend fun keepConnectionOpen(account: MailAccount, folder: Folder) {
|
||||||
log.info { "Listening to new mails of ${account.username}" }
|
log.info { "Listening to new mails of ${account.username}" }
|
||||||
|
|
||||||
// Use IMAP IDLE to keep the connection alive
|
// Use IMAP IDLE to keep the connection alive
|
||||||
|
@ -78,7 +78,7 @@ class MailReader(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun listAllMessagesWithEInvoice(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX"): List<MailWithInvoice> {
|
open fun listAllMessagesWithEInvoice(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX"): List<MailWithInvoice> {
|
||||||
try {
|
try {
|
||||||
return connect(account) { store ->
|
return connect(account) { store ->
|
||||||
val inbox = store.getFolder(emailFolderName)
|
val inbox = store.getFolder(emailFolderName)
|
||||||
|
@ -95,7 +95,7 @@ class MailReader(
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listAllMessagesWithEInvoiceInFolder(folder: Folder, downloadMessageBody: Boolean): List<MailWithInvoice> = runBlocking {
|
protected open fun listAllMessagesWithEInvoiceInFolder(folder: Folder, downloadMessageBody: Boolean): List<MailWithInvoice> = runBlocking {
|
||||||
val messageCount = folder.messageCount
|
val messageCount = folder.messageCount
|
||||||
if (messageCount <= 0) {
|
if (messageCount <= 0) {
|
||||||
return@runBlocking emptyList()
|
return@runBlocking emptyList()
|
||||||
|
@ -115,7 +115,7 @@ class MailReader(
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findEInvoice(message: Message, downloadMessageBody: Boolean): MailWithInvoice? {
|
protected open fun findEInvoice(message: Message, downloadMessageBody: Boolean): MailWithInvoice? {
|
||||||
try {
|
try {
|
||||||
val parts = getAllMessageParts(message)
|
val parts = getAllMessageParts(message)
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ class MailReader(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findEInvoice(messagePart: MessagePart): MailAttachmentWithEInvoice? {
|
protected open fun findEInvoice(messagePart: MessagePart): MailAttachmentWithEInvoice? {
|
||||||
try {
|
try {
|
||||||
val part = messagePart.part
|
val part = messagePart.part
|
||||||
val invoice = tryToReadEInvoice(part, messagePart.mediaType)
|
val invoice = tryToReadEInvoice(part, messagePart.mediaType)
|
||||||
|
@ -160,7 +160,7 @@ class MailReader(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryToReadEInvoice(part: Part, mediaType: String?): Invoice? = try {
|
protected open fun tryToReadEInvoice(part: Part, mediaType: String?): Invoice? = try {
|
||||||
val filename = part.fileName?.lowercase() ?: ""
|
val filename = part.fileName?.lowercase() ?: ""
|
||||||
|
|
||||||
if (filename.endsWith(".pdf") || mediaType == "application/pdf" || mediaType == "application/octet-stream") {
|
if (filename.endsWith(".pdf") || mediaType == "application/pdf" || mediaType == "application/octet-stream") {
|
||||||
|
@ -176,7 +176,7 @@ class MailReader(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getAllMessageParts(part: Part): List<MessagePart> {
|
protected open fun getAllMessageParts(part: Part): List<MessagePart> {
|
||||||
return if (part.isMimeType("multipart/*")) {
|
return if (part.isMimeType("multipart/*")) {
|
||||||
val multipart = part.content as Multipart
|
val multipart = part.content as Multipart
|
||||||
val parts = IntRange(0, multipart.count - 1).map { multipart.getBodyPart(it) }
|
val parts = IntRange(0, multipart.count - 1).map { multipart.getBodyPart(it) }
|
||||||
|
@ -202,7 +202,7 @@ class MailReader(
|
||||||
*
|
*
|
||||||
* -> This method removes parameters and return media type (first part) only
|
* -> This method removes parameters and return media type (first part) only
|
||||||
*/
|
*/
|
||||||
private fun getMediaType(part: Part): String? = part.contentType?.lowercase()?.let { contentType ->
|
protected open fun getMediaType(part: Part): String? = part.contentType?.lowercase()?.let { contentType ->
|
||||||
val indexOfSeparator = contentType.indexOf(';')
|
val indexOfSeparator = contentType.indexOf(';')
|
||||||
|
|
||||||
if (indexOfSeparator > -1) {
|
if (indexOfSeparator > -1) {
|
||||||
|
@ -212,11 +212,11 @@ class MailReader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPlainTextBody(parts: Collection<MessagePart>) = getBodyWithMediaType(parts, "text/plain")
|
protected open fun getPlainTextBody(parts: Collection<MessagePart>) = getBodyWithMediaType(parts, "text/plain")
|
||||||
|
|
||||||
private fun getHtmlBody(parts: Collection<MessagePart>) = getBodyWithMediaType(parts, "text/html")
|
protected open fun getHtmlBody(parts: Collection<MessagePart>) = getBodyWithMediaType(parts, "text/html")
|
||||||
|
|
||||||
private fun getBodyWithMediaType(parts: Collection<MessagePart>, mediaType: String): String? = try {
|
protected open fun getBodyWithMediaType(parts: Collection<MessagePart>, mediaType: String): String? = try {
|
||||||
val partsForMediaType = parts.filter { it.mediaType == mediaType }
|
val partsForMediaType = parts.filter { it.mediaType == mediaType }
|
||||||
|
|
||||||
if (partsForMediaType.size == 1) {
|
if (partsForMediaType.size == 1) {
|
||||||
|
@ -239,11 +239,11 @@ class MailReader(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun map(date: Date): Instant =
|
protected open fun map(date: Date): Instant =
|
||||||
date.toInstant()
|
date.toInstant()
|
||||||
|
|
||||||
|
|
||||||
private fun <T> connect(account: MailAccount, connected: (Store) -> T): T {
|
protected open fun <T> connect(account: MailAccount, connected: (Store) -> T): T {
|
||||||
val properties = mapAccountToJavaMailProperties(account)
|
val properties = mapAccountToJavaMailProperties(account)
|
||||||
|
|
||||||
val session = Session.getInstance(properties)
|
val session = Session.getInstance(properties)
|
||||||
|
@ -254,7 +254,7 @@ class MailReader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapAccountToJavaMailProperties(account: MailAccount) = Properties().apply {
|
protected open fun mapAccountToJavaMailProperties(account: MailAccount) = Properties().apply {
|
||||||
put("mail.store.protocol", "imap")
|
put("mail.store.protocol", "imap")
|
||||||
|
|
||||||
put("mail.imap.host", account.serverAddress)
|
put("mail.imap.host", account.serverAddress)
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
package net.codinux.invoicing.mapper
|
package net.codinux.invoicing.mapper
|
||||||
|
|
||||||
import net.codinux.invoicing.model.LineItem
|
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.Party
|
||||||
import org.mustangproject.*
|
import org.mustangproject.*
|
||||||
import org.mustangproject.ZUGFeRD.IExportableTransaction
|
import org.mustangproject.ZUGFeRD.IExportableTransaction
|
||||||
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem
|
import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem
|
||||||
|
import java.math.BigDecimal
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class MustangMapper {
|
open class MustangMapper(
|
||||||
|
protected open val calculator: AmountsCalculator = AmountsCalculator()
|
||||||
|
) {
|
||||||
|
|
||||||
fun mapToTransaction(invoice: net.codinux.invoicing.model.Invoice): IExportableTransaction = Invoice().apply {
|
open fun mapToTransaction(invoice: net.codinux.invoicing.model.Invoice): IExportableTransaction = Invoice().apply {
|
||||||
this.number = invoice.invoiceNumber
|
this.number = invoice.invoiceNumber
|
||||||
this.issueDate = map(invoice.invoicingDate)
|
this.issueDate = map(invoice.invoicingDate)
|
||||||
this.sender = mapParty(invoice.sender)
|
this.sender = mapParty(invoice.sender)
|
||||||
|
@ -24,9 +30,19 @@ class MustangMapper {
|
||||||
this.paymentTermDescription = invoice.paymentDescription
|
this.paymentTermDescription = invoice.paymentDescription
|
||||||
|
|
||||||
this.referenceNumber = invoice.buyerReference
|
this.referenceNumber = invoice.buyerReference
|
||||||
|
|
||||||
|
invoice.amountAdjustments?.let { adjustments ->
|
||||||
|
this.totalPrepaidAmount = adjustments.prepaidAmounts
|
||||||
|
adjustments.charges.forEach { this.addCharge(mapCharge(it)) }
|
||||||
|
adjustments.allowances.forEach { this.addAllowance(mapAllowance(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invoice.totalAmounts == null) {
|
||||||
|
invoice.totalAmounts = calculator.calculateTotalAmounts(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapParty(party: Party): TradeParty = TradeParty(
|
open fun mapParty(party: Party): TradeParty = TradeParty(
|
||||||
party.name, party.street, party.postalCode, party.city, party.countryIsoCode
|
party.name, party.street, party.postalCode, party.city, party.countryIsoCode
|
||||||
).apply {
|
).apply {
|
||||||
this.setVATID(party.vatId)
|
this.setVATID(party.vatId)
|
||||||
|
@ -45,15 +61,35 @@ class MustangMapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapLineItem(item: LineItem): IZUGFeRDExportableItem = Item(
|
open fun mapLineItem(item: InvoiceItem): IZUGFeRDExportableItem = Item(
|
||||||
// description has to be an empty string if not set
|
// description has to be an empty string if not set
|
||||||
Product(item.name, item.description ?: "", item.unit, item.vatPercentage), item.price, item.quantity
|
Product(item.name, item.description ?: "", item.unit, item.vatRate), item.unitPrice, item.quantity
|
||||||
).apply {
|
).apply {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun mapCharge(charge: ChargeOrAllowance) = Charge(charge.actualAmount).apply {
|
||||||
|
this.percent = charge.calculationPercent
|
||||||
|
|
||||||
fun mapToInvoice(invoice: Invoice) = net.codinux.invoicing.model.Invoice(
|
this.reason = charge.reason
|
||||||
|
this.reasonCode = charge.reasonCode
|
||||||
|
|
||||||
|
this.taxPercent = charge.taxRateApplicablePercent
|
||||||
|
this.categoryCode = charge.taxCategoryCode
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun mapAllowance(allowance: ChargeOrAllowance) = Allowance(allowance.actualAmount).apply {
|
||||||
|
this.percent = allowance.calculationPercent
|
||||||
|
|
||||||
|
this.reason = allowance.reason
|
||||||
|
this.reasonCode = allowance.reasonCode
|
||||||
|
|
||||||
|
this.taxPercent = allowance.taxRateApplicablePercent
|
||||||
|
this.categoryCode = allowance.taxCategoryCode
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open fun mapToInvoice(invoice: Invoice) = net.codinux.invoicing.model.Invoice(
|
||||||
invoiceNumber = invoice.number,
|
invoiceNumber = invoice.number,
|
||||||
invoicingDate = map(invoice.issueDate),
|
invoicingDate = map(invoice.issueDate),
|
||||||
sender = mapParty(invoice.sender),
|
sender = mapParty(invoice.sender),
|
||||||
|
@ -63,35 +99,57 @@ class MustangMapper {
|
||||||
dueDate = map(invoice.dueDate ?: invoice.paymentTerms?.dueDate),
|
dueDate = map(invoice.dueDate ?: invoice.paymentTerms?.dueDate),
|
||||||
paymentDescription = invoice.paymentTermDescription ?: invoice.paymentTerms?.description,
|
paymentDescription = invoice.paymentTermDescription ?: invoice.paymentTerms?.description,
|
||||||
|
|
||||||
buyerReference = invoice.referenceNumber
|
buyerReference = invoice.referenceNumber,
|
||||||
|
|
||||||
|
amountAdjustments = mapAmountAdjustments(invoice),
|
||||||
|
|
||||||
|
totalAmounts = calculator.calculateTotalAmounts(invoice)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun mapParty(party: TradeParty) = Party(
|
open fun mapParty(party: TradeParty) = Party(
|
||||||
party.name, party.street, party.zip, party.location, party.country, party.vatID,
|
party.name, party.street, party.zip, party.location, party.country, party.vatID,
|
||||||
party.email ?: party.contact?.eMail, party.contact?.phone, party.contact?.fax, party.contact?.name,
|
party.email ?: party.contact?.eMail, party.contact?.phone, party.contact?.fax, party.contact?.name,
|
||||||
party.bankDetails?.firstOrNull()?.let { net.codinux.invoicing.model.BankDetails(it.iban, it.bic, it.accountName) }
|
party.bankDetails?.firstOrNull()?.let { net.codinux.invoicing.model.BankDetails(it.iban, it.bic, it.accountName) }
|
||||||
)
|
)
|
||||||
|
|
||||||
fun mapLineItem(item: IZUGFeRDExportableItem) = LineItem(
|
open fun mapLineItem(item: IZUGFeRDExportableItem) = InvoiceItem(
|
||||||
item.product.name, item.product.unit, item.quantity, 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.description.takeUnless { it.isBlank() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
protected open fun mapAmountAdjustments(invoice: Invoice): AmountAdjustments? {
|
||||||
|
if ((invoice.totalPrepaidAmount == null || invoice.totalPrepaidAmount == BigDecimal.ZERO)
|
||||||
|
&& invoice.zfCharges.isNullOrEmpty() && invoice.zfAllowances.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return AmountAdjustments(
|
||||||
|
invoice.totalPrepaidAmount,
|
||||||
|
invoice.zfCharges.mapNotNull { mapChargeOrAllowance(it as? Charge) },
|
||||||
|
invoice.zfAllowances.mapNotNull { mapChargeOrAllowance(it as? Allowance ?: it as? Charge) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapChargeOrAllowance(chargeOrAllowance: Charge?) = chargeOrAllowance?.let {
|
||||||
|
ChargeOrAllowance(it.totalAmount, null, null, it.percent, it.reason, it.reasonCode, it.taxPercent, it.categoryCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@JvmName("mapNullable")
|
@JvmName("mapNullable")
|
||||||
private fun map(date: LocalDate?) =
|
protected fun map(date: LocalDate?) =
|
||||||
date?.let { map(it) }
|
date?.let { map(it) }
|
||||||
|
|
||||||
private fun map(date: LocalDate): Date =
|
protected open fun map(date: LocalDate): Date =
|
||||||
Date.from(mapToInstant(date))
|
Date.from(mapToInstant(date))
|
||||||
|
|
||||||
private fun mapToInstant(date: LocalDate): Instant =
|
protected open fun mapToInstant(date: LocalDate): Instant =
|
||||||
date.atStartOfDay(ZoneId.systemDefault()).toInstant()
|
date.atStartOfDay(ZoneId.systemDefault()).toInstant()
|
||||||
|
|
||||||
@JvmName("mapNullable")
|
@JvmName("mapNullable")
|
||||||
private fun map(date: Date?) =
|
protected fun map(date: Date?) =
|
||||||
date?.let { map(it) }
|
date?.let { map(it) }
|
||||||
|
|
||||||
private fun map(date: Date): LocalDate =
|
protected open fun map(date: Date): LocalDate =
|
||||||
date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
|
date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package net.codinux.invoicing.model
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
class AmountAdjustments(
|
||||||
|
/**
|
||||||
|
* Vorauszahlungen.
|
||||||
|
*/
|
||||||
|
val prepaidAmounts: BigDecimal = BigDecimal.ZERO,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zusätzliche Gebühren.
|
||||||
|
*/
|
||||||
|
val charges: List<ChargeOrAllowance> = emptyList(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abzüge / Nachlässe.
|
||||||
|
*/
|
||||||
|
val allowances: List<ChargeOrAllowance> = emptyList()
|
||||||
|
) {
|
||||||
|
override fun toString() = "${prepaidAmounts.toPlainString()} prepaid, ${charges.size} charges, ${allowances.size} allowances"
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package net.codinux.invoicing.model
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
class ChargeOrAllowance(
|
||||||
|
/**
|
||||||
|
* Gesamtbetrag der Gebühr oder des Nachlasses.
|
||||||
|
* Evtl. berechnet aus [basisAmount] und [calculationPercent].
|
||||||
|
*/
|
||||||
|
val actualAmount: BigDecimal,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Ausgangsbetrag auf den die Gebühr oder der Nachlass angewendet wird.
|
||||||
|
*/
|
||||||
|
val basisAmount: BigDecimal? = null,
|
||||||
|
/**
|
||||||
|
* Menge der Ware oder Dienstleistung auf die die Gebühr oder der Nachlass angewendet wird.
|
||||||
|
*/
|
||||||
|
val basisQuantity: BigDecimal? = null,
|
||||||
|
/**
|
||||||
|
* Der auf [basisAmount] anwendbare Prozentsatz der Gebühr oder des Nachlasses.
|
||||||
|
*/
|
||||||
|
val calculationPercent: BigDecimal? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menschenlesbare Beschreibung der Gebühr oder des Nachlasses.
|
||||||
|
*/
|
||||||
|
val reason: String? = null,
|
||||||
|
/**
|
||||||
|
* Code für Begründung der Gebühr oder des Nachlasses aus [UNTDID 5189](https://unece.org/fileadmin/DAM/trade/untdid/d16b/tred/tred5189.htm).
|
||||||
|
*/
|
||||||
|
val reasonCode: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anzuwendender Steuersatz (z. B. in Deutschland 7 % oder 19 %).
|
||||||
|
*/
|
||||||
|
val taxRateApplicablePercent: BigDecimal? = null,
|
||||||
|
/**
|
||||||
|
* Steuer Kategorie Code
|
||||||
|
*/
|
||||||
|
val taxCategoryCode: String? = null
|
||||||
|
) {
|
||||||
|
override fun toString() = "${reason?.let { "$it: " } ?: ""}${actualAmount.toPlainString()}"
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ class Invoice(
|
||||||
val invoicingDate: LocalDate,
|
val invoicingDate: LocalDate,
|
||||||
val sender: Party,
|
val sender: Party,
|
||||||
val recipient: Party,
|
val recipient: Party,
|
||||||
val items: List<LineItem>,
|
val items: List<InvoiceItem>,
|
||||||
|
|
||||||
val dueDate: LocalDate? = null,
|
val dueDate: LocalDate? = null,
|
||||||
val paymentDescription: String? = null,
|
val paymentDescription: String? = null,
|
||||||
|
@ -15,7 +15,17 @@ class Invoice(
|
||||||
/**
|
/**
|
||||||
* Unique reference number of the buyer, e.g. the Leitweg-ID required by German authorities (Behörden)
|
* Unique reference number of the buyer, e.g. the Leitweg-ID required by German authorities (Behörden)
|
||||||
*/
|
*/
|
||||||
val buyerReference: String? = null
|
val buyerReference: String? = null,
|
||||||
|
|
||||||
|
val amountAdjustments: AmountAdjustments? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total amounts of the 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
|
||||||
) {
|
) {
|
||||||
override fun toString() = "$invoicingDate $invoiceNumber to $recipient"
|
override fun toString() = "$invoicingDate $invoiceNumber to $recipient ${totalAmounts?.duePayableAmount?.let { " (${it.toPlainString()})" } ?: ""}"
|
||||||
}
|
}
|
|
@ -2,13 +2,13 @@ package net.codinux.invoicing.model
|
||||||
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
class LineItem(
|
class InvoiceItem(
|
||||||
val name: String,
|
val name: String,
|
||||||
val unit: String,
|
|
||||||
val quantity: BigDecimal,
|
val quantity: BigDecimal,
|
||||||
val price: BigDecimal,
|
val unit: String,
|
||||||
val vatPercentage: BigDecimal,
|
val unitPrice: BigDecimal,
|
||||||
|
val vatRate: BigDecimal,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
) {
|
) {
|
||||||
override fun toString() = "$name, $quantity x $price, $vatPercentage %"
|
override fun toString() = "$name, $quantity x $unitPrice, $vatRate %"
|
||||||
}
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package net.codinux.invoicing.model
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
class TotalAmounts(
|
||||||
|
/**
|
||||||
|
* Gesamtbetrag einer einzelnen Rechnungsposition.
|
||||||
|
*/
|
||||||
|
val lineTotalAmount: BigDecimal,
|
||||||
|
|
||||||
|
val chargeTotalAmount: BigDecimal = BigDecimal.ZERO,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gesamtbetrag aller gewährten Rabatte, Abzüge oder Nachlässe.
|
||||||
|
*/
|
||||||
|
val allowanceTotalAmount: BigDecimal = BigDecimal.ZERO,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Gesamtbetrag, der als Grundlage für die Steuerberechnung dient, nach Abzug von Rabatten (Allowance) und vor
|
||||||
|
* Hinzufügen der Steuerbeträge.
|
||||||
|
*/
|
||||||
|
val taxBasisTotalAmount: BigDecimal,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Gesamtsumme der auf der Rechnung anfallenden Steuern. Dies umfasst in der Regel alle Steuerarten, die auf den
|
||||||
|
* Tax Basis Total Amount angewendet werden (z. B. Mehrwertsteuer oder Umsatzsteuer).
|
||||||
|
*/
|
||||||
|
val taxTotalAmount: BigDecimal,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Gesamtbetrag der Rechnung nach Berücksichtigung aller Kosten, Abzüge und Steuern. Dies ist der Betrag, der
|
||||||
|
* vor eventuellen Vorauszahlungen oder Gutschriften fällig wäre.
|
||||||
|
*/
|
||||||
|
val grandTotalAmount: BigDecimal,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Betrag, der bereits im Voraus bezahlt wurde. Dieser Betrag wird vom Grand Total Amount abgezogen, um den
|
||||||
|
* verbleibenden Zahlungsbetrag zu ermitteln.
|
||||||
|
*/
|
||||||
|
val totalPrepaidAmount: BigDecimal,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der noch zu zahlende Betrag, den der Kunde begleichen muss. Er ergibt sich aus dem Grand Total Amount abzüglich
|
||||||
|
* des Total Prepaid Amount und eventueller weiterer Gutschriften oder Anpassungen.
|
||||||
|
*/
|
||||||
|
val duePayableAmount: BigDecimal
|
||||||
|
) {
|
||||||
|
override fun toString() = "${duePayableAmount.toPlainString()} (net: ${taxBasisTotalAmount.toPlainString()}, tax: ${taxTotalAmount.toPlainString()})"
|
||||||
|
}
|
|
@ -6,39 +6,46 @@ import org.mustangproject.ZUGFeRD.ZUGFeRDInvoiceImporter
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
class EInvoiceReader(
|
open class EInvoiceReader(
|
||||||
private val mapper: MustangMapper = MustangMapper()
|
protected open val mapper: MustangMapper = MustangMapper()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun extractFromXml(xmlFile: File) = extractFromXml(xmlFile.inputStream())
|
companion object {
|
||||||
|
val KnownEInvoiceXmlAttachmentNames = listOf(
|
||||||
|
"factur-x.xml", "zugferd-invoice.xml", "xrechnung.xml" // also "ZUGFeRD-invoice.xml" is found but we make compare case insensitive anyway
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun extractFromXml(stream: InputStream) = extractFromXml(stream.reader().readText())
|
|
||||||
|
|
||||||
fun extractFromXml(xml: String): Invoice {
|
open fun extractFromXml(xmlFile: File) = extractFromXml(xmlFile.inputStream())
|
||||||
|
|
||||||
|
open fun extractFromXml(stream: InputStream) = extractFromXml(stream.reader().readText())
|
||||||
|
|
||||||
|
open fun extractFromXml(xml: String): Invoice {
|
||||||
val importer = ZUGFeRDInvoiceImporter() // XRechnungImporter only reads properties but not to a Invoice object
|
val importer = ZUGFeRDInvoiceImporter() // XRechnungImporter only reads properties but not to a Invoice object
|
||||||
importer.fromXML(xml)
|
importer.fromXML(xml)
|
||||||
|
|
||||||
return extractInvoice(importer)
|
return extractInvoice(importer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun extractFromPdf(pdfFile: File) = extractFromPdf(pdfFile.inputStream())
|
open fun extractFromPdf(pdfFile: File) = extractFromPdf(pdfFile.inputStream())
|
||||||
|
|
||||||
fun extractFromPdf(stream: InputStream): Invoice {
|
open fun extractFromPdf(stream: InputStream): Invoice {
|
||||||
val importer = ZUGFeRDInvoiceImporter(stream)
|
val importer = ZUGFeRDInvoiceImporter(stream)
|
||||||
|
|
||||||
return extractInvoice(importer)
|
return extractInvoice(importer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun extractXmlFromPdf(pdfFile: File) = extractXmlFromPdf(pdfFile.inputStream())
|
open fun extractXmlFromPdf(pdfFile: File) = extractXmlFromPdf(pdfFile.inputStream())
|
||||||
|
|
||||||
fun extractXmlFromPdf(stream: InputStream): String {
|
open fun extractXmlFromPdf(stream: InputStream): String {
|
||||||
val importer = ZUGFeRDInvoiceImporter(stream)
|
val importer = ZUGFeRDInvoiceImporter(stream)
|
||||||
|
|
||||||
return String(importer.rawXML, Charsets.UTF_8)
|
return String(importer.rawXML, Charsets.UTF_8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun extractInvoice(importer: ZUGFeRDInvoiceImporter): Invoice {
|
protected open fun extractInvoice(importer: ZUGFeRDInvoiceImporter): Invoice {
|
||||||
val invoice = importer.extractInvoice()
|
val invoice = importer.extractInvoice()
|
||||||
|
|
||||||
// TODO: the values LineTotalAmount, ChargeTotalAmount, AllowanceTotalAmount, TaxBasisTotalAmount, TaxTotalAmount,
|
// TODO: the values LineTotalAmount, ChargeTotalAmount, AllowanceTotalAmount, TaxBasisTotalAmount, TaxTotalAmount,
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
package net.codinux.invoicing.validation
|
package net.codinux.invoicing.validation
|
||||||
|
|
||||||
|
import net.codinux.log.logger
|
||||||
import org.mustangproject.validator.ZUGFeRDValidator
|
import org.mustangproject.validator.ZUGFeRDValidator
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
|
||||||
class EInvoiceValidator {
|
open class EInvoiceValidator {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val SectionField = getPrivateField("section")
|
private val SectionField = getPrivateField("section")
|
||||||
private val CriterionField = getPrivateField("criterion")
|
private val CriterionField = getPrivateField("criterion")
|
||||||
private val StacktraceField = getPrivateField("stacktrace")
|
private val StacktraceField = getPrivateField("stacktrace")
|
||||||
|
|
||||||
|
private val log by logger()
|
||||||
|
|
||||||
private fun getPrivateField(fieldName: String): Field? = try {
|
private fun getPrivateField(fieldName: String): Field? = try {
|
||||||
org.mustangproject.validator.ValidationResultItem::class.java.getDeclaredField(fieldName).apply {
|
org.mustangproject.validator.ValidationResultItem::class.java.getDeclaredField(fieldName).apply {
|
||||||
trySetAccessible()
|
trySetAccessible()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
log.error(e) { "Could not access private field '$fieldName' of Mustang ValidationResultItem" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun validate(fileToValidate: File, disableNotices: Boolean = false): InvoiceValidationResult {
|
open fun validate(fileToValidate: File, disableNotices: Boolean = false): InvoiceValidationResult {
|
||||||
val validator = object : ZUGFeRDValidator() {
|
val validator = object : ZUGFeRDValidator() {
|
||||||
fun getContext() = this.context
|
fun getContext() = this.context
|
||||||
}
|
}
|
||||||
|
@ -42,10 +46,10 @@ class EInvoiceValidator {
|
||||||
return InvoiceValidationResult(validator.wasCompletelyValid(), isXmlValid, xmlValidationResults, report)
|
return InvoiceValidationResult(validator.wasCompletelyValid(), isXmlValid, xmlValidationResults, report)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapValidationResultItem(item: org.mustangproject.validator.ValidationResultItem) =
|
protected open fun mapValidationResultItem(item: org.mustangproject.validator.ValidationResultItem) =
|
||||||
ValidationResultItem(mapSeverity(item), item.message, item.location, SectionField?.get(item) as? Int, CriterionField?.get(item) as? String, StacktraceField?.get(item) as? String)
|
ValidationResultItem(mapSeverity(item), item.message, item.location, SectionField?.get(item) as? Int, CriterionField?.get(item) as? String, StacktraceField?.get(item) as? String)
|
||||||
|
|
||||||
private fun mapSeverity(item: org.mustangproject.validator.ValidationResultItem): ValidationResultSeverity {
|
protected open fun mapSeverity(item: org.mustangproject.validator.ValidationResultItem): ValidationResultSeverity {
|
||||||
var name = item.severity.name
|
var name = item.severity.name
|
||||||
name = name.first().uppercase() + name.substring(1)
|
name = name.first().uppercase() + name.substring(1)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import net.codinux.invoicing.creation.EInvoiceCreator
|
||||||
import net.codinux.invoicing.mail.MailAccount
|
import net.codinux.invoicing.mail.MailAccount
|
||||||
import net.codinux.invoicing.mail.MailReader
|
import net.codinux.invoicing.mail.MailReader
|
||||||
import net.codinux.invoicing.model.Invoice
|
import net.codinux.invoicing.model.Invoice
|
||||||
import net.codinux.invoicing.model.LineItem
|
import net.codinux.invoicing.model.InvoiceItem
|
||||||
import net.codinux.invoicing.model.Party
|
import net.codinux.invoicing.model.Party
|
||||||
import net.codinux.invoicing.reader.EInvoiceReader
|
import net.codinux.invoicing.reader.EInvoiceReader
|
||||||
import net.codinux.invoicing.validation.EInvoiceValidator
|
import net.codinux.invoicing.validation.EInvoiceValidator
|
||||||
|
@ -83,6 +83,6 @@ class Demonstration {
|
||||||
invoicingDate = LocalDate.now(),
|
invoicingDate = LocalDate.now(),
|
||||||
sender = Party("codinux GmbH & Co. KG", "Fun Street 1", "12345", "Glückstadt"),
|
sender = Party("codinux GmbH & Co. KG", "Fun Street 1", "12345", "Glückstadt"),
|
||||||
recipient = Party("Abzock GmbH", "Ausbeutstr.", "12345", "Abzockhausen"),
|
recipient = Party("Abzock GmbH", "Ausbeutstr.", "12345", "Abzockhausen"),
|
||||||
items = listOf(LineItem("Erbrachte Dienstleistungen", "HUR", BigDecimal(170), BigDecimal(1_000_000), BigDecimal(19))) // HUR = EN code for hour
|
items = listOf(InvoiceItem("Erbrachte Dienstleistungen", BigDecimal(170), "HUR", BigDecimal(1_000_000), BigDecimal(19))) // HUR = EN code for hour
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ package net.codinux.invoicing.test
|
||||||
|
|
||||||
import net.codinux.invoicing.model.BankDetails
|
import net.codinux.invoicing.model.BankDetails
|
||||||
import net.codinux.invoicing.model.Invoice
|
import net.codinux.invoicing.model.Invoice
|
||||||
import net.codinux.invoicing.model.LineItem
|
import net.codinux.invoicing.model.InvoiceItem
|
||||||
import net.codinux.invoicing.model.Party
|
import net.codinux.invoicing.model.Party
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
@ -35,10 +35,10 @@ object DataGenerator {
|
||||||
val RecipientBankDetails: BankDetails? = null
|
val RecipientBankDetails: BankDetails? = null
|
||||||
|
|
||||||
const val ItemName = "Erbrachte Dienstleistungen"
|
const val ItemName = "Erbrachte Dienstleistungen"
|
||||||
const val ItemUnit = "HUR" // EN code for 'hour'
|
|
||||||
val ItemQuantity = BigDecimal(1)
|
val ItemQuantity = BigDecimal(1)
|
||||||
val ItemPrice = BigDecimal(99)
|
const val ItemUnit = "HUR" // EN code for 'hour'
|
||||||
val ItemVatPercentage = BigDecimal(19)
|
val ItemUnitPrice = BigDecimal(99)
|
||||||
|
val ItemVatRate = BigDecimal(19)
|
||||||
val ItemDescription: String? = null
|
val ItemDescription: String? = null
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ object DataGenerator {
|
||||||
bankDetails = SenderBankDetails),
|
bankDetails = SenderBankDetails),
|
||||||
recipient: Party = createParty(RecipientName, RecipientStreet, RecipientPostalCode, RecipientCity, RecipientCountry, RecipientVatId, RecipientEmail, RecipientPhone,
|
recipient: Party = createParty(RecipientName, RecipientStreet, RecipientPostalCode, RecipientCity, RecipientCountry, RecipientVatId, RecipientEmail, RecipientPhone,
|
||||||
bankDetails = RecipientBankDetails),
|
bankDetails = RecipientBankDetails),
|
||||||
items: List<LineItem> = listOf(createItem()),
|
items: List<InvoiceItem> = listOf(createItem()),
|
||||||
dueDate: LocalDate? = DueDate,
|
dueDate: LocalDate? = DueDate,
|
||||||
paymentDescription: String? = dueDate?.let { "Zahlbar ohne Abzug bis ${DateTimeFormatter.ofPattern("dd.MM.yyyy").format(dueDate)}" },
|
paymentDescription: String? = dueDate?.let { "Zahlbar ohne Abzug bis ${DateTimeFormatter.ofPattern("dd.MM.yyyy").format(dueDate)}" },
|
||||||
buyerReference: String? = null
|
buyerReference: String? = null
|
||||||
|
@ -71,11 +71,11 @@ object DataGenerator {
|
||||||
|
|
||||||
fun createItem(
|
fun createItem(
|
||||||
name: String = ItemName,
|
name: String = ItemName,
|
||||||
unit: String = ItemUnit,
|
|
||||||
quantity: BigDecimal = ItemQuantity,
|
quantity: BigDecimal = ItemQuantity,
|
||||||
price: BigDecimal = ItemPrice,
|
unit: String = ItemUnit,
|
||||||
vatPercentage: BigDecimal = ItemVatPercentage,
|
unitPrice: BigDecimal = ItemUnitPrice,
|
||||||
|
vatRate: BigDecimal = ItemVatRate,
|
||||||
description: String? = ItemDescription,
|
description: String? = ItemDescription,
|
||||||
) = LineItem(name, unit, quantity, price, vatPercentage, description)
|
) = InvoiceItem(name, quantity, unit, unitPrice, vatRate, description)
|
||||||
|
|
||||||
}
|
}
|
|
@ -4,7 +4,7 @@ import assertk.assertThat
|
||||||
import assertk.assertions.*
|
import assertk.assertions.*
|
||||||
import net.codinux.invoicing.model.BankDetails
|
import net.codinux.invoicing.model.BankDetails
|
||||||
import net.codinux.invoicing.model.Invoice
|
import net.codinux.invoicing.model.Invoice
|
||||||
import net.codinux.invoicing.model.LineItem
|
import net.codinux.invoicing.model.InvoiceItem
|
||||||
import net.codinux.invoicing.model.Party
|
import net.codinux.invoicing.model.Party
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ object InvoiceAsserter {
|
||||||
assertParty(asserter, receiverXPath, DataGenerator.RecipientName, DataGenerator.RecipientStreet, DataGenerator.RecipientPostalCode, DataGenerator.RecipientCity, DataGenerator.RecipientVatId, DataGenerator.RecipientEmail, DataGenerator.RecipientPhone)
|
assertParty(asserter, receiverXPath, DataGenerator.RecipientName, DataGenerator.RecipientStreet, DataGenerator.RecipientPostalCode, DataGenerator.RecipientCity, DataGenerator.RecipientVatId, DataGenerator.RecipientEmail, DataGenerator.RecipientPhone)
|
||||||
|
|
||||||
val lineItemXPath = "//rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem"
|
val lineItemXPath = "//rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem"
|
||||||
assertLineItem(asserter, lineItemXPath, DataGenerator.ItemName, DataGenerator.ItemUnit, DataGenerator.ItemQuantity, DataGenerator.ItemPrice, DataGenerator.ItemVatPercentage, DataGenerator.ItemDescription)
|
assertLineItem(asserter, lineItemXPath, DataGenerator.ItemName, DataGenerator.ItemQuantity, DataGenerator.ItemUnit, DataGenerator.ItemUnitPrice, DataGenerator.ItemVatRate, DataGenerator.ItemDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertParty(asserter: XPathAsserter, partyXPath: String, name: String, street: String, postalCode: String, city: String, vatId: String, email: String, phone: String) {
|
private fun assertParty(asserter: XPathAsserter, partyXPath: String, name: String, street: String, postalCode: String, city: String, vatId: String, email: String, phone: String) {
|
||||||
|
@ -42,14 +42,15 @@ object InvoiceAsserter {
|
||||||
asserter.xpathHasValue("$partyXPath/ram:DefinedTradeContact/ram:TelephoneUniversalCommunication/ram:CompleteNumber", phone)
|
asserter.xpathHasValue("$partyXPath/ram:DefinedTradeContact/ram:TelephoneUniversalCommunication/ram:CompleteNumber", phone)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertLineItem(asserter: XPathAsserter, itemXPath: String, name: String, unit: String, quantity: BigDecimal, price: BigDecimal, vatPercentage: BigDecimal, description: String?) {
|
private fun assertLineItem(asserter: XPathAsserter, itemXPath: String, name: String, quantity: BigDecimal, unit: String, unitPrice: BigDecimal, vatRate: BigDecimal, description: String?) {
|
||||||
asserter.xpathHasValue("$itemXPath/ram:SpecifiedTradeProduct/ram:Name", name)
|
asserter.xpathHasValue("$itemXPath/ram:SpecifiedTradeProduct/ram:Name", name)
|
||||||
|
|
||||||
asserter.xpathHasValue("$itemXPath/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode", unit)
|
asserter.xpathHasValue("$itemXPath/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode", unit)
|
||||||
asserter.xpathHasValue("$itemXPath/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity", quantity, 4)
|
asserter.xpathHasValue("$itemXPath/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity", quantity, 4)
|
||||||
|
|
||||||
asserter.xpathHasValue("$itemXPath/ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount", price, 2)
|
asserter.xpathHasValue("$itemXPath/ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount", unitPrice, 2)
|
||||||
asserter.xpathHasValue("$itemXPath/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent", vatPercentage, 2)
|
asserter.xpathHasValue("$itemXPath/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent",
|
||||||
|
vatRate, 2)
|
||||||
|
|
||||||
// asserter.xpathHasValue("$partyXPath/ram:URIUniversalCommunication/ram:URIID", description)
|
// asserter.xpathHasValue("$partyXPath/ram:URIUniversalCommunication/ram:URIID", description)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +67,7 @@ object InvoiceAsserter {
|
||||||
assertParty(invoice.recipient, DataGenerator.RecipientName, DataGenerator.RecipientStreet, DataGenerator.RecipientPostalCode, DataGenerator.RecipientCity, DataGenerator.RecipientCountry, DataGenerator.RecipientVatId, DataGenerator.RecipientEmail, DataGenerator.RecipientPhone, DataGenerator.RecipientBankDetails)
|
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)
|
assertThat(invoice.items).hasSize(1)
|
||||||
assertLineItem(invoice.items.first(), DataGenerator.ItemName, DataGenerator.ItemUnit, DataGenerator.ItemQuantity, DataGenerator.ItemPrice, DataGenerator.ItemVatPercentage, DataGenerator.ItemDescription)
|
assertLineItem(invoice.items.first(), DataGenerator.ItemName, DataGenerator.ItemQuantity, DataGenerator.ItemUnit, DataGenerator.ItemUnitPrice, DataGenerator.ItemVatRate, DataGenerator.ItemDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertParty(party: Party, name: String, street: String, postalCode: String, city: String, country: String?, vatId: String, email: String, phone: String, bankDetails: BankDetails?) {
|
private fun assertParty(party: Party, name: String, street: String, postalCode: String, city: String, country: String?, vatId: String, email: String, phone: String, bankDetails: BankDetails?) {
|
||||||
|
@ -92,14 +93,14 @@ object InvoiceAsserter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertLineItem(item: LineItem, name: String, unit: String, quantity: BigDecimal, price: BigDecimal, vatPercentage: BigDecimal, description: String?) {
|
private fun assertLineItem(item: InvoiceItem, name: String, quantity: BigDecimal, unit: String, unitPrice: BigDecimal, vatRate: BigDecimal, description: String?) {
|
||||||
assertThat(item.name).isEqualTo(name)
|
assertThat(item.name).isEqualTo(name)
|
||||||
|
|
||||||
assertThat(item.unit).isEqualTo(unit)
|
assertThat(item.unit).isEqualTo(unit)
|
||||||
assertThat(item.quantity).isEqualTo(quantity.setScale(4))
|
assertThat(item.quantity).isEqualTo(quantity.setScale(4))
|
||||||
|
|
||||||
assertThat(item.price).isEqualTo(price.setScale(4))
|
assertThat(item.unitPrice).isEqualTo(unitPrice.setScale(4))
|
||||||
assertThat(item.vatPercentage).isEqualTo(vatPercentage.setScale(2))
|
assertThat(item.vatRate).isEqualTo(vatRate.setScale(2))
|
||||||
|
|
||||||
// assertThat(item.description).isEqualTo(description)
|
// assertThat(item.description).isEqualTo(description)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue