feat: atualizar fluxo OT, dashboard, transacoes e PDFs
This commit is contained in:
+102
-21
@@ -1,9 +1,14 @@
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pdf/fpdf"
|
||||
)
|
||||
@@ -15,6 +20,7 @@ type DocMeta struct {
|
||||
CompanyIBAN string
|
||||
CompanyPhone string
|
||||
CompanyEmail string
|
||||
CompanyLogo string
|
||||
DocType string // "Fatura" or "Orçamento"
|
||||
DocNumber string // e.g. "FAT/2026/001"
|
||||
IssuedAt string // e.g. "30/06/2026"
|
||||
@@ -39,44 +45,52 @@ func Generate(meta DocMeta, items []LineItem, staffTotal float64, outPath string
|
||||
f := fpdf.New("P", "mm", "A4", "")
|
||||
f.AddPage()
|
||||
f.SetMargins(15, 15, 15)
|
||||
tr := f.UnicodeTranslatorFromDescriptor("")
|
||||
t := func(s string) string { return tr(s) }
|
||||
|
||||
hasLogo := addLogo(f, meta.CompanyLogo)
|
||||
leftW := 120.0
|
||||
if hasLogo {
|
||||
leftW = 100.0
|
||||
}
|
||||
|
||||
// Header
|
||||
f.SetFont("Helvetica", "B", 18)
|
||||
f.CellFormat(120, 10, meta.CompanyName, "", 0, "L", false, 0, "")
|
||||
f.CellFormat(leftW, 10, t(meta.CompanyName), "", 0, "L", false, 0, "")
|
||||
f.SetFont("Helvetica", "B", 14)
|
||||
f.CellFormat(60, 10, meta.DocType, "", 1, "R", false, 0, "")
|
||||
f.CellFormat(60, 10, t(meta.DocType), "", 1, "R", false, 0, "")
|
||||
|
||||
f.SetFont("Helvetica", "", 9)
|
||||
if meta.CompanyNIF != "" {
|
||||
f.CellFormat(120, 5, "NIF: "+meta.CompanyNIF, "", 0, "L", false, 0, "")
|
||||
f.CellFormat(leftW, 5, t("NIF: ")+meta.CompanyNIF, "", 0, "L", false, 0, "")
|
||||
} else {
|
||||
f.CellFormat(120, 5, "", "", 0, "L", false, 0, "")
|
||||
f.CellFormat(leftW, 5, "", "", 0, "L", false, 0, "")
|
||||
}
|
||||
f.SetFont("Helvetica", "", 11)
|
||||
f.CellFormat(60, 5, meta.DocNumber, "", 1, "R", false, 0, "")
|
||||
f.CellFormat(60, 5, t(meta.DocNumber), "", 1, "R", false, 0, "")
|
||||
|
||||
f.SetFont("Helvetica", "", 9)
|
||||
if meta.CompanyAddress != "" {
|
||||
f.MultiCell(120, 5, meta.CompanyAddress, "", "L", false)
|
||||
f.MultiCell(leftW, 5, t(meta.CompanyAddress), "", "L", false)
|
||||
}
|
||||
f.Ln(3)
|
||||
curY := f.GetY()
|
||||
f.SetXY(135, curY-3)
|
||||
f.CellFormat(60, 5, "Data: "+meta.IssuedAt, "", 1, "R", false, 0, "")
|
||||
f.CellFormat(60, 5, t("Data: ")+meta.IssuedAt, "", 1, "R", false, 0, "")
|
||||
f.SetY(curY + 3)
|
||||
f.Ln(3)
|
||||
|
||||
// Client block
|
||||
if meta.ClientName != "" {
|
||||
f.SetFont("Helvetica", "B", 9)
|
||||
f.CellFormat(180, 5, "Cliente", "", 1, "L", false, 0, "")
|
||||
f.CellFormat(180, 5, t("Cliente"), "", 1, "L", false, 0, "")
|
||||
f.SetFont("Helvetica", "", 9)
|
||||
f.CellFormat(180, 5, meta.ClientName, "", 1, "L", false, 0, "")
|
||||
f.CellFormat(180, 5, t(meta.ClientName), "", 1, "L", false, 0, "")
|
||||
if meta.ClientNIF != "" {
|
||||
f.CellFormat(180, 5, "NIF: "+meta.ClientNIF, "", 1, "L", false, 0, "")
|
||||
f.CellFormat(180, 5, t("NIF: ")+meta.ClientNIF, "", 1, "L", false, 0, "")
|
||||
}
|
||||
if meta.VehiclePlate != "" {
|
||||
f.CellFormat(180, 5, "Matrícula: "+meta.VehiclePlate, "", 1, "L", false, 0, "")
|
||||
f.CellFormat(180, 5, t("Matrícula: ")+meta.VehiclePlate, "", 1, "L", false, 0, "")
|
||||
}
|
||||
f.Ln(4)
|
||||
}
|
||||
@@ -85,11 +99,11 @@ func Generate(meta DocMeta, items []LineItem, staffTotal float64, outPath string
|
||||
f.SetFillColor(50, 50, 50)
|
||||
f.SetTextColor(255, 255, 255)
|
||||
f.SetFont("Helvetica", "B", 9)
|
||||
f.CellFormat(90, 7, "Descricao", "1", 0, "L", true, 0, "")
|
||||
f.CellFormat(20, 7, "Qtd.", "1", 0, "C", true, 0, "")
|
||||
f.CellFormat(25, 7, "Preco Unit.", "1", 0, "R", true, 0, "")
|
||||
f.CellFormat(20, 7, "Desc.%", "1", 0, "C", true, 0, "")
|
||||
f.CellFormat(25, 7, "Total", "1", 1, "R", true, 0, "")
|
||||
f.CellFormat(90, 7, t("Descrição"), "1", 0, "L", true, 0, "")
|
||||
f.CellFormat(20, 7, t("Qtd."), "1", 0, "C", true, 0, "")
|
||||
f.CellFormat(25, 7, t("Preço Unit."), "1", 0, "R", true, 0, "")
|
||||
f.CellFormat(20, 7, t("Desc.%"), "1", 0, "C", true, 0, "")
|
||||
f.CellFormat(25, 7, t("Total"), "1", 1, "R", true, 0, "")
|
||||
|
||||
// Table rows
|
||||
f.SetFillColor(245, 245, 245)
|
||||
@@ -97,7 +111,7 @@ func Generate(meta DocMeta, items []LineItem, staffTotal float64, outPath string
|
||||
f.SetFont("Helvetica", "", 9)
|
||||
fill := false
|
||||
for _, item := range items {
|
||||
f.CellFormat(90, 6, item.Description, "1", 0, "L", fill, 0, "")
|
||||
f.CellFormat(90, 6, t(item.Description), "1", 0, "L", fill, 0, "")
|
||||
f.CellFormat(20, 6, fmt.Sprintf("%.2f", item.Qty), "1", 0, "C", fill, 0, "")
|
||||
f.CellFormat(25, 6, fmt.Sprintf("%.2f EUR", item.UnitPrice), "1", 0, "R", fill, 0, "")
|
||||
f.CellFormat(20, 6, fmt.Sprintf("%.0f%%", item.DiscountPct), "1", 0, "C", fill, 0, "")
|
||||
@@ -115,21 +129,88 @@ func Generate(meta DocMeta, items []LineItem, staffTotal float64, outPath string
|
||||
f.Ln(3)
|
||||
f.SetFont("Helvetica", "", 9)
|
||||
if staffTotal > 0 {
|
||||
f.CellFormat(155, 6, "Subtotal pecas/servicos", "", 0, "R", false, 0, "")
|
||||
f.CellFormat(155, 6, t("Subtotal peças/serviços"), "", 0, "R", false, 0, "")
|
||||
f.CellFormat(25, 6, fmt.Sprintf("%.2f EUR", subtotal), "1", 1, "R", false, 0, "")
|
||||
f.CellFormat(155, 6, "Mao de obra", "", 0, "R", false, 0, "")
|
||||
f.CellFormat(155, 6, t("Mão de obra"), "", 0, "R", false, 0, "")
|
||||
f.CellFormat(25, 6, fmt.Sprintf("%.2f EUR", staffTotal), "1", 1, "R", false, 0, "")
|
||||
}
|
||||
f.SetFont("Helvetica", "B", 10)
|
||||
f.CellFormat(155, 7, "TOTAL", "", 0, "R", false, 0, "")
|
||||
f.CellFormat(155, 7, t("TOTAL"), "", 0, "R", false, 0, "")
|
||||
f.CellFormat(25, 7, fmt.Sprintf("%.2f EUR", grandTotal), "1", 1, "R", false, 0, "")
|
||||
|
||||
// IBAN footer
|
||||
if meta.CompanyIBAN != "" {
|
||||
f.Ln(8)
|
||||
f.SetFont("Helvetica", "", 8)
|
||||
f.CellFormat(180, 5, "IBAN: "+meta.CompanyIBAN, "", 1, "C", false, 0, "")
|
||||
f.CellFormat(180, 5, t("IBAN: ")+meta.CompanyIBAN, "", 1, "C", false, 0, "")
|
||||
}
|
||||
|
||||
return f.OutputFileAndClose(outPath)
|
||||
}
|
||||
|
||||
func addLogo(f *fpdf.Fpdf, logo string) bool {
|
||||
logo = strings.TrimSpace(logo)
|
||||
if logo == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(logo, "data:image/") {
|
||||
comma := strings.Index(logo, ",")
|
||||
if comma <= 0 {
|
||||
return false
|
||||
}
|
||||
meta := logo[:comma]
|
||||
payload := logo[comma+1:]
|
||||
if !strings.Contains(meta, ";base64") {
|
||||
return false
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(payload)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
imgType := "PNG"
|
||||
if strings.Contains(meta, "image/jpeg") || strings.Contains(meta, "image/jpg") {
|
||||
imgType = "JPG"
|
||||
}
|
||||
opts := fpdf.ImageOptions{ImageType: imgType, ReadDpi: true}
|
||||
f.RegisterImageOptionsReader("company_logo", opts, bytes.NewReader(raw))
|
||||
f.ImageOptions("company_logo", 15, 14, 26, 0, false, opts, 0, "")
|
||||
f.SetX(45)
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(logo, "http://") || strings.HasPrefix(logo, "https://") {
|
||||
client := http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Get(logo)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return false
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(resp.Body); err != nil {
|
||||
return false
|
||||
}
|
||||
imgType := "PNG"
|
||||
ct := strings.ToLower(resp.Header.Get("Content-Type"))
|
||||
if strings.Contains(ct, "jpeg") || strings.Contains(ct, "jpg") {
|
||||
imgType = "JPG"
|
||||
}
|
||||
opts := fpdf.ImageOptions{ImageType: imgType, ReadDpi: true}
|
||||
f.RegisterImageOptionsReader("company_logo", opts, bytes.NewReader(buf.Bytes()))
|
||||
f.ImageOptions("company_logo", 15, 14, 26, 0, false, opts, 0, "")
|
||||
f.SetX(45)
|
||||
return true
|
||||
}
|
||||
|
||||
if _, err := os.Stat(logo); err == nil {
|
||||
opts := fpdf.ImageOptions{ReadDpi: true}
|
||||
f.ImageOptions(logo, 15, 14, 26, 0, false, opts, 0, "")
|
||||
f.SetX(45)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user