mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-06 02:36:49 +00:00
feat(pdf): redesign invoice PDF layout and structure
Refactored the invoice PDF generation to use a modular, visually improved layout. Extracted header, details, items, payments, summary, and notes sections into dedicated functions for clarity and maintainability. Introduced color constants and improved table formatting for better readability. Updated labels and formatting to enhance the overall appearance and user experience of generated invoices.
This commit is contained in:
parent
0f327c814a
commit
24f289b767
1 changed files with 246 additions and 61 deletions
|
|
@ -15,76 +15,46 @@ import (
|
|||
|
||||
type HTMLPdfClient struct{}
|
||||
|
||||
const (
|
||||
invoiceMarginX = 10.0
|
||||
invoiceContentWidth = 190.0
|
||||
invoiceHeaderHeight = 28.0
|
||||
invoiceTableRowH = 8.5
|
||||
invoiceSummaryLineH = 10.0
|
||||
invoiceNotesPadding = 3.0
|
||||
)
|
||||
|
||||
type rgbColor struct {
|
||||
r int
|
||||
g int
|
||||
b int
|
||||
}
|
||||
|
||||
var (
|
||||
invoiceTeal = rgbColor{0, 123, 143}
|
||||
invoiceLightGray = rgbColor{244, 244, 244}
|
||||
invoiceMidGray = rgbColor{221, 221, 221}
|
||||
invoiceTextGray = rgbColor{85, 85, 85}
|
||||
invoiceNoteBg = rgbColor{237, 247, 249}
|
||||
)
|
||||
|
||||
func NewPdfClient() (*HTMLPdfClient, error) {
|
||||
return &HTMLPdfClient{}, nil
|
||||
}
|
||||
|
||||
func (pc *HTMLPdfClient) BuildInvoice(data booking.Invoice) (*booking.GeneratedFile, error) {
|
||||
pdf := gofpdf.New("P", "mm", "A4", "")
|
||||
pdf.SetMargins(16, 16, 16)
|
||||
pdf.SetAutoPageBreak(true, 16)
|
||||
pdf.SetMargins(invoiceMarginX, 12, invoiceMarginX)
|
||||
pdf.SetAutoPageBreak(true, 14)
|
||||
pdf.AddPage()
|
||||
|
||||
tr := pdf.UnicodeTranslatorFromDescriptor("")
|
||||
writeLine := func(fontStyle string, fontSize float64, text string) {
|
||||
pdf.SetFont("Helvetica", fontStyle, fontSize)
|
||||
pdf.CellFormat(0, 7, tr(text), "", 1, "", false, 0, "")
|
||||
}
|
||||
|
||||
writeLine("B", 20, fmt.Sprintf("Invoice %s", data.ID))
|
||||
pdf.Ln(2)
|
||||
writeLine("", 11, data.Host.Name)
|
||||
writeLine("", 11, data.Host.Address)
|
||||
writeLine("", 11, fmt.Sprintf("%s %s", data.Host.ZipCode, data.Host.City))
|
||||
writeLine("", 11, data.Host.PhoneNumber)
|
||||
writeLine("", 11, data.Host.Email)
|
||||
|
||||
pdf.Ln(4)
|
||||
writeLine("B", 14, "Guest")
|
||||
writeLine("", 11, data.Name)
|
||||
if data.PhoneNumber != "" {
|
||||
writeLine("", 11, data.PhoneNumber)
|
||||
}
|
||||
writeLine("", 11, fmt.Sprintf("Stay: %s - %s", data.From, data.To))
|
||||
writeLine("", 11, fmt.Sprintf("Platform: %s", data.Platform))
|
||||
writeLine("", 11, fmt.Sprintf("Guests: %d", data.CustomersNumber))
|
||||
|
||||
pdf.Ln(4)
|
||||
writeLine("B", 14, "Charges")
|
||||
pdf.SetFont("Helvetica", "B", 11)
|
||||
pdf.CellFormat(90, 8, tr("Item"), "1", 0, "", false, 0, "")
|
||||
pdf.CellFormat(25, 8, tr("Qty"), "1", 0, "C", false, 0, "")
|
||||
pdf.CellFormat(35, 8, tr("Unit"), "1", 0, "R", false, 0, "")
|
||||
pdf.CellFormat(25, 8, tr("Total"), "1", 1, "R", false, 0, "")
|
||||
pdf.SetFont("Helvetica", "", 11)
|
||||
for _, line := range data.Lines {
|
||||
pdf.CellFormat(90, 8, tr(line.Name), "1", 0, "", false, 0, "")
|
||||
pdf.CellFormat(25, 8, fmt.Sprintf("%d", line.Quantity), "1", 0, "C", false, 0, "")
|
||||
pdf.CellFormat(35, 8, fmt.Sprintf("%s EUR", line.Price), "1", 0, "R", false, 0, "")
|
||||
pdf.CellFormat(25, 8, fmt.Sprintf("%s EUR", line.Total), "1", 1, "R", false, 0, "")
|
||||
}
|
||||
|
||||
pdf.Ln(4)
|
||||
writeLine("B", 14, "Payments")
|
||||
if len(data.Payments) == 0 {
|
||||
writeLine("", 11, "No payment recorded")
|
||||
} else {
|
||||
pdf.SetFont("Helvetica", "B", 11)
|
||||
pdf.CellFormat(45, 8, tr("Date"), "1", 0, "", false, 0, "")
|
||||
pdf.CellFormat(80, 8, tr("Method"), "1", 0, "", false, 0, "")
|
||||
pdf.CellFormat(50, 8, tr("Amount"), "1", 1, "R", false, 0, "")
|
||||
pdf.SetFont("Helvetica", "", 11)
|
||||
for _, payment := range data.Payments {
|
||||
pdf.CellFormat(45, 8, tr(payment.Date), "1", 0, "", false, 0, "")
|
||||
pdf.CellFormat(80, 8, tr(payment.Method), "1", 0, "", false, 0, "")
|
||||
pdf.CellFormat(50, 8, fmt.Sprintf("%s EUR", payment.Amount), "1", 1, "R", false, 0, "")
|
||||
}
|
||||
}
|
||||
|
||||
pdf.Ln(4)
|
||||
writeLine("B", 12, fmt.Sprintf("Total: %s EUR", data.Total))
|
||||
writeLine("", 12, fmt.Sprintf("Paid: %s EUR", data.AmountPaid))
|
||||
writeLine("B", 12, fmt.Sprintf("Balance due: %s EUR", data.BalanceDue))
|
||||
drawInvoiceHeader(pdf, tr, data)
|
||||
drawInvoiceDetails(pdf, tr, data)
|
||||
drawInvoiceItemsTable(pdf, tr, data)
|
||||
drawInvoicePayments(pdf, tr, data)
|
||||
drawInvoiceSummary(pdf, tr, data)
|
||||
drawInvoiceNotes(pdf, tr, data)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pdf.Output(&buf); err != nil {
|
||||
|
|
@ -118,3 +88,218 @@ func (pc *HTMLPdfClient) BuildReport(report booking.ReportData, period string, m
|
|||
|
||||
return outputPath, nil
|
||||
}
|
||||
|
||||
func drawInvoiceHeader(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
|
||||
pdf.SetFillColor(invoiceTeal.r, invoiceTeal.g, invoiceTeal.b)
|
||||
pdf.Rect(invoiceMarginX, 12, invoiceContentWidth, invoiceHeaderHeight, "F")
|
||||
|
||||
if logo, err := assets.Static.ReadFile("assets/img/logo.png"); err == nil {
|
||||
info := pdf.RegisterImageOptionsReader(
|
||||
"invoice-logo",
|
||||
gofpdf.ImageOptions{ImageType: "PNG"},
|
||||
bytes.NewReader(logo),
|
||||
)
|
||||
if info != nil {
|
||||
pdf.ImageOptions("invoice-logo", 14, 15.5, 17, 17, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "")
|
||||
}
|
||||
}
|
||||
|
||||
pdf.SetTextColor(255, 255, 255)
|
||||
pdf.SetXY(110, 14.5)
|
||||
pdf.SetFont("Helvetica", "B", 11)
|
||||
pdf.CellFormat(86, 4.8, tr(data.Host.Name), "", 1, "R", false, 0, "")
|
||||
pdf.SetFont("Helvetica", "", 8.8)
|
||||
hostLines := []string{
|
||||
data.Host.Address,
|
||||
fmt.Sprintf("%s %s", data.Host.ZipCode, data.Host.City),
|
||||
fmt.Sprintf("Tel : %s", data.Host.PhoneNumber),
|
||||
fmt.Sprintf("Mail : %s", data.Host.Email),
|
||||
}
|
||||
for _, line := range hostLines {
|
||||
pdf.SetX(110)
|
||||
pdf.CellFormat(86, 4.0, tr(line), "", 1, "R", false, 0, "")
|
||||
}
|
||||
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pdf.SetDrawColor(invoiceMidGray.r, invoiceMidGray.g, invoiceMidGray.b)
|
||||
pdf.Line(invoiceMarginX, 45, invoiceMarginX+invoiceContentWidth, 45)
|
||||
pdf.SetY(49)
|
||||
}
|
||||
|
||||
func drawInvoiceDetails(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
|
||||
leftX := invoiceMarginX + 4
|
||||
rightX := 103.0
|
||||
labelColor := invoiceTextGray
|
||||
|
||||
writeDetail := func(x float64, label, value string, boldValue bool) {
|
||||
pdf.SetX(x)
|
||||
pdf.SetFont("Helvetica", "B", 10.5)
|
||||
pdf.SetTextColor(labelColor.r, labelColor.g, labelColor.b)
|
||||
pdf.CellFormat(38, 7, tr(label), "", 0, "", false, 0, "")
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
style := ""
|
||||
if boldValue {
|
||||
style = "B"
|
||||
}
|
||||
pdf.SetFont("Helvetica", style, 10.5)
|
||||
pdf.CellFormat(48, 7, tr(value), "", 1, "", false, 0, "")
|
||||
}
|
||||
|
||||
pdf.SetFont("Helvetica", "B", 12)
|
||||
pdf.SetTextColor(invoiceTextGray.r, invoiceTextGray.g, invoiceTextGray.b)
|
||||
pdf.SetX(leftX)
|
||||
pdf.CellFormat(86, 7, tr(data.Name), "", 0, "", false, 0, "")
|
||||
pdf.SetX(rightX)
|
||||
pdf.CellFormat(86, 7, tr("N° de facture :"), "", 0, "", false, 0, "")
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pdf.SetFont("Helvetica", "", 11)
|
||||
pdf.CellFormat(20, 7, tr(data.ID), "", 1, "R", false, 0, "")
|
||||
|
||||
writeDetail(leftX, "Tel :", emptyFallback(data.PhoneNumber), false)
|
||||
writeDetail(rightX, "Du :", data.From, false)
|
||||
|
||||
writeDetail(leftX, "Nombre de clients :", fmt.Sprintf("%d", data.CustomersNumber), false)
|
||||
writeDetail(rightX, "Au :", data.To, false)
|
||||
|
||||
writeDetail(leftX, "Plateforme :", data.Platform, false)
|
||||
writeDetail(rightX, "Montant Total :", euroAmount(data.Total), false)
|
||||
|
||||
pdf.Ln(4)
|
||||
pdf.SetDrawColor(invoiceMidGray.r, invoiceMidGray.g, invoiceMidGray.b)
|
||||
pdf.Line(invoiceMarginX, pdf.GetY(), invoiceMarginX+invoiceContentWidth, pdf.GetY())
|
||||
pdf.Ln(5)
|
||||
}
|
||||
|
||||
func drawInvoiceItemsTable(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
|
||||
widths := []float64{70, 28, 39, 53}
|
||||
headers := []string{"Objet", "Quantité", "Prix (€)", "Total (€)"}
|
||||
|
||||
writeTableHeader(pdf, tr, widths, headers)
|
||||
pdf.SetFont("Helvetica", "", 10.5)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
|
||||
for i, line := range data.Lines {
|
||||
fill := i%2 == 0
|
||||
if fill {
|
||||
pdf.SetFillColor(invoiceLightGray.r, invoiceLightGray.g, invoiceLightGray.b)
|
||||
}
|
||||
pdf.CellFormat(widths[0], invoiceTableRowH, tr(line.Name), "1", 0, "", fill, 0, "")
|
||||
pdf.CellFormat(widths[1], invoiceTableRowH, fmt.Sprintf("%d", line.Quantity), "1", 0, "C", fill, 0, "")
|
||||
pdf.CellFormat(widths[2], invoiceTableRowH, line.Price, "1", 0, "R", fill, 0, "")
|
||||
pdf.CellFormat(widths[3], invoiceTableRowH, line.Total, "1", 1, "R", fill, 0, "")
|
||||
}
|
||||
|
||||
pdf.Ln(6)
|
||||
}
|
||||
|
||||
func drawInvoicePayments(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
|
||||
pdf.SetFont("Helvetica", "B", 13)
|
||||
pdf.SetTextColor(invoiceTeal.r, invoiceTeal.g, invoiceTeal.b)
|
||||
pdf.CellFormat(0, 7, tr("Historique des Paiements"), "", 1, "", false, 0, "")
|
||||
pdf.Ln(1)
|
||||
|
||||
widths := []float64{54, 84, 52}
|
||||
headers := []string{"Date", "Mode de Paiement", "Montant (€)"}
|
||||
writeTableHeader(pdf, tr, widths, headers)
|
||||
|
||||
pdf.SetFont("Helvetica", "", 10.5)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
if len(data.Payments) == 0 {
|
||||
pdf.SetFillColor(invoiceLightGray.r, invoiceLightGray.g, invoiceLightGray.b)
|
||||
pdf.CellFormat(widths[0], invoiceTableRowH, tr("-"), "1", 0, "", true, 0, "")
|
||||
pdf.CellFormat(widths[1], invoiceTableRowH, tr("Aucun paiement enregistré"), "1", 0, "", true, 0, "")
|
||||
pdf.CellFormat(widths[2], invoiceTableRowH, tr("0.00"), "1", 1, "R", true, 0, "")
|
||||
} else {
|
||||
for i, payment := range data.Payments {
|
||||
fill := i%2 == 0
|
||||
if fill {
|
||||
pdf.SetFillColor(invoiceLightGray.r, invoiceLightGray.g, invoiceLightGray.b)
|
||||
}
|
||||
pdf.CellFormat(widths[0], invoiceTableRowH, tr(payment.Date), "1", 0, "", fill, 0, "")
|
||||
pdf.CellFormat(widths[1], invoiceTableRowH, tr(payment.Method), "1", 0, "", fill, 0, "")
|
||||
pdf.CellFormat(widths[2], invoiceTableRowH, payment.Amount, "1", 1, "R", fill, 0, "")
|
||||
}
|
||||
}
|
||||
|
||||
pdf.Ln(6)
|
||||
}
|
||||
|
||||
func drawInvoiceSummary(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
|
||||
labels := []string{"Montant Total :", "Montant Payé :", "Solde Restant :"}
|
||||
values := []string{data.Total, data.AmountPaid, data.BalanceDue}
|
||||
|
||||
for i := range labels {
|
||||
pdf.SetX(invoiceMarginX + 3)
|
||||
pdf.SetFont("Helvetica", "B", 11)
|
||||
pdf.SetTextColor(invoiceTextGray.r, invoiceTextGray.g, invoiceTextGray.b)
|
||||
pdf.CellFormat(60, invoiceSummaryLineH, tr(labels[i]), "", 0, "", false, 0, "")
|
||||
pdf.SetFont("Helvetica", "", 11)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pdf.CellFormat(95, invoiceSummaryLineH, "", "", 0, "", false, 0, "")
|
||||
pdf.CellFormat(32, invoiceSummaryLineH, values[i], "", 1, "R", false, 0, "")
|
||||
}
|
||||
|
||||
pdf.Ln(2)
|
||||
pdf.SetDrawColor(invoiceMidGray.r, invoiceMidGray.g, invoiceMidGray.b)
|
||||
pdf.Line(invoiceMarginX, pdf.GetY(), invoiceMarginX+invoiceContentWidth, pdf.GetY())
|
||||
pdf.Ln(6)
|
||||
}
|
||||
|
||||
func drawInvoiceNotes(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
|
||||
notes := "Notes\nTVA non applicable, art. 293 B du CGI\nDispensé d'immatriculation au registre du commerce et des sociétés (RCS) et au répertoire des métiers.\nConditions de paiement : paiement à réception de facture. Aucun escompte consenti pour règlement anticipé ou désistement. Tout incident de paiement est passible d'intérêts de retard. Le montant des pénalités résulte de l'application aux sommes restant dues d'un taux d'intérêt légal en vigueur au moment de l'incident.\nIl sera demandé 15€ par jour par personne supplémentaire."
|
||||
|
||||
pdf.SetFont("Helvetica", "", 7.5)
|
||||
lines := pdf.SplitLines([]byte(tr(notes)), invoiceContentWidth-(invoiceNotesPadding*2))
|
||||
noteBoxHeight := float64(len(lines))*3.2 + invoiceNotesPadding*2
|
||||
totalBlockHeight := noteBoxHeight + 18
|
||||
if pdf.GetY()+totalBlockHeight > 282 {
|
||||
pdf.AddPage()
|
||||
}
|
||||
|
||||
y := pdf.GetY()
|
||||
pdf.SetFillColor(invoiceNoteBg.r, invoiceNoteBg.g, invoiceNoteBg.b)
|
||||
pdf.Rect(invoiceMarginX, y, invoiceContentWidth, noteBoxHeight, "F")
|
||||
pdf.SetXY(invoiceMarginX+invoiceNotesPadding, y+invoiceNotesPadding)
|
||||
pdf.SetTextColor(invoiceTextGray.r, invoiceTextGray.g, invoiceTextGray.b)
|
||||
pdf.MultiCell(invoiceContentWidth-(invoiceNotesPadding*2), 3.2, tr(notes), "", "L", false)
|
||||
|
||||
pdf.Ln(4)
|
||||
pdf.SetDrawColor(invoiceMidGray.r, invoiceMidGray.g, invoiceMidGray.b)
|
||||
pdf.Line(invoiceMarginX, pdf.GetY(), invoiceMarginX+invoiceContentWidth, pdf.GetY())
|
||||
pdf.Ln(4)
|
||||
|
||||
pdf.SetTextColor(invoiceTextGray.r, invoiceTextGray.g, invoiceTextGray.b)
|
||||
pdf.SetFont("Helvetica", "B", 11)
|
||||
pdf.CellFormat(0, 5, tr("Total"), "", 1, "R", false, 0, "")
|
||||
pdf.SetTextColor(invoiceTeal.r, invoiceTeal.g, invoiceTeal.b)
|
||||
pdf.SetFont("Helvetica", "B", 15)
|
||||
pdf.CellFormat(0, 7, tr(euroAmount(data.Total)), "", 0, "R", false, 0, "")
|
||||
}
|
||||
|
||||
func writeTableHeader(pdf *gofpdf.Fpdf, tr func(string) string, widths []float64, headers []string) {
|
||||
pdf.SetFillColor(invoiceTeal.r, invoiceTeal.g, invoiceTeal.b)
|
||||
pdf.SetTextColor(255, 255, 255)
|
||||
pdf.SetFont("Helvetica", "B", 10)
|
||||
alignments := []string{"L", "C", "R", "R"}
|
||||
if len(widths) == 3 {
|
||||
alignments = []string{"L", "L", "R"}
|
||||
}
|
||||
for i, header := range headers {
|
||||
end := 0
|
||||
if i == len(headers)-1 {
|
||||
end = 1
|
||||
}
|
||||
pdf.CellFormat(widths[i], 8, tr(header), "1", end, alignments[i], true, 0, "")
|
||||
}
|
||||
}
|
||||
|
||||
func euroAmount(value string) string {
|
||||
return fmt.Sprintf("%s €", value)
|
||||
}
|
||||
|
||||
func emptyFallback(value string) string {
|
||||
if value == "" {
|
||||
return "-"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue