diff --git a/internal/driver/pdf/html.go b/internal/driver/pdf/html.go
index 6d11b2a..a7d6543 100644
--- a/internal/driver/pdf/html.go
+++ b/internal/driver/pdf/html.go
@@ -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
+}