67a69ab073
- Go backend API with full CRUD for all modules - Rust analytics service with parallel processing - C runtime with POSIX shared memory IPC - PostgreSQL schema with 30+ tables - Redis cache, Kafka event streaming - WebSocket hub, automation engine - PDF generation, Resend email integration - JWT auth, multi-tenant - Docker Compose deployment - Nginx reverse proxy Refs: BOC-001
328 lines
7.9 KiB
Go
328 lines
7.9 KiB
Go
package pdf
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jung-kurt/gofpdf"
|
|
)
|
|
|
|
// InvoiceData contains all data needed to generate an invoice PDF
|
|
type InvoiceData struct {
|
|
InvoiceNumber string
|
|
InvoiceDate time.Time
|
|
DueDate time.Time
|
|
CustomerName string
|
|
CustomerAddress string
|
|
CustomerOrgNr string
|
|
Items []InvoiceItem
|
|
Subtotal float64
|
|
VATRate float64
|
|
VATAmount float64
|
|
Total float64
|
|
Currency string
|
|
CompanyName string
|
|
CompanyAddress string
|
|
CompanyOrgNr string
|
|
CompanyBankgiro string
|
|
Notes string
|
|
}
|
|
|
|
// InvoiceItem represents a line item on an invoice
|
|
type InvoiceItem struct {
|
|
Description string
|
|
Quantity float64
|
|
Unit string
|
|
UnitPrice float64
|
|
Total float64
|
|
}
|
|
|
|
// QuoteData contains all data needed to generate a quote PDF
|
|
type QuoteData struct {
|
|
QuoteNumber string
|
|
QuoteDate time.Time
|
|
ValidUntil time.Time
|
|
CustomerName string
|
|
CustomerAddress string
|
|
Items []QuoteItem
|
|
Subtotal float64
|
|
VATRate float64
|
|
VATAmount float64
|
|
Total float64
|
|
Currency string
|
|
CompanyName string
|
|
CompanyAddress string
|
|
Notes string
|
|
}
|
|
|
|
// QuoteItem represents a line item on a quote
|
|
type QuoteItem struct {
|
|
Description string
|
|
Quantity float64
|
|
Unit string
|
|
UnitPrice float64
|
|
Total float64
|
|
}
|
|
|
|
// GenerateInvoice creates a professional invoice PDF
|
|
func GenerateInvoice(data InvoiceData) ([]byte, error) {
|
|
pdf := gofpdf.New("P", "mm", "A4", "")
|
|
pdf.AddPage()
|
|
|
|
// Header with company info
|
|
pdf.SetFont("Arial", "B", 20)
|
|
pdf.SetTextColor(201, 106, 58) // Terracotta
|
|
pdf.Cell(0, 12, "FAKTURA")
|
|
pdf.Ln(8)
|
|
|
|
// Company info
|
|
pdf.SetFont("Arial", "B", 10)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.Cell(0, 5, data.CompanyName)
|
|
pdf.Ln(5)
|
|
pdf.SetFont("Arial", "", 9)
|
|
pdf.Cell(0, 4, data.CompanyAddress)
|
|
pdf.Ln(4)
|
|
if data.CompanyOrgNr != "" {
|
|
pdf.Cell(0, 4, fmt.Sprintf("Org.nr: %s", data.CompanyOrgNr))
|
|
pdf.Ln(4)
|
|
}
|
|
if data.CompanyBankgiro != "" {
|
|
pdf.Cell(0, 4, fmt.Sprintf("Bankgiro: %s", data.CompanyBankgiro))
|
|
pdf.Ln(4)
|
|
}
|
|
pdf.Ln(5)
|
|
|
|
// Invoice details box
|
|
pdf.SetFillColor(250, 248, 245)
|
|
pdf.Rect(130, 30, 70, 35, "F")
|
|
pdf.SetXY(135, 33)
|
|
pdf.SetFont("Arial", "B", 9)
|
|
pdf.SetTextColor(201, 106, 58)
|
|
pdf.Cell(0, 5, "FAKTURAINFORMATION")
|
|
pdf.Ln(6)
|
|
pdf.SetFont("Arial", "", 9)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.SetX(135)
|
|
pdf.Cell(0, 4, fmt.Sprintf("Fakturanr: %s", data.InvoiceNumber))
|
|
pdf.Ln(4)
|
|
pdf.SetX(135)
|
|
pdf.Cell(0, 4, fmt.Sprintf("Datum: %s", data.InvoiceDate.Format("2006-01-02")))
|
|
pdf.Ln(4)
|
|
pdf.SetX(135)
|
|
pdf.Cell(0, 4, fmt.Sprintf("Förfallo: %s", data.DueDate.Format("2006-01-02")))
|
|
pdf.Ln(4)
|
|
pdf.SetX(135)
|
|
pdf.Cell(0, 4, fmt.Sprintf("Valuta: %s", data.Currency))
|
|
pdf.Ln(4)
|
|
|
|
// Customer info
|
|
pdf.SetXY(10, 75)
|
|
pdf.SetFont("Arial", "B", 10)
|
|
pdf.SetTextColor(201, 106, 58)
|
|
pdf.Cell(0, 5, "KUND")
|
|
pdf.Ln(6)
|
|
pdf.SetFont("Arial", "B", 10)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.Cell(0, 5, data.CustomerName)
|
|
pdf.Ln(5)
|
|
pdf.SetFont("Arial", "", 9)
|
|
pdf.Cell(0, 4, data.CustomerAddress)
|
|
pdf.Ln(4)
|
|
if data.CustomerOrgNr != "" {
|
|
pdf.Cell(0, 4, fmt.Sprintf("Org.nr: %s", data.CustomerOrgNr))
|
|
pdf.Ln(4)
|
|
}
|
|
pdf.Ln(10)
|
|
|
|
// Items table header
|
|
pdf.SetFillColor(201, 106, 58)
|
|
pdf.SetTextColor(255, 255, 255)
|
|
pdf.SetFont("Arial", "B", 9)
|
|
pdf.Cell(80, 8, "Beskrivning")
|
|
pdf.Cell(25, 8, "Antal")
|
|
pdf.Cell(25, 8, "Enhet")
|
|
pdf.Cell(30, 8, "Pris")
|
|
pdf.Cell(30, 8, "Belopp")
|
|
pdf.Ln(8)
|
|
|
|
// Items
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.SetFont("Arial", "", 9)
|
|
for i, item := range data.Items {
|
|
if i%2 == 0 {
|
|
pdf.SetFillColor(250, 248, 245)
|
|
pdf.Rect(10, pdf.GetY(), 190, 6, "F")
|
|
}
|
|
pdf.Cell(80, 6, item.Description)
|
|
pdf.Cell(25, 6, fmt.Sprintf("%.2f", item.Quantity))
|
|
pdf.Cell(25, 6, item.Unit)
|
|
pdf.Cell(30, 6, fmt.Sprintf("%.2f", item.UnitPrice))
|
|
pdf.Cell(30, 6, fmt.Sprintf("%.2f", item.Total))
|
|
pdf.Ln(6)
|
|
}
|
|
|
|
pdf.Ln(5)
|
|
|
|
// Totals
|
|
pdf.SetX(120)
|
|
pdf.SetFont("Arial", "", 9)
|
|
pdf.Cell(40, 5, "Delsumma:")
|
|
pdf.Cell(30, 5, fmt.Sprintf("%.2f %s", data.Subtotal, data.Currency))
|
|
pdf.Ln(5)
|
|
pdf.SetX(120)
|
|
pdf.Cell(40, 5, fmt.Sprintf("Moms (%.0f%%):", data.VATRate*100))
|
|
pdf.Cell(30, 5, fmt.Sprintf("%.2f %s", data.VATAmount, data.Currency))
|
|
pdf.Ln(5)
|
|
pdf.SetX(120)
|
|
pdf.SetFont("Arial", "B", 11)
|
|
pdf.SetTextColor(201, 106, 58)
|
|
pdf.Cell(40, 7, "ATT BETALA:")
|
|
pdf.Cell(30, 7, fmt.Sprintf("%.2f %s", data.Total, data.Currency))
|
|
pdf.Ln(10)
|
|
|
|
// Notes
|
|
if data.Notes != "" {
|
|
pdf.SetFont("Arial", "I", 8)
|
|
pdf.SetTextColor(100, 100, 100)
|
|
pdf.MultiCell(0, 4, data.Notes, "", "", false)
|
|
}
|
|
|
|
// Footer
|
|
pdf.SetY(-20)
|
|
pdf.SetFont("Arial", "", 8)
|
|
pdf.SetTextColor(150, 150, 150)
|
|
pdf.Cell(0, 4, fmt.Sprintf("%s | Faktura %s | Sida %d", data.CompanyName, data.InvoiceNumber, pdf.PageNo()))
|
|
|
|
var buf bytes.Buffer
|
|
err := pdf.Output(&buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// GenerateQuote creates a professional quote PDF
|
|
func GenerateQuote(data QuoteData) ([]byte, error) {
|
|
pdf := gofpdf.New("P", "mm", "A4", "")
|
|
pdf.AddPage()
|
|
|
|
// Header
|
|
pdf.SetFont("Arial", "B", 20)
|
|
pdf.SetTextColor(201, 106, 58)
|
|
pdf.Cell(0, 12, "OFFERT")
|
|
pdf.Ln(8)
|
|
|
|
// Company info
|
|
pdf.SetFont("Arial", "B", 10)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.Cell(0, 5, data.CompanyName)
|
|
pdf.Ln(5)
|
|
pdf.SetFont("Arial", "", 9)
|
|
pdf.Cell(0, 4, data.CompanyAddress)
|
|
pdf.Ln(5)
|
|
|
|
// Quote details box
|
|
pdf.SetFillColor(250, 248, 245)
|
|
pdf.Rect(130, 30, 70, 30, "F")
|
|
pdf.SetXY(135, 33)
|
|
pdf.SetFont("Arial", "B", 9)
|
|
pdf.SetTextColor(201, 106, 58)
|
|
pdf.Cell(0, 5, "OFFERTINFORMATION")
|
|
pdf.Ln(6)
|
|
pdf.SetFont("Arial", "", 9)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.SetX(135)
|
|
pdf.Cell(0, 4, fmt.Sprintf("Offertnr: %s", data.QuoteNumber))
|
|
pdf.Ln(4)
|
|
pdf.SetX(135)
|
|
pdf.Cell(0, 4, fmt.Sprintf("Datum: %s", data.QuoteDate.Format("2006-01-02")))
|
|
pdf.Ln(4)
|
|
pdf.SetX(135)
|
|
pdf.Cell(0, 4, fmt.Sprintf("Giltig till: %s", data.ValidUntil.Format("2006-01-02")))
|
|
pdf.Ln(4)
|
|
|
|
// Customer info
|
|
pdf.SetXY(10, 75)
|
|
pdf.SetFont("Arial", "B", 10)
|
|
pdf.SetTextColor(201, 106, 58)
|
|
pdf.Cell(0, 5, "KUND")
|
|
pdf.Ln(6)
|
|
pdf.SetFont("Arial", "B", 10)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.Cell(0, 5, data.CustomerName)
|
|
pdf.Ln(5)
|
|
pdf.SetFont("Arial", "", 9)
|
|
pdf.Cell(0, 4, data.CustomerAddress)
|
|
pdf.Ln(10)
|
|
|
|
// Items table header
|
|
pdf.SetFillColor(201, 106, 58)
|
|
pdf.SetTextColor(255, 255, 255)
|
|
pdf.SetFont("Arial", "B", 9)
|
|
pdf.Cell(80, 8, "Beskrivning")
|
|
pdf.Cell(25, 8, "Antal")
|
|
pdf.Cell(25, 8, "Enhet")
|
|
pdf.Cell(30, 8, "Pris")
|
|
pdf.Cell(30, 8, "Belopp")
|
|
pdf.Ln(8)
|
|
|
|
// Items
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.SetFont("Arial", "", 9)
|
|
for i, item := range data.Items {
|
|
if i%2 == 0 {
|
|
pdf.SetFillColor(250, 248, 245)
|
|
pdf.Rect(10, pdf.GetY(), 190, 6, "F")
|
|
}
|
|
pdf.Cell(80, 6, item.Description)
|
|
pdf.Cell(25, 6, fmt.Sprintf("%.2f", item.Quantity))
|
|
pdf.Cell(25, 6, item.Unit)
|
|
pdf.Cell(30, 6, fmt.Sprintf("%.2f", item.UnitPrice))
|
|
pdf.Cell(30, 6, fmt.Sprintf("%.2f", item.Total))
|
|
pdf.Ln(6)
|
|
}
|
|
|
|
pdf.Ln(5)
|
|
|
|
// Totals
|
|
pdf.SetX(120)
|
|
pdf.SetFont("Arial", "", 9)
|
|
pdf.Cell(40, 5, "Delsumma:")
|
|
pdf.Cell(30, 5, fmt.Sprintf("%.2f %s", data.Subtotal, data.Currency))
|
|
pdf.Ln(5)
|
|
pdf.SetX(120)
|
|
pdf.Cell(40, 5, fmt.Sprintf("Moms (%.0f%%):", data.VATRate*100))
|
|
pdf.Cell(30, 5, fmt.Sprintf("%.2f %s", data.VATAmount, data.Currency))
|
|
pdf.Ln(5)
|
|
pdf.SetX(120)
|
|
pdf.SetFont("Arial", "B", 11)
|
|
pdf.SetTextColor(201, 106, 58)
|
|
pdf.Cell(40, 7, "TOTALT:")
|
|
pdf.Cell(30, 7, fmt.Sprintf("%.2f %s", data.Total, data.Currency))
|
|
pdf.Ln(10)
|
|
|
|
// Notes
|
|
if data.Notes != "" {
|
|
pdf.SetFont("Arial", "I", 8)
|
|
pdf.SetTextColor(100, 100, 100)
|
|
pdf.MultiCell(0, 4, data.Notes, "", "", false)
|
|
}
|
|
|
|
// Footer
|
|
pdf.SetY(-20)
|
|
pdf.SetFont("Arial", "", 8)
|
|
pdf.SetTextColor(150, 150, 150)
|
|
pdf.Cell(0, 4, fmt.Sprintf("%s | Offert %s | Sida %d", data.CompanyName, data.QuoteNumber, pdf.PageNo()))
|
|
|
|
var buf bytes.Buffer
|
|
err := pdf.Output(&buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|