package main import ( "bytes" "fmt" "github.com/jung-kurt/gofpdf" "golang.org/x/text/language" "golang.org/x/text/message" "gopkg.in/yaml.v2" "image/png" "io/ioutil" "log" "math" "os" "path/filepath" "strings" "github.com/stapelberg/qrbill" ) // Metadata type type Metadata struct { InvoiceNr string `yaml:"invoice_nr"` InvoiceDate string `yaml:"invoice_date"` Vat float64 `yaml:"vat"` Account string `yaml:"account"` VatNumber string `yaml:"vat_number"` } // Address type type Address struct { Name string `yaml:"name"` Street string `yaml:"street"` StreetNumber string `yaml:"streetnumber"` 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 smallFontSize = 7 const paymentSlipTitleFontSize = 11 const paymentSlipHeadingFontSize = 6 const paymentSlipValuesFontSize = 8 const marginTop = 7 const logoTop = 6 const logoHeight = 20 const lineSpacing = 5 const lineSpacingTextsmall = 3 const lineSpacingSmall = 4.5 const lineSpacingPaymentSlipBig = 6 const lineSpacingPaymentSlipBelowHeading = 2.8 const lineSpacingPaymentSlipSmall = 3.175 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 = 158 const maxYPos = 265 const paymentSlipTop = 193.5 const paymentSlipWaehrung = 270 const paymentSlipDashLeft = 60.5 const tabstopPaymentSlipLeft = 5 const tabstopPaymentSlipLeft2 = 17 const tabstopPaymentSlipLeft3 = 40 const tabstopPaymentSlipMiddle = 68 const tabstopPaymentSlipMiddle2 = 80 const tabstopPaymentSlipRight = 125 const scissorsTop = 200 const widthA4 = 210 const lengthA4 = 297 func round5rappen(f float64) float64 { return (math.Round(f*20) / 20) } func floatToString(f float64, sep string) string { p := message.NewPrinter(language.English) s := strings.ReplaceAll(p.Sprintf("%.2f", f), ",", sep) //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) } 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 writeTextsmall(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, 6, tr(text), "", 0, align, false, 0, "") } func setupInvoice() { pdf = gofpdf.New("P", "mm", "A4", "") pdf.SetMargins(0, 0, 0) pdf.SetAutoPageBreak(false, 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+" "+invoiceData.SenderAddress.StreetNumber) 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+" "+invoiceData.BillingAddress.StreetNumber) 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("%g", 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 strings.HasPrefix(i.Text, "@") { pdf.SetFont("Dejavusans", "", smallFontSize) textwithoutprefix := i.Text[1:] lines := pdf.SplitText(textwithoutprefix, widthItemText) for _, il := range lines { totalItemLines = totalItemLines + 1 writeTextsmall(tabstopLeft, yPos, 0, strings.ReplaceAll(il, "_", " ")) yPos = yPos + lineSpacingTextsmall maybeNewPage() } pdf.SetFont("Dejavusans", "", defaultFontSize) } else 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+(lineSpacing/2), tabstopRight, yPos+(lineSpacing/2)) pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) yPos = yPos + lineSpacing writeText(tabstopLeft, yPos, 0, "Summe") pdf.SetFont("Dejavusans", "", defaultFontSize) writeText(tabstopPrice, yPos, widthPrice, floatToString(totalNetAmount, "'"), "TR") yPos = yPos + lineSpacing pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) writeText(tabstopLeft, yPos, 0, fmt.Sprintf("MwSt. %.1f%%", invoiceData.Metadata.Vat)) mwstAmount := round5rappen(totalNetAmount * invoiceData.Metadata.Vat / 100) pdf.SetFont("Dejavusans", "", defaultFontSize) writeText(tabstopPrice, yPos, widthPrice, floatToString(mwstAmount, "'"), "TR") yPos = yPos + lineSpacing yPos = yPos + lineSpacing pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) writeText(tabstopLeft, yPos, 0, "Rechnungbetrag in CHF") totalInvoiceAmount = totalNetAmount + mwstAmount writeText(tabstopPrice, yPos, widthPrice, floatToString(totalNetAmount+mwstAmount, "'"), "TR") yPos = yPos + lineSpacingSmall pdf.SetFont("Dejavusans", "", smallFontSize) writeText(tabstopLeft, yPos, 0, "30 Tage netto") } func qrchFromInvoiceData(id InvoiceData) *qrbill.QRCH { return &qrbill.QRCH{ CdtrInf: qrbill.QRCHCdtrInf{ IBAN: id.Metadata.Account, Cdtr: qrbill.Address{ AdrTp: qrbill.AddressType(string(qrbill.AddressTypeStructured)), Name: id.SenderAddress.Name, StrtNmOrAdrLine1: id.SenderAddress.Street, BldgNbOrAdrLine2: id.SenderAddress.StreetNumber, PstCd: id.SenderAddress.Zip, TwnNm: id.SenderAddress.City, Ctry: "CH", }, }, CcyAmt: qrbill.QRCHCcyAmt{ Amt: fmt.Sprintf("%.2f", totalInvoiceAmount), Ccy: "CHF", }, UltmtDbtr: qrbill.Address{ AdrTp: qrbill.AddressType(string(qrbill.AddressTypeStructured)), Name: id.BillingAddress.Name, StrtNmOrAdrLine1: id.BillingAddress.Street, BldgNbOrAdrLine2: id.BillingAddress.StreetNumber, PstCd: id.BillingAddress.Zip, TwnNm: id.BillingAddress.City, Ctry: "CH", }, RmtInf: qrbill.QRCHRmtInf{ Tp: "NON", // Reference type Ref: "", // Reference AddInf: qrbill.QRCHRmtInfAddInf{ Ustrd: "Rechnung Nummer " + id.Metadata.InvoiceNr, }, }, } } func printPaymentSlip() { var opt gofpdf.ImageOptions pdf.SetDashPattern([]float64{0.8, 0.8}, 0) pdf.Line(0, paymentSlipTop, widthA4, paymentSlipTop) pdf.Line(paymentSlipDashLeft, paymentSlipTop, paymentSlipDashLeft, lengthA4) opt.ImageType = "png" pdf.ImageOptions("logos/scissors-rotated.png", paymentSlipDashLeft-1.5, paymentSlipTop+10, 3, 3, false, opt, 0, "") pdf.SetFont("Helvetica", "B", paymentSlipTitleFontSize) yPos = paymentSlipTop + 7 writeText(tabstopPaymentSlipLeft, yPos, 0, "Empfangsschein") yPos = yPos + lineSpacingPaymentSlipBig pdf.SetFont("Helvetica", "B", paymentSlipHeadingFontSize) writeText(tabstopPaymentSlipLeft, yPos, 0, "Konto / Zahlbar an") yPos = yPos + lineSpacingPaymentSlipBelowHeading pdf.SetFont("Helvetica", "", paymentSlipValuesFontSize) writeText(tabstopPaymentSlipLeft, yPos, 0, invoiceData.Metadata.Account) yPos = yPos + lineSpacingPaymentSlipSmall writeText(tabstopPaymentSlipLeft, yPos, 0, invoiceData.SenderAddress.Name) yPos = yPos + lineSpacingPaymentSlipSmall writeText(tabstopPaymentSlipLeft, yPos, 0, invoiceData.SenderAddress.Street+" "+invoiceData.SenderAddress.StreetNumber) yPos = yPos + lineSpacingPaymentSlipSmall writeText(tabstopPaymentSlipLeft, yPos, 0, invoiceData.SenderAddress.Zip+" "+invoiceData.SenderAddress.City) yPos = yPos + lineSpacingPaymentSlipSmall yPos = yPos + lineSpacingPaymentSlipSmall pdf.SetFont("Helvetica", "B", paymentSlipHeadingFontSize) writeText(tabstopPaymentSlipLeft, yPos, 0, "Zahlbar durch") yPos = yPos + lineSpacingPaymentSlipBelowHeading pdf.SetFont("Helvetica", "", paymentSlipValuesFontSize) writeText(tabstopPaymentSlipLeft, yPos, 0, invoiceData.BillingAddress.Name) yPos = yPos + lineSpacingPaymentSlipSmall writeText(tabstopPaymentSlipLeft, yPos, 0, invoiceData.BillingAddress.Street+" "+invoiceData.BillingAddress.StreetNumber) yPos = yPos + lineSpacingPaymentSlipSmall writeText(tabstopPaymentSlipLeft, yPos, 0, invoiceData.BillingAddress.Zip+" "+invoiceData.BillingAddress.City) yPos = paymentSlipWaehrung pdf.SetFont("Helvetica", "B", paymentSlipHeadingFontSize) writeText(tabstopPaymentSlipLeft, yPos, 0, "Währung") writeText(tabstopPaymentSlipLeft2, yPos, 0, "Betrag") yPos = yPos + lineSpacingPaymentSlipBelowHeading pdf.SetFont("Helvetica", "", paymentSlipValuesFontSize) writeText(tabstopPaymentSlipLeft, yPos, 0, "CHF") writeText(tabstopPaymentSlipLeft2, yPos, 0, floatToString(totalInvoiceAmount, " ")) yPos = yPos + lineSpacingPaymentSlipSmall yPos = yPos + lineSpacingPaymentSlipSmall pdf.SetFont("Helvetica", "B", paymentSlipHeadingFontSize) writeText(tabstopPaymentSlipLeft3, yPos, 0, "Annahmestelle") pdf.SetFont("Helvetica", "B", paymentSlipTitleFontSize) yPos = paymentSlipTop + 7 writeText(tabstopPaymentSlipMiddle, yPos, 0, "Zahlteil") yPos = paymentSlipWaehrung pdf.SetFont("Helvetica", "B", paymentSlipHeadingFontSize) writeText(tabstopPaymentSlipMiddle, yPos, 0, "Währung") writeText(tabstopPaymentSlipMiddle2, yPos, 0, "Betrag") yPos = yPos + lineSpacingPaymentSlipBelowHeading pdf.SetFont("Helvetica", "", paymentSlipValuesFontSize) writeText(tabstopPaymentSlipMiddle, yPos, 0, "CHF") writeText(tabstopPaymentSlipMiddle2, yPos, 0, floatToString(totalInvoiceAmount, " ")) yPos = paymentSlipTop + 7 pdf.SetFont("Helvetica", "B", paymentSlipHeadingFontSize) writeText(tabstopPaymentSlipRight, yPos, 0, "Konto / Zahlbar an") yPos = yPos + lineSpacingPaymentSlipBelowHeading pdf.SetFont("Helvetica", "", paymentSlipValuesFontSize) writeText(tabstopPaymentSlipRight, yPos, 0, invoiceData.Metadata.Account) yPos = yPos + lineSpacingPaymentSlipSmall writeText(tabstopPaymentSlipRight, yPos, 0, invoiceData.SenderAddress.Name) yPos = yPos + lineSpacingPaymentSlipSmall writeText(tabstopPaymentSlipRight, yPos, 0, invoiceData.SenderAddress.Street+" "+invoiceData.SenderAddress.StreetNumber) yPos = yPos + lineSpacingPaymentSlipSmall writeText(tabstopPaymentSlipRight, yPos, 0, invoiceData.SenderAddress.Zip+" "+invoiceData.SenderAddress.City) yPos = yPos + (lineSpacingPaymentSlipSmall * 1.5) pdf.SetFont("Helvetica", "B", paymentSlipHeadingFontSize) writeText(tabstopPaymentSlipRight, yPos, 0, "Zusätzliche Informationen") yPos = yPos + lineSpacingPaymentSlipBelowHeading pdf.SetFont("Helvetica", "", paymentSlipValuesFontSize) writeText(tabstopPaymentSlipRight, yPos, 0, "Rechnung Nummer "+invoiceData.Metadata.InvoiceNr) yPos = yPos + (lineSpacingPaymentSlipSmall * 1.5) pdf.SetFont("Helvetica", "B", paymentSlipHeadingFontSize) writeText(tabstopPaymentSlipRight, yPos, 0, "Zahlbar durch") yPos = yPos + lineSpacingPaymentSlipBelowHeading pdf.SetFont("Helvetica", "", paymentSlipValuesFontSize) writeText(tabstopPaymentSlipRight, yPos, 0, invoiceData.BillingAddress.Name) yPos = yPos + lineSpacingPaymentSlipSmall writeText(tabstopPaymentSlipRight, yPos, 0, invoiceData.BillingAddress.Street+" "+invoiceData.BillingAddress.StreetNumber) yPos = yPos + lineSpacingPaymentSlipSmall writeText(tabstopPaymentSlipRight, yPos, 0, invoiceData.BillingAddress.Zip+" "+invoiceData.BillingAddress.City) } func printQR() { var opt gofpdf.ImageOptions var b []byte var err error qrch := qrchFromInvoiceData(invoiceData) bill, err := qrch.Encode() code, err := bill.EncodeToImage() if err != nil { log.Printf("%s", err) return } var buf bytes.Buffer if err := png.Encode(&buf, code); err != nil { log.Printf("%s", err) return } b = buf.Bytes() err = ioutil.WriteFile("qr-images/"+invoiceData.Metadata.InvoiceNr+".png", b, 0644) if err != nil { // handle error } opt.ImageType = "png" opt.ReadDpi = false pdf.ImageOptions("qr-images/"+invoiceData.Metadata.InvoiceNr+".png", tabstopPaymentSlipMiddle-2, paymentSlipTop+13, 56, 56, false, opt, 0, "") } func CreateInvoice() { totalNetAmount = 0 totalItemLines = 0 readInvoiceData(os.Args[1]) setupInvoice() printPageHeader(true) printAddress() printMetadata(true) printItemsHeader() printItems() printTotals() printPaymentSlip() 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) } }