package main import ( "fmt" "github.com/jung-kurt/gofpdf" "golang.org/x/text/language" "golang.org/x/text/message" "gopkg.in/yaml.v2" "io/ioutil" "log" "math" "os" "os/exec" "path/filepath" "strings" ) // Metadata type type Metadata struct { InvoiceNr string `yaml:"invoice_nr"` InvoiceInfo string `yaml:"invoice_info"` InvoiceDate string `yaml:"invoice_date"` Vat float64 `yaml:"vat"` Account string `yaml:"account"` DueDate string `yaml:"due_date"` VatNumber string `yaml:"vat_number"` } // Address type type Address struct { Name string `yaml:"name"` Street string `yaml:"street"` Zip string `yaml:"zip"` City string `yaml:"city"` TelNo string `yaml:"tel_no"` Email string `yaml:"email"` } // InvoiceItem type type InvoiceItem struct { Text string `yaml:"text"` Quantity float64 `yaml:"quantity"` PricePerUnit float64 `yaml:"price_per_unit"` } // InvoiceData type type InvoiceData struct { Metadata Metadata `yaml:"metadata"` SenderAddress Address `yaml:"sender_address"` BillingAddress Address `yaml:"billing_address"` InvoiceItems []InvoiceItem `yaml:"invoice_items"` } /* global variable declaration */ var pdf *gofpdf.Fpdf var yPos float64 var invoiceData InvoiceData var progDir string var currentPage int var totalPages int var totalItemLines int var totalNetAmount float64 var totalInvoiceAmount float64 const defaultFontSize = 9 const marginTop = 7 const logoTop = 6 const logoHeight = 20 const lineSpacing = 5 const lineSpacingSmall = 3.5 const addressTop = 50 const metadataTopFirstPage = 70 const metadataTopNotFirstPage = 30 const tabstopLeft = 20 const tabstopLeftAlt = 40 const tabstopMetadata = 60 const tabstopAddress = 120 const tabstopLogo = 155 const tabstopRight = 200 const widthItemText = 96 const widthQuantity = 28 const widthPricePerUnit = 28 const widthPrice = 28 const tabstopQuantity = tabstopRight - widthPrice - widthPricePerUnit - widthQuantity const tabstopPricePerUnit = tabstopRight - widthPrice - widthPricePerUnit const tabstopPrice = tabstopRight - widthPrice const itemsTopFirstPage = 105 const itemsTopNotFirstPage = 50 const totalsTop = 165 const maxYPos = 265 func round5rappen(f float64) float64 { return (math.Round(f*20) / 20) } func floatToString(f float64) string { p := message.NewPrinter(language.English) s := strings.ReplaceAll(p.Sprintf("%.2f", f), ",", "'") fmt.Printf("--- s: @%s@\n", s) return s } func readInvoiceData(filename string) { data, err := ioutil.ReadFile(filename) if err != nil { log.Fatal(err) } //fmt.Printf("File contents: %s", data) err = yaml.Unmarshal([]byte(data), &invoiceData) if err != nil { log.Fatalf("error: %v", err) } fmt.Printf("--- t:\n%v\n\n", invoiceData) fmt.Printf("%s\n", invoiceData.BillingAddress.Name) } func writeText(x float64, y float64, w float64, text string, alignStr ...string) { tr := pdf.UnicodeTranslatorFromDescriptor("") align := "LT" if len(alignStr) > 0 { align = alignStr[0] } pdf.SetXY(x, y) pdf.CellFormat(w, 11, tr(text), "", 0, align, false, 0, "") } func setupInvoice() { pdf = gofpdf.New("P", "mm", "A4", "") pdf.SetMargins(0, 0, 0) pdf.SetFontLocation("fonts") pdf.AddFont("Dejavusans", "", "DejaVuSans.json") pdf.AddFont("Dejavusans-Bold", "", "DejaVuSans-Bold.json") pdf.SetFont("Dejavusans", "", defaultFontSize) currentPage = 0 } func printPageHeader(firstPage bool) { var opt gofpdf.ImageOptions currentPage = currentPage + 1 pdf.AddPage() yPos = marginTop pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) writeText(tabstopLeft, yPos, 0, invoiceData.SenderAddress.Name) pdf.SetFont("Dejavusans", "", defaultFontSize) yPos = yPos + lineSpacing writeText(tabstopLeft, yPos, 0, invoiceData.SenderAddress.Street) yPos = yPos + lineSpacing writeText(tabstopLeft, yPos, 0, invoiceData.SenderAddress.Zip+" "+invoiceData.SenderAddress.City) if firstPage { yPos = yPos + lineSpacing yPos = yPos + lineSpacing writeText(tabstopLeft, yPos, 0, "Telefon") writeText(tabstopLeftAlt, yPos, 0, invoiceData.SenderAddress.TelNo) yPos = yPos + lineSpacing writeText(tabstopLeft, yPos, 0, "E-Mail") writeText(tabstopLeftAlt, yPos, 0, invoiceData.SenderAddress.Email) yPos = yPos + lineSpacing writeText(tabstopLeft, yPos, 0, "MwSt. Nr.") writeText(tabstopLeftAlt, yPos, 0, invoiceData.Metadata.VatNumber) } opt.ImageType = "png" opt.ReadDpi = true pdf.ImageOptions("logos/nbit-logo-40x20mm-400dpi.png", tabstopLogo, logoTop, 0, logoHeight, false, opt, 0, "") pdf.SetFont("Dejavusans", "", defaultFontSize) } func printAddress() { yPos = addressTop writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Name) yPos = yPos + lineSpacing writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Street) yPos = yPos + lineSpacing writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Zip+" "+invoiceData.BillingAddress.City) } func printMetadata(firstPage bool) { yPos = metadataTopNotFirstPage if firstPage { yPos = metadataTopFirstPage pdf.SetFont("Dejavusans-Bold", "", 16) writeText(tabstopLeft, yPos, 0, "Rechnung") yPos = yPos + lineSpacing yPos = yPos + lineSpacing } pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) writeText(tabstopLeft, yPos, 0, "Rechnungsnummer:") pdf.SetFont("Dejavusans", "", defaultFontSize) writeText(tabstopMetadata, yPos, 0, invoiceData.Metadata.InvoiceNr) yPos = yPos + lineSpacing if firstPage { pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) writeText(tabstopLeft, yPos, 0, "Rechnungsdatum:") pdf.SetFont("Dejavusans", "", defaultFontSize) writeText(tabstopMetadata, yPos, 0, invoiceData.Metadata.InvoiceDate) yPos = yPos + lineSpacing } pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) writeText(tabstopLeft, yPos, 0, "Seite:") pdf.SetFont("Dejavusans", "", defaultFontSize) writeText(tabstopMetadata, yPos, 0, fmt.Sprintf("%d von %d", currentPage, totalPages)) } func printItemsHeader() { if currentPage == 1 { yPos = itemsTopFirstPage } else { yPos = itemsTopNotFirstPage } pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) writeText(tabstopLeft, yPos, 0, "Bezeichnung") writeText(tabstopQuantity, yPos, widthQuantity, "Menge", "TR") writeText(tabstopPricePerUnit, yPos, widthPricePerUnit, "Einheitspreis", "TR") writeText(tabstopPrice, yPos, widthPrice, "Preis", "TR") pdf.SetFont("Dejavusans", "", defaultFontSize) yPos = yPos + lineSpacing pdf.Line(tabstopLeft, yPos, tabstopRight, yPos) yPos = yPos + lineSpacing } func maybeNewPage() { if yPos > maxYPos { printPageHeader(false) printMetadata(false) printItemsHeader() } } func printItems() { for _, i := range invoiceData.InvoiceItems { if i.Quantity != 0 { writeText(tabstopQuantity, yPos, widthQuantity, fmt.Sprintf("%.1f", i.Quantity), "TR") writeText(tabstopPricePerUnit, yPos, widthPricePerUnit, fmt.Sprintf("%.2f", i.PricePerUnit), "TR") itemNetAmount := round5rappen(i.Quantity * i.PricePerUnit) totalNetAmount = totalNetAmount + itemNetAmount writeText(tabstopPrice, yPos, widthPrice, floatToString(itemNetAmount), "TR") } if i.Text != "" { lines := pdf.SplitText(i.Text, widthItemText) for _, il := range lines { totalItemLines = totalItemLines + 1 writeText(tabstopLeft, yPos, 0, strings.ReplaceAll(il, "_", " ")) yPos = yPos + lineSpacing maybeNewPage() } } else { totalItemLines = totalItemLines + 1 yPos = yPos + lineSpacing maybeNewPage() } } } func printTotals() { yPos = totalsTop pdf.Line(tabstopRight-widthPrice, yPos, tabstopRight, yPos) pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) yPos = yPos + lineSpacing writeText(tabstopLeft, yPos, 0, "Netto Betrag") writeText(tabstopPrice, yPos, widthPrice, floatToString(totalNetAmount), "TR") yPos = yPos + lineSpacing yPos = yPos + lineSpacing writeText(tabstopLeft, yPos, 0, fmt.Sprintf("MwSt. %.1f%%", invoiceData.Metadata.Vat)) mwstAmount := round5rappen(totalNetAmount * invoiceData.Metadata.Vat / 100) writeText(tabstopPrice, yPos, widthPrice, floatToString(mwstAmount), "TR") yPos = yPos + lineSpacing yPos = yPos + lineSpacing writeText(tabstopLeft, yPos, 0, "Total Betrag sFr.") totalInvoiceAmount = totalNetAmount + mwstAmount writeText(tabstopPrice, yPos, widthPrice, floatToString(totalNetAmount+mwstAmount), "TR") } func printQR() { var opt gofpdf.ImageOptions cmd := exec.Command(filepath.Join(progDir, "qrbill.sh"), "--account", invoiceData.Metadata.Account, "--amount", floatToString(totalInvoiceAmount), "--creditor-name", invoiceData.SenderAddress.Name, "--creditor-street", invoiceData.SenderAddress.Street, "--creditor-postalcode", invoiceData.SenderAddress.Zip, "--creditor-city", invoiceData.SenderAddress.City, "--extra-infos", invoiceData.Metadata.InvoiceInfo, "--debtor-name", invoiceData.BillingAddress.Name, "--debtor-street", invoiceData.BillingAddress.Street, "--debtor-postalcode", invoiceData.BillingAddress.Zip, "--debtor-city", invoiceData.BillingAddress.City, "--due-date", invoiceData.Metadata.DueDate, "--language", "de") cmd.Env = append(os.Environ(), "INVNO="+invoiceData.Metadata.InvoiceNr, ) stdoutStderr, err := cmd.CombinedOutput() fmt.Printf("%s\n", stdoutStderr) if err != nil { log.Fatal(err) } fmt.Printf("%s\n", stdoutStderr) opt.ImageType = "png" opt.ReadDpi = true pdf.ImageOptions("qr-images/"+invoiceData.Metadata.InvoiceNr+".png", 0, 200, -1, -1, false, opt, 0, "") } func CreateInvoice() { totalNetAmount = 0 totalItemLines = 0 readInvoiceData(os.Args[1]) setupInvoice() printPageHeader(true) printAddress() printMetadata(true) printItemsHeader() printItems() printTotals() printQR() } func main() { dir, err := filepath.Abs(filepath.Dir(os.Args[0])) if err != nil { log.Fatal(err) } progDir = dir if len(os.Args) != 2 { fmt.Printf("usage: mkinvoice \n") os.Exit(1) } // First Run to get total number of pages CreateInvoice() totalPages = pdf.PageNo() fmt.Printf("Total Pages is: %d\n", totalPages) fmt.Printf("Total Item Lines is: %d\n", totalItemLines) // Second Run CreateInvoice() err = pdf.OutputFileAndClose(filepath.Join(progDir, "output", invoiceData.Metadata.InvoiceNr+".pdf")) if err == nil { fmt.Printf("Successfully created invoice\n") } else { fmt.Printf("Error: %v\n", err) } }