Add PDF invoice generation: implement backend API, introduce PdfService, update frontend repository and UI with download logic, and mark roadmap task complete.
This commit is contained in:
@@ -21,6 +21,7 @@ dependencies {
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
implementation(libs.spring.boot.starter.actuator)
|
||||
implementation(libs.jackson.module.kotlin)
|
||||
implementation(libs.openpdf)
|
||||
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||
implementation(libs.micrometer.tracing.bridge.brave)
|
||||
implementation(libs.zipkin.reporter.brave)
|
||||
|
||||
+18
-1
@@ -5,11 +5,14 @@ package at.mocode.billing.api.rest
|
||||
import at.mocode.billing.domain.model.Buchung
|
||||
import at.mocode.billing.domain.model.BuchungsTyp
|
||||
import at.mocode.billing.domain.model.TeilnehmerKonto
|
||||
import at.mocode.billing.service.PdfService
|
||||
import at.mocode.billing.service.TeilnehmerKontoService
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import jakarta.validation.Valid
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.NotNull
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import kotlin.time.Instant
|
||||
@@ -20,7 +23,8 @@ import kotlinx.serialization.Serializable
|
||||
@RestController
|
||||
@RequestMapping("/api/billing")
|
||||
class BillingController(
|
||||
private val kontoService: TeilnehmerKontoService
|
||||
private val kontoService: TeilnehmerKontoService,
|
||||
private val pdfService: PdfService
|
||||
) {
|
||||
|
||||
data class KontoDto(
|
||||
@@ -106,6 +110,19 @@ class BillingController(
|
||||
return ResponseEntity.ok(konto.toDto())
|
||||
}
|
||||
|
||||
@GetMapping("/konten/{kontoId}/rechnung", produces = [MediaType.APPLICATION_PDF_VALUE])
|
||||
fun downloadRechnung(@PathVariable kontoId: String): ResponseEntity<ByteArray> {
|
||||
val uuid = try { Uuid.parse(kontoId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
|
||||
val konto = kontoService.getKontoById(uuid) ?: return ResponseEntity.notFound().build()
|
||||
|
||||
val pdf = pdfService.generateRechnung(konto)
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"rechnung_${konto.personName.replace(" ", "_")}.pdf\"")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(pdf)
|
||||
}
|
||||
|
||||
private fun TeilnehmerKonto.toDto() = KontoDto(
|
||||
kontoId = kontoId.toString(),
|
||||
veranstaltungId = veranstaltungId.toString(),
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.billing.service
|
||||
|
||||
import at.mocode.billing.domain.model.TeilnehmerKonto
|
||||
import at.mocode.billing.domain.repository.BuchungRepository
|
||||
import com.lowagie.text.*
|
||||
import com.lowagie.text.pdf.PdfPCell
|
||||
import com.lowagie.text.pdf.PdfPTable
|
||||
import com.lowagie.text.pdf.PdfWriter
|
||||
import org.springframework.stereotype.Service
|
||||
import java.awt.Color
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
@Service
|
||||
class PdfService(
|
||||
private val buchungRepository: BuchungRepository
|
||||
) {
|
||||
|
||||
fun generateRechnung(konto: TeilnehmerKonto): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
val document = Document(PageSize.A4)
|
||||
PdfWriter.getInstance(document, out)
|
||||
|
||||
document.open()
|
||||
|
||||
// Header
|
||||
val titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 18f)
|
||||
val header = Paragraph("Rechnung / Kontoauszug", titleFont)
|
||||
header.alignment = Element.ALIGN_CENTER
|
||||
header.spacingAfter = 20f
|
||||
document.add(header)
|
||||
|
||||
// Teilnehmer Info
|
||||
val infoFont = FontFactory.getFont(FontFactory.HELVETICA, 12f)
|
||||
document.add(Paragraph("Teilnehmer: ${konto.personName}", infoFont))
|
||||
document.add(Paragraph("Datum: ${java.time.LocalDate.now()}", infoFont))
|
||||
document.add(Paragraph("Konto-ID: ${konto.kontoId}", infoFont))
|
||||
document.add(Paragraph("Veranstaltung: ${konto.veranstaltungId}", infoFont))
|
||||
document.add(Paragraph(" ", infoFont))
|
||||
|
||||
// Tabelle
|
||||
val table = PdfPTable(4)
|
||||
table.widthPercentage = 100f
|
||||
table.setWidths(floatArrayOf(2f, 4f, 2f, 2f))
|
||||
|
||||
val headFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 11f)
|
||||
|
||||
fun addCell(text: String, font: Font = headFont, bgColor: Color? = Color.LIGHT_GRAY) {
|
||||
val cell = PdfPCell(Phrase(text, font))
|
||||
if (bgColor != null) cell.backgroundColor = bgColor
|
||||
cell.setPadding(5f)
|
||||
table.addCell(cell)
|
||||
}
|
||||
|
||||
addCell("Datum")
|
||||
addCell("Zweck")
|
||||
addCell("Typ")
|
||||
addCell("Betrag")
|
||||
|
||||
val buchungen = buchungRepository.findByKonto(konto.kontoId)
|
||||
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.GERMANY)
|
||||
val bodyFont = FontFactory.getFont(FontFactory.HELVETICA, 10f)
|
||||
|
||||
buchungen.forEach { b ->
|
||||
addCell(b.gebuchtAm.toString().substring(0, 10), bodyFont, null)
|
||||
addCell(b.verwendungszweck, bodyFont, null)
|
||||
addCell(b.typ.name, bodyFont, null)
|
||||
val betragStr = currencyFormat.format(b.betragCent / 100.0)
|
||||
addCell(betragStr, bodyFont, null)
|
||||
}
|
||||
|
||||
document.add(table)
|
||||
|
||||
// Saldo
|
||||
val saldoFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 14f)
|
||||
val saldoPara = Paragraph(" ", saldoFont)
|
||||
saldoPara.spacingBefore = 20f
|
||||
document.add(saldoPara)
|
||||
|
||||
val saldoText = "Gesamtsaldo: ${currencyFormat.format(konto.saldoCent / 100.0)}"
|
||||
val finalSaldo = Paragraph(saldoText, saldoFont)
|
||||
finalSaldo.alignment = Element.ALIGN_RIGHT
|
||||
document.add(finalSaldo)
|
||||
|
||||
document.close()
|
||||
return out.toByteArray()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user