Compare commits
10 Commits
fb5cce224f
...
42f4d09ff3
Author | SHA1 | Date |
---|---|---|
dankito | 42f4d09ff3 | |
dankito | bb48356011 | |
dankito | 3f84e7994a | |
dankito | 59b2999ec2 | |
dankito | 1a967efcbd | |
dankito | 658c02296a | |
dankito | 7ab88390f1 | |
dankito | f96885ca42 | |
dankito | c94b9f8ece | |
dankito | 148fc91b58 |
|
@ -10,21 +10,28 @@ kotlin {
|
|||
|
||||
val mustangVersion: String by project
|
||||
|
||||
val angusMailVersion: String by project
|
||||
|
||||
val klfVersion: String by project
|
||||
|
||||
val assertKVersion: String by project
|
||||
val xunitVersion: String by project
|
||||
val logbackVersion: String by project
|
||||
|
||||
dependencies {
|
||||
implementation("org.mustangproject:library:$mustangVersion")
|
||||
|
||||
implementation("org.eclipse.angus:angus-mail:$angusMailVersion")
|
||||
|
||||
implementation("net.codinux.log:klf:$klfVersion")
|
||||
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
|
||||
implementation("com.willowtreeapps.assertk:assertk:$assertKVersion")
|
||||
testImplementation("com.willowtreeapps.assertk:assertk:$assertKVersion")
|
||||
testImplementation("org.xmlunit:xmlunit-core:$xunitVersion")
|
||||
|
||||
testImplementation("ch.qos.logback:logback-classic:$logbackVersion")
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package net.codinux.invoicing.converter
|
||||
|
||||
import net.codinux.invoicing.creation.EInvoiceCreator
|
||||
import net.codinux.invoicing.model.Invoice
|
||||
import org.mustangproject.CII.CIIToUBL
|
||||
import org.mustangproject.ZUGFeRD.ZUGFeRDVisualizer
|
||||
import java.io.File
|
||||
|
||||
class EInvoiceConverter {
|
||||
|
||||
fun convertInvoiceToHtml(invoice: Invoice, outputFile: File, language: ZUGFeRDVisualizer.Language = ZUGFeRDVisualizer.Language.DE) =
|
||||
convertInvoiceToHtml(createXRechnungXml(invoice), outputFile, language)
|
||||
|
||||
fun convertInvoiceToHtml(invoiceXml: String, outputFile: File, language: ZUGFeRDVisualizer.Language = ZUGFeRDVisualizer.Language.DE): String {
|
||||
val xmlFile = File.createTempFile("Zugferd", ".xml")
|
||||
.also { it.writeText(invoiceXml) }
|
||||
|
||||
val visualizer = ZUGFeRDVisualizer()
|
||||
|
||||
val html = visualizer.visualize(xmlFile.absolutePath, language)
|
||||
|
||||
outputFile.writeText(html)
|
||||
copyResource("xrechnung-viewer.css", outputFile, ".css")
|
||||
copyResource("xrechnung-viewer.js", outputFile, ".js")
|
||||
|
||||
xmlFile.delete()
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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))
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// TODO: extract a common method for this
|
||||
val xmlFile = File.createTempFile("Zugferd", ".xml")
|
||||
.also { it.writeText(invoiceXml) }
|
||||
val ublFile = File(xmlFile.parentFile, xmlFile.nameWithoutExtension + "-ubl.xml")
|
||||
|
||||
convertCiiToUbl(xmlFile, ublFile)
|
||||
|
||||
val ubl = ublFile.readText()
|
||||
|
||||
xmlFile.delete()
|
||||
ublFile.delete()
|
||||
|
||||
return ubl
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
val cii2Ubl = CIIToUBL()
|
||||
cii2Ubl.convert(xmlFile, outputFile)
|
||||
}
|
||||
|
||||
|
||||
private fun createXRechnungXml(invoice: Invoice): String = EInvoiceCreator().createXRechnungXml(invoice)
|
||||
|
||||
private fun copyResource(resourceName: String, outputFile: File, outputFileExtension: String) {
|
||||
javaClass.classLoader.getResourceAsStream(resourceName).use {
|
||||
it?.copyTo(File(outputFile.parentFile, outputFile.nameWithoutExtension + outputFileExtension).outputStream())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
package net.codinux.invoicing.creation
|
||||
|
||||
import net.codinux.invoicing.mapper.MustangMapper
|
||||
import net.codinux.invoicing.model.Invoice
|
||||
import org.mustangproject.ZUGFeRD.IXMLProvider
|
||||
import org.mustangproject.ZUGFeRD.Profiles
|
||||
import org.mustangproject.ZUGFeRD.ZUGFeRD2PullProvider
|
||||
import org.mustangproject.ZUGFeRD.ZUGFeRDExporterFromA3
|
||||
import org.mustangproject.ZUGFeRD.*
|
||||
import java.io.File
|
||||
|
||||
class EInvoiceCreator(
|
||||
private val mapper: MustangMapper = MustangMapper()
|
||||
|
@ -17,14 +16,40 @@ class EInvoiceCreator(
|
|||
return createXml(provider, invoice)
|
||||
}
|
||||
|
||||
fun createZugferdXml(invoice: Invoice, zugferdVersion: Int = 2): String {
|
||||
fun createZugferdXml(invoice: Invoice): String {
|
||||
val exporter = ZUGFeRDExporterFromA3()
|
||||
.setZUGFeRDVersion(zugferdVersion)
|
||||
.setProfile("EN16931")
|
||||
.setProfile("EN16931") // required for XML?
|
||||
|
||||
return createXml(exporter.provider, invoice)
|
||||
}
|
||||
|
||||
fun createZugferdPdf(invoice: Invoice, outputFile: File) {
|
||||
val xml = createZugferdXml(invoice)
|
||||
val xmlFile = File.createTempFile(outputFile.nameWithoutExtension, ".xml")
|
||||
.also { it.writeText(xml) }
|
||||
val pdfFile = File(xmlFile.parentFile, xmlFile.nameWithoutExtension + ".pdf")
|
||||
|
||||
val visualizer = ZUGFeRDVisualizer()
|
||||
visualizer.toPDF(xmlFile.absolutePath, pdfFile.absolutePath)
|
||||
|
||||
combinePdfAndXml(pdfFile, xml, outputFile)
|
||||
|
||||
xmlFile.delete()
|
||||
pdfFile.delete()
|
||||
}
|
||||
|
||||
fun combinePdfAndXml(pdfFile: File, xml: String, outputFile: File) {
|
||||
val exporter = ZUGFeRDExporterFromA3()
|
||||
.setZUGFeRDVersion(2)
|
||||
.setProfile("EN16931") // available values: MINIMUM, BASICWL, BASIC, CIUS, EN16931, EXTENDED, XRECHNUNG
|
||||
// .disableFacturX()
|
||||
.setProducer("danki die geile Sau")
|
||||
.setCreator(System.getProperty("user.name"))
|
||||
|
||||
return createXml(exporter.provider, invoice)
|
||||
exporter.load(pdfFile.inputStream())
|
||||
exporter.setXML(xml.toByteArray())
|
||||
|
||||
exporter.export(outputFile.outputStream())
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package net.codinux.invoicing.mail
|
||||
|
||||
class MailAccount(
|
||||
val username: String,
|
||||
val password: String,
|
||||
/**
|
||||
* For reading mails the IMAP server address, for sending mails the SMTP server address.
|
||||
*/
|
||||
val serverAddress: String,
|
||||
/**
|
||||
* Even though not mandatory it's better to specify the port, otherwise default port is tried.
|
||||
*/
|
||||
val port: Int? = null
|
||||
) {
|
||||
override fun toString() = "$username $serverAddress${port?.let { ":$it" } ?: ""}"
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package net.codinux.invoicing.mail
|
||||
|
||||
import net.codinux.invoicing.model.Invoice
|
||||
import java.io.File
|
||||
|
||||
class MailAttachmentWithEInvoice(
|
||||
val filename: String,
|
||||
val contentType: String,
|
||||
val invoice: Invoice,
|
||||
val file: File
|
||||
) {
|
||||
override fun toString() = "$filename: $invoice"
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package net.codinux.invoicing.mail
|
||||
|
||||
import jakarta.mail.BodyPart
|
||||
import jakarta.mail.Folder
|
||||
import jakarta.mail.Part
|
||||
import jakarta.mail.Session
|
||||
import jakarta.mail.internet.MimeMultipart
|
||||
import net.codinux.invoicing.model.Invoice
|
||||
import net.codinux.invoicing.reader.EInvoiceReader
|
||||
import net.codinux.log.logger
|
||||
import java.io.File
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
|
||||
class MailReader(
|
||||
private val eInvoiceReader: EInvoiceReader = EInvoiceReader()
|
||||
) {
|
||||
|
||||
private val extractionErrorMessages = mutableSetOf<String?>()
|
||||
|
||||
private val extractionErrors = mutableSetOf<Throwable>()
|
||||
|
||||
private val log by logger()
|
||||
|
||||
|
||||
fun listAllMessagesWithEInvoice(account: MailAccount): List<MailWithInvoice> {
|
||||
val properties = mapAccountToJavaMailProperties(account)
|
||||
|
||||
try {
|
||||
val session = Session.getInstance(properties)
|
||||
session.getStore("imap").use { store ->
|
||||
store.connect(account.serverAddress, account.username, account.password)
|
||||
|
||||
val inbox = store.getFolder("INBOX")
|
||||
inbox.open(Folder.READ_ONLY)
|
||||
|
||||
return listAllMessagesWithEInvoiceInFolder(inbox).also {
|
||||
inbox.close(false)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not read mails of account $account" }
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun mapAccountToJavaMailProperties(account: MailAccount) = Properties().apply {
|
||||
put("mail.store.protocol", "imap")
|
||||
|
||||
put("mail.imap.host", account.serverAddress)
|
||||
put("mail.imap.port", account.port?.toString() ?: "993") // Default IMAP over SSL
|
||||
put("mail.imap.ssl.enable", "true")
|
||||
|
||||
put("mail.imap.connectiontimeout", "5000")
|
||||
put("mail.imap.timeout", "5000")
|
||||
}
|
||||
|
||||
|
||||
private fun listAllMessagesWithEInvoiceInFolder(folder: Folder): List<MailWithInvoice> = folder.messages.mapNotNull { message ->
|
||||
try {
|
||||
if (message.isMimeType("multipart/*")) {
|
||||
val multipart = message.content as MimeMultipart
|
||||
val parts = IntRange(0, multipart.count - 1).map { multipart.getBodyPart(it) }
|
||||
|
||||
val attachmentsWithEInvoice = parts.mapNotNull { part ->
|
||||
findEInvoice(part)
|
||||
}
|
||||
|
||||
if (attachmentsWithEInvoice.isNotEmpty()) {
|
||||
return@mapNotNull MailWithInvoice(message.from.joinToString(), message.subject, map(message.sentDate), attachmentsWithEInvoice)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not read mail $message" }
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
private fun findEInvoice(part: BodyPart): MailAttachmentWithEInvoice? {
|
||||
try {
|
||||
if (part.disposition == Part.ATTACHMENT) {
|
||||
val invoice = tryToReadEInvoice(part)
|
||||
if (invoice != null) {
|
||||
var contentType = part.contentType
|
||||
val indexOfSeparator = contentType.indexOf(';')
|
||||
if (indexOfSeparator > -1) {
|
||||
contentType = contentType.substring(0, indexOfSeparator)
|
||||
}
|
||||
|
||||
val filename = File(part.fileName)
|
||||
val file = File.createTempFile(filename.nameWithoutExtension, filename.extension).also { file ->
|
||||
part.inputStream.use { it.copyTo(file.outputStream()) }
|
||||
}
|
||||
|
||||
return MailAttachmentWithEInvoice(part.fileName, contentType, invoice, file)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Could not check attachment '${part.fileName}' for eInvoice" }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun tryToReadEInvoice(part: BodyPart): Invoice? {
|
||||
val filename = part.fileName.lowercase()
|
||||
val contentType = part.contentType.lowercase()
|
||||
|
||||
return if (filename.endsWith(".pdf") || contentType.startsWith("application/pdf") || contentType.startsWith("application/octet-stream")) {
|
||||
try {
|
||||
eInvoiceReader.extractFromPdf(part.inputStream)
|
||||
} catch (e: Throwable) {
|
||||
extractionErrorMessages.add(e.message)
|
||||
extractionErrors.add(e)
|
||||
null
|
||||
}
|
||||
} else if (filename.endsWith(".xml") || contentType.startsWith("application/xml") || contentType.startsWith("text/xml")) {
|
||||
eInvoiceReader.readFromXml(part.inputStream)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: same code as in MustangMapper
|
||||
private fun map(date: Date): LocalDate =
|
||||
date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package net.codinux.invoicing.mail
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
class MailWithInvoice(
|
||||
val sender: String,
|
||||
val subject: String,
|
||||
val date: LocalDate,
|
||||
val attachmentsWithEInvoice: List<MailAttachmentWithEInvoice>
|
||||
) {
|
||||
override fun toString() = "$date $sender: $subject, ${attachmentsWithEInvoice.size} invoices"
|
||||
}
|
|
@ -29,7 +29,7 @@ class MustangMapper {
|
|||
fun mapParty(party: Party): TradeParty = TradeParty(
|
||||
party.name, party.street, party.postalCode, party.city, party.countryIsoCode
|
||||
).apply {
|
||||
this.taxID = party.vatId
|
||||
this.setVATID(party.vatId)
|
||||
// TODO: description?
|
||||
|
||||
this.email = party.email
|
||||
|
@ -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()
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
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(xmlFile: File) = readFromXml(xmlFile.inputStream())
|
||||
|
||||
fun readFromXml(stream: InputStream) = readFromXml(stream.reader().readText())
|
||||
|
||||
fun readFromXml(xml: String): Invoice {
|
||||
val importer = ZUGFeRDInvoiceImporter() // XRechnungImporter only reads properties but not to a Invoice object
|
||||
importer.fromXML(xml)
|
||||
|
||||
return extractInvoice(importer)
|
||||
}
|
||||
|
||||
fun extractFromPdf(pdfFile: File) = extractFromPdf(pdfFile.inputStream())
|
||||
|
||||
fun extractFromPdf(stream: InputStream): Invoice {
|
||||
val importer = ZUGFeRDInvoiceImporter(stream)
|
||||
|
||||
return extractInvoice(importer)
|
||||
}
|
||||
|
||||
fun extractXmlFromPdf(pdfFile: File) = extractXmlFromPdf(pdfFile.inputStream())
|
||||
|
||||
fun extractXmlFromPdf(stream: InputStream): String {
|
||||
val importer = ZUGFeRDInvoiceImporter(stream)
|
||||
|
||||
return String(importer.rawXML, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package net.codinux.invoicing.converter
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isNotEmpty
|
||||
import net.codinux.invoicing.test.DataGenerator
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
|
||||
class EInvoiceConverterTest {
|
||||
|
||||
private val underTest = EInvoiceConverter()
|
||||
|
||||
|
||||
@Test
|
||||
fun convertInvoiceToHtml() {
|
||||
val invoice = createInvoice()
|
||||
val testFile = File.createTempFile("Zugferd", ".html")
|
||||
|
||||
val result = underTest.convertInvoiceToHtml(invoice, testFile)
|
||||
|
||||
assertThat(result).isNotEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun convertCiiToUbl() {
|
||||
val invoice = createInvoice()
|
||||
|
||||
val result = underTest.convertCiiToUbl(invoice)
|
||||
|
||||
assertThat(result).isNotEmpty()
|
||||
}
|
||||
|
||||
|
||||
private fun createInvoice() = DataGenerator.createInvoice()
|
||||
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
package net.codinux.invoicing.creation
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isNotEmpty
|
||||
import net.codinux.invoicing.test.DataGenerator
|
||||
import net.codinux.invoicing.test.XPathAsserter
|
||||
import java.math.BigDecimal
|
||||
import net.codinux.invoicing.test.InvoiceAsserter
|
||||
import org.mustangproject.ZUGFeRD.ZUGFeRDInvoiceImporter
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
|
||||
class EInvoiceCreatorTest {
|
||||
|
@ -30,51 +29,24 @@ class EInvoiceCreatorTest {
|
|||
assertInvoiceXml(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createZugferdPdf() {
|
||||
val invoice = createInvoice()
|
||||
val testFile = File.createTempFile("Zugferd", ".pdf")
|
||||
|
||||
underTest.createZugferdPdf(invoice, testFile)
|
||||
|
||||
val importer = ZUGFeRDInvoiceImporter(testFile.inputStream())
|
||||
val xml = String(importer.rawXML, Charsets.UTF_8)
|
||||
|
||||
assertInvoiceXml(xml)
|
||||
}
|
||||
|
||||
|
||||
private fun createInvoice() = DataGenerator.createInvoice()
|
||||
|
||||
private fun assertInvoiceXml(xml: String) {
|
||||
assertThat(xml).isNotEmpty()
|
||||
|
||||
val asserter = XPathAsserter(xml)
|
||||
|
||||
asserter.xpathHasValue("//rsm:ExchangedDocument/ram:ID", DataGenerator.InvoiceNumber)
|
||||
asserter.xpathHasValue("//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString", DataGenerator.InvoicingDate.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 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 lineItemXPath = "//rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem"
|
||||
assertLineItem(asserter, lineItemXPath, DataGenerator.ItemName, DataGenerator.ItemUnit, DataGenerator.ItemQuantity, DataGenerator.ItemPrice, DataGenerator.ItemVat, DataGenerator.ItemDescription)
|
||||
}
|
||||
|
||||
private fun assertParty(asserter: XPathAsserter, partyXPath: String, name: String, street: 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:PostcodeCode", postalCode)
|
||||
asserter.xpathHasValue("$partyXPath/ram:PostalTradeAddress/ram:CityName", city)
|
||||
|
||||
asserter.xpathHasValue("$partyXPath/ram:SpecifiedTaxRegistration/ram:ID", vatId)
|
||||
|
||||
asserter.xpathHasValue("$partyXPath/ram:URIUniversalCommunication/ram:URIID", email)
|
||||
asserter.xpathHasValue("$partyXPath/ram:DefinedTradeContact/ram:EmailURIUniversalCommunication/ram:URIID", email)
|
||||
asserter.xpathHasValue("$partyXPath/ram:DefinedTradeContact/ram:TelephoneUniversalCommunication/ram:CompleteNumber", phone)
|
||||
}
|
||||
|
||||
private fun assertLineItem(asserter: XPathAsserter, partyXPath: String, name: String, unit: String, quantity: BigDecimal, price: BigDecimal, vatPercentage: BigDecimal, description: String?) {
|
||||
asserter.xpathHasValue("$partyXPath/ram:SpecifiedTradeProduct/ram:Name", name)
|
||||
|
||||
asserter.xpathHasValue("$partyXPath/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode", unit)
|
||||
asserter.xpathHasValue("$partyXPath/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity", quantity, 4)
|
||||
|
||||
asserter.xpathHasValue("$partyXPath/ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount", price, 2)
|
||||
asserter.xpathHasValue("$partyXPath/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent", vatPercentage, 2)
|
||||
|
||||
// asserter.xpathHasValue("$partyXPath/ram:URIUniversalCommunication/ram:URIID", description)
|
||||
InvoiceAsserter.assertInvoiceXml(xml)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package net.codinux.invoicing.mail
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isNotEmpty
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore // not an automatic test, set your mail account settings below
|
||||
class MailReaderTest {
|
||||
|
||||
companion object {
|
||||
// specify your mail account here
|
||||
private val mailAccount = MailAccount(
|
||||
username = "",
|
||||
password = "",
|
||||
serverAddress = "",
|
||||
port = null // can be left as null if default port 993 is used
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private val underTest = MailReader()
|
||||
|
||||
|
||||
@Test
|
||||
fun listAllMessagesWithEInvoice() {
|
||||
val result = underTest.listAllMessagesWithEInvoice(mailAccount)
|
||||
|
||||
assertThat(result).isNotEmpty()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package net.codinux.invoicing.reader
|
||||
|
||||
import net.codinux.invoicing.model.Invoice
|
||||
import net.codinux.invoicing.test.InvoiceAsserter
|
||||
import java.io.InputStream
|
||||
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)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun extractXmlFromPdf() {
|
||||
val result = underTest.extractXmlFromPdf(getTestFile("ZUGFeRD.pdf"))
|
||||
|
||||
InvoiceAsserter.assertInvoiceXml(result)
|
||||
}
|
||||
|
||||
|
||||
private fun getTestFile(filename: String): InputStream =
|
||||
this.javaClass.classLoader.getResourceAsStream("files/$filename")!!
|
||||
|
||||
private fun assertInvoice(invoice: Invoice?) {
|
||||
InvoiceAsserter.assertInvoice(invoice)
|
||||
}
|
||||
|
||||
}
|
|
@ -19,27 +19,26 @@ object DataGenerator {
|
|||
const val SenderPostalCode = "12345"
|
||||
const val SenderCity = "Glückstadt"
|
||||
const val SenderCountry = "DE"
|
||||
const val SenderVatId = "DE12345678"
|
||||
const val SenderVatId = "DE123456789"
|
||||
const val SenderEmail = "working-class-hero@rock.me"
|
||||
const val SenderPhone = "+4917012345678"
|
||||
const val SenderAccountId = "DE00123456780987654321"
|
||||
const val SenderBankCode = "12345678"
|
||||
const val SenderAccountHolderName = "Manuela Musterfrau"
|
||||
val SenderBankDetails = BankDetails("DE00123456780987654321", "ABZODEFFXXX", "Manuela Musterfrau")
|
||||
|
||||
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 = "DE87654321"
|
||||
const val RecipientVatId = "DE987654321"
|
||||
const val RecipientEmail = "exploiter@your.boss"
|
||||
const val RecipientPhone = "+4912345678"
|
||||
const val RecipientPhone = "+491234567890"
|
||||
val RecipientBankDetails: BankDetails? = null
|
||||
|
||||
const val ItemName = "Erbrachte Dienstleistungen"
|
||||
const val ItemUnit = "HUR" // EN code for 'hour'
|
||||
val ItemQuantity = BigDecimal(1)
|
||||
val ItemPrice = BigDecimal(99)
|
||||
val ItemVat = BigDecimal(0.19)
|
||||
val ItemVatPercentage = BigDecimal(19)
|
||||
val ItemDescription: String? = null
|
||||
|
||||
|
||||
|
@ -47,8 +46,9 @@ object DataGenerator {
|
|||
invoiceNumber: String = InvoiceNumber,
|
||||
invoicingDate: LocalDate = InvoicingDate,
|
||||
sender: Party = createParty(SenderName, SenderStreet, SenderPostalCode, SenderCity, SenderCountry, SenderVatId, SenderEmail, SenderPhone,
|
||||
bankDetails = BankDetails(SenderAccountId, SenderBankCode, SenderAccountHolderName)),
|
||||
recipient: Party = createParty(RecipientName, RecipientStreet, RecipientPostalCode, RecipientCity, RecipientCountry, RecipientVatId, RecipientEmail, RecipientPhone),
|
||||
bankDetails = SenderBankDetails),
|
||||
recipient: Party = createParty(RecipientName, RecipientStreet, RecipientPostalCode, RecipientCity, RecipientCountry, RecipientVatId, RecipientEmail, RecipientPhone,
|
||||
bankDetails = RecipientBankDetails),
|
||||
items: List<LineItem> = listOf(createItem()),
|
||||
dueDate: LocalDate? = DueDate,
|
||||
paymentDescription: String? = dueDate?.let { "Zahlbar ohne Abzug bis ${DateTimeFormatter.ofPattern("dd.MM.yyyy").format(dueDate)}" },
|
||||
|
@ -74,7 +74,7 @@ object DataGenerator {
|
|||
unit: String = ItemUnit,
|
||||
quantity: BigDecimal = ItemQuantity,
|
||||
price: BigDecimal = ItemPrice,
|
||||
vatPercentage: BigDecimal = ItemVat,
|
||||
vatPercentage: BigDecimal = ItemVatPercentage,
|
||||
description: String? = ItemDescription,
|
||||
) = LineItem(name, unit, quantity, price, vatPercentage, description)
|
||||
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package net.codinux.invoicing.test
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.*
|
||||
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 java.math.BigDecimal
|
||||
|
||||
object InvoiceAsserter {
|
||||
|
||||
fun assertInvoiceXml(xml: String) {
|
||||
assertThat(xml).isNotEmpty()
|
||||
|
||||
val asserter = XPathAsserter(xml)
|
||||
|
||||
asserter.xpathHasValue("//rsm:ExchangedDocument/ram:ID", DataGenerator.InvoiceNumber)
|
||||
asserter.xpathHasValue("//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString", DataGenerator.InvoicingDate.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 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 lineItemXPath = "//rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem"
|
||||
assertLineItem(asserter, lineItemXPath, DataGenerator.ItemName, DataGenerator.ItemUnit, DataGenerator.ItemQuantity, DataGenerator.ItemPrice, DataGenerator.ItemVatPercentage, DataGenerator.ItemDescription)
|
||||
}
|
||||
|
||||
private fun assertParty(asserter: XPathAsserter, partyXPath: String, name: String, street: 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:PostcodeCode", postalCode)
|
||||
asserter.xpathHasValue("$partyXPath/ram:PostalTradeAddress/ram:CityName", city)
|
||||
|
||||
asserter.xpathHasValue("$partyXPath/ram:SpecifiedTaxRegistration/ram:ID", vatId)
|
||||
|
||||
asserter.xpathHasValue("$partyXPath/ram:URIUniversalCommunication/ram:URIID", email)
|
||||
asserter.xpathHasValue("$partyXPath/ram:DefinedTradeContact/ram:EmailURIUniversalCommunication/ram:URIID", email)
|
||||
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?) {
|
||||
asserter.xpathHasValue("$itemXPath/ram:SpecifiedTradeProduct/ram:Name", name)
|
||||
|
||||
asserter.xpathHasValue("$itemXPath/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode", unit)
|
||||
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:ApplicableTradeTax/ram:RateApplicablePercent", vatPercentage, 2)
|
||||
|
||||
// asserter.xpathHasValue("$partyXPath/ram:URIUniversalCommunication/ram:URIID", description)
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100">
|
||||
<!-- generated by: mustangproject.org vnull-->
|
||||
<rsm:ExchangedDocumentContext>
|
||||
|
||||
<ram:BusinessProcessSpecifiedDocumentContextParameter>
|
||||
|
||||
<ram:ID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</ram:ID>
|
||||
|
||||
</ram:BusinessProcessSpecifiedDocumentContextParameter>
|
||||
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>12345</ram:ID>
|
||||
<ram:TypeCode>380</ram:TypeCode>
|
||||
<ram:IssueDateTime>
|
||||
<udt:DateTimeString format="102">20151021</udt:DateTimeString>
|
||||
</ram:IssueDateTime>
|
||||
</rsm:ExchangedDocument>
|
||||
<rsm:SupplyChainTradeTransaction>
|
||||
<ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:AssociatedDocumentLineDocument>
|
||||
<ram:LineID>1</ram:LineID>
|
||||
</ram:AssociatedDocumentLineDocument>
|
||||
<ram:SpecifiedTradeProduct>
|
||||
<ram:Name>Erbrachte Dienstleistungen</ram:Name>
|
||||
</ram:SpecifiedTradeProduct>
|
||||
<ram:SpecifiedLineTradeAgreement>
|
||||
<ram:NetPriceProductTradePrice>
|
||||
<ram:ChargeAmount>99.0000</ram:ChargeAmount>
|
||||
<ram:BasisQuantity unitCode="HUR">1.0000</ram:BasisQuantity>
|
||||
</ram:NetPriceProductTradePrice>
|
||||
</ram:SpecifiedLineTradeAgreement>
|
||||
<ram:SpecifiedLineTradeDelivery>
|
||||
<ram:BilledQuantity unitCode="HUR">1.0000</ram:BilledQuantity>
|
||||
</ram:SpecifiedLineTradeDelivery>
|
||||
<ram:SpecifiedLineTradeSettlement>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount>99.00</ram:LineTotalAmount>
|
||||
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
</ram:SpecifiedLineTradeSettlement>
|
||||
</ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:SellerTradeParty>
|
||||
<ram:Name>Hochwürdiger Leistungserbringer</ram:Name>
|
||||
<ram:DefinedTradeContact>
|
||||
<ram:TelephoneUniversalCommunication>
|
||||
<ram:CompleteNumber>+4917012345678</ram:CompleteNumber>
|
||||
</ram:TelephoneUniversalCommunication>
|
||||
<ram:EmailURIUniversalCommunication>
|
||||
<ram:URIID>working-class-hero@rock.me</ram:URIID>
|
||||
</ram:EmailURIUniversalCommunication>
|
||||
</ram:DefinedTradeContact>
|
||||
<ram:PostalTradeAddress>
|
||||
<ram:PostcodeCode>12345</ram:PostcodeCode>
|
||||
<ram:LineOne>Fun Street 1</ram:LineOne>
|
||||
<ram:CityName>Glückstadt</ram:CityName>
|
||||
<ram:CountryID>DE</ram:CountryID>
|
||||
</ram:PostalTradeAddress>
|
||||
<ram:URIUniversalCommunication>
|
||||
<ram:URIID schemeID="EM">working-class-hero@rock.me</ram:URIID>
|
||||
</ram:URIUniversalCommunication>
|
||||
<ram:SpecifiedTaxRegistration>
|
||||
<ram:ID schemeID="VA">DE123456789</ram:ID>
|
||||
</ram:SpecifiedTaxRegistration>
|
||||
</ram:SellerTradeParty>
|
||||
<ram:BuyerTradeParty>
|
||||
<ram:Name>Untertänigster Leistungsempfänger</ram:Name>
|
||||
<ram:DefinedTradeContact>
|
||||
<ram:TelephoneUniversalCommunication>
|
||||
<ram:CompleteNumber>+491234567890</ram:CompleteNumber>
|
||||
</ram:TelephoneUniversalCommunication>
|
||||
<ram:EmailURIUniversalCommunication>
|
||||
<ram:URIID>exploiter@your.boss</ram:URIID>
|
||||
</ram:EmailURIUniversalCommunication>
|
||||
</ram:DefinedTradeContact>
|
||||
<ram:PostalTradeAddress>
|
||||
<ram:PostcodeCode>12345</ram:PostcodeCode>
|
||||
<ram:LineOne>Party Street 1</ram:LineOne>
|
||||
<ram:CityName>Glückstadt</ram:CityName>
|
||||
<ram:CountryID>DE</ram:CountryID>
|
||||
</ram:PostalTradeAddress>
|
||||
<ram:URIUniversalCommunication>
|
||||
<ram:URIID schemeID="EM">exploiter@your.boss</ram:URIID>
|
||||
</ram:URIUniversalCommunication>
|
||||
<ram:SpecifiedTaxRegistration>
|
||||
<ram:ID schemeID="VA">DE987654321</ram:ID>
|
||||
</ram:SpecifiedTaxRegistration>
|
||||
</ram:BuyerTradeParty>
|
||||
</ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:ApplicableHeaderTradeDelivery/>
|
||||
<ram:ApplicableHeaderTradeSettlement>
|
||||
<ram:PaymentReference>12345</ram:PaymentReference>
|
||||
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
|
||||
<ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:TypeCode>58</ram:TypeCode>
|
||||
<ram:Information>SEPA credit transfer</ram:Information>
|
||||
<ram:PayeePartyCreditorFinancialAccount>
|
||||
<ram:IBANID>DE00123456780987654321</ram:IBANID>
|
||||
<ram:AccountName>Manuela Musterfrau</ram:AccountName>
|
||||
</ram:PayeePartyCreditorFinancialAccount>
|
||||
<ram:PayeeSpecifiedCreditorFinancialInstitution>
|
||||
<ram:BICID>ABZODEFFXXX</ram:BICID>
|
||||
</ram:PayeeSpecifiedCreditorFinancialInstitution>
|
||||
</ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:CalculatedAmount>18.81</ram:CalculatedAmount>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:BasisAmount>99.00</ram:BasisAmount>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradePaymentTerms>
|
||||
<ram:Description>Zahlbar ohne Abzug bis 15.06.2016</ram:Description>
|
||||
<ram:DueDateDateTime>
|
||||
<udt:DateTimeString format="102">20160615</udt:DateTimeString>
|
||||
</ram:DueDateDateTime>
|
||||
</ram:SpecifiedTradePaymentTerms>
|
||||
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
<ram:LineTotalAmount>99.00</ram:LineTotalAmount>
|
||||
<ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>
|
||||
<ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>
|
||||
<ram:TaxBasisTotalAmount>99.00</ram:TaxBasisTotalAmount>
|
||||
<ram:TaxTotalAmount currencyID="EUR">18.81</ram:TaxTotalAmount>
|
||||
<ram:GrandTotalAmount>117.81</ram:GrandTotalAmount>
|
||||
<ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
|
||||
<ram:DuePayableAmount>117.81</ram:DuePayableAmount>
|
||||
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
</ram:ApplicableHeaderTradeSettlement>
|
||||
</rsm:SupplyChainTradeTransaction>
|
||||
</rsm:CrossIndustryInvoice>
|
Binary file not shown.
|
@ -0,0 +1,134 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100">
|
||||
<!-- generated by: mustangproject.org vnull-->
|
||||
<rsm:ExchangedDocumentContext>
|
||||
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>12345</ram:ID>
|
||||
<ram:TypeCode>380</ram:TypeCode>
|
||||
<ram:IssueDateTime>
|
||||
<udt:DateTimeString format="102">20151021</udt:DateTimeString>
|
||||
</ram:IssueDateTime>
|
||||
</rsm:ExchangedDocument>
|
||||
<rsm:SupplyChainTradeTransaction>
|
||||
<ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:AssociatedDocumentLineDocument>
|
||||
<ram:LineID>1</ram:LineID>
|
||||
</ram:AssociatedDocumentLineDocument>
|
||||
<ram:SpecifiedTradeProduct>
|
||||
<ram:Name>Erbrachte Dienstleistungen</ram:Name>
|
||||
</ram:SpecifiedTradeProduct>
|
||||
<ram:SpecifiedLineTradeAgreement>
|
||||
<ram:NetPriceProductTradePrice>
|
||||
<ram:ChargeAmount>99.0000</ram:ChargeAmount>
|
||||
<ram:BasisQuantity unitCode="HUR">1.0000</ram:BasisQuantity>
|
||||
</ram:NetPriceProductTradePrice>
|
||||
</ram:SpecifiedLineTradeAgreement>
|
||||
<ram:SpecifiedLineTradeDelivery>
|
||||
<ram:BilledQuantity unitCode="HUR">1.0000</ram:BilledQuantity>
|
||||
</ram:SpecifiedLineTradeDelivery>
|
||||
<ram:SpecifiedLineTradeSettlement>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount>99.00</ram:LineTotalAmount>
|
||||
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
</ram:SpecifiedLineTradeSettlement>
|
||||
</ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:SellerTradeParty>
|
||||
<ram:Name>Hochwürdiger Leistungserbringer</ram:Name>
|
||||
<ram:DefinedTradeContact>
|
||||
<ram:TelephoneUniversalCommunication>
|
||||
<ram:CompleteNumber>+4917012345678</ram:CompleteNumber>
|
||||
</ram:TelephoneUniversalCommunication>
|
||||
<ram:EmailURIUniversalCommunication>
|
||||
<ram:URIID>working-class-hero@rock.me</ram:URIID>
|
||||
</ram:EmailURIUniversalCommunication>
|
||||
</ram:DefinedTradeContact>
|
||||
<ram:PostalTradeAddress>
|
||||
<ram:PostcodeCode>12345</ram:PostcodeCode>
|
||||
<ram:LineOne>Fun Street 1</ram:LineOne>
|
||||
<ram:CityName>Glückstadt</ram:CityName>
|
||||
<ram:CountryID>DE</ram:CountryID>
|
||||
</ram:PostalTradeAddress>
|
||||
<ram:URIUniversalCommunication>
|
||||
<ram:URIID schemeID="EM">working-class-hero@rock.me</ram:URIID>
|
||||
</ram:URIUniversalCommunication>
|
||||
<ram:SpecifiedTaxRegistration>
|
||||
<ram:ID schemeID="VA">DE123456789</ram:ID>
|
||||
</ram:SpecifiedTaxRegistration>
|
||||
</ram:SellerTradeParty>
|
||||
<ram:BuyerTradeParty>
|
||||
<ram:Name>Untertänigster Leistungsempfänger</ram:Name>
|
||||
<ram:DefinedTradeContact>
|
||||
<ram:TelephoneUniversalCommunication>
|
||||
<ram:CompleteNumber>+491234567890</ram:CompleteNumber>
|
||||
</ram:TelephoneUniversalCommunication>
|
||||
<ram:EmailURIUniversalCommunication>
|
||||
<ram:URIID>exploiter@your.boss</ram:URIID>
|
||||
</ram:EmailURIUniversalCommunication>
|
||||
</ram:DefinedTradeContact>
|
||||
<ram:PostalTradeAddress>
|
||||
<ram:PostcodeCode>12345</ram:PostcodeCode>
|
||||
<ram:LineOne>Party Street 1</ram:LineOne>
|
||||
<ram:CityName>Glückstadt</ram:CityName>
|
||||
<ram:CountryID>DE</ram:CountryID>
|
||||
</ram:PostalTradeAddress>
|
||||
<ram:URIUniversalCommunication>
|
||||
<ram:URIID schemeID="EM">exploiter@your.boss</ram:URIID>
|
||||
</ram:URIUniversalCommunication>
|
||||
<ram:SpecifiedTaxRegistration>
|
||||
<ram:ID schemeID="VA">DE987654321</ram:ID>
|
||||
</ram:SpecifiedTaxRegistration>
|
||||
</ram:BuyerTradeParty>
|
||||
</ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:ApplicableHeaderTradeDelivery/>
|
||||
<ram:ApplicableHeaderTradeSettlement>
|
||||
<ram:PaymentReference>12345</ram:PaymentReference>
|
||||
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
|
||||
<ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:TypeCode>58</ram:TypeCode>
|
||||
<ram:Information>SEPA credit transfer</ram:Information>
|
||||
<ram:PayeePartyCreditorFinancialAccount>
|
||||
<ram:IBANID>DE00123456780987654321</ram:IBANID>
|
||||
<ram:AccountName>Manuela Musterfrau</ram:AccountName>
|
||||
</ram:PayeePartyCreditorFinancialAccount>
|
||||
<ram:PayeeSpecifiedCreditorFinancialInstitution>
|
||||
<ram:BICID>ABZODEFFXXX</ram:BICID>
|
||||
</ram:PayeeSpecifiedCreditorFinancialInstitution>
|
||||
</ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:CalculatedAmount>18.81</ram:CalculatedAmount>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:BasisAmount>99.00</ram:BasisAmount>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradePaymentTerms>
|
||||
<ram:Description>Zahlbar ohne Abzug bis 15.06.2016</ram:Description>
|
||||
<ram:DueDateDateTime>
|
||||
<udt:DateTimeString format="102">20160615</udt:DateTimeString>
|
||||
</ram:DueDateDateTime>
|
||||
</ram:SpecifiedTradePaymentTerms>
|
||||
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
<ram:LineTotalAmount>99.00</ram:LineTotalAmount>
|
||||
<ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>
|
||||
<ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>
|
||||
<ram:TaxBasisTotalAmount>99.00</ram:TaxBasisTotalAmount>
|
||||
<ram:TaxTotalAmount currencyID="EUR">18.81</ram:TaxTotalAmount>
|
||||
<ram:GrandTotalAmount>117.81</ram:GrandTotalAmount>
|
||||
<ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
|
||||
<ram:DuePayableAmount>117.81</ram:DuePayableAmount>
|
||||
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
</ram:ApplicableHeaderTradeSettlement>
|
||||
</rsm:SupplyChainTradeTransaction>
|
||||
</rsm:CrossIndustryInvoice>
|
|
@ -0,0 +1,24 @@
|
|||
<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>
|
||||
|
||||
</configuration>
|
|
@ -7,7 +7,11 @@ kotlinVersion=1.9.25
|
|||
|
||||
mustangVersion=2.14.2
|
||||
|
||||
angusMailVersion=2.0.3
|
||||
|
||||
klfVersion=1.6.2
|
||||
# only used for tests
|
||||
logbackVersion=1.5.12
|
||||
|
||||
assertKVersion=0.28.1
|
||||
xunitVersion=2.10.0
|
Loading…
Reference in New Issue