diff --git a/6060900045.yml b/6060900045.yml new file mode 100644 index 0000000..75977e6 --- /dev/null +++ b/6060900045.yml @@ -0,0 +1,26 @@ +--- +metadata: + invoice_nr: 6060900045 + invoice_info: Rechnung Nummer 6060900045 + invoice_date: 2. April 2020 + vat: 7.7 + account: CH92 0023 5235 5662 3601 G + due_date: 2019-10-31 +sender_address: + name: nbit Informatik GmbH + street: Kirchweg 2 + zip: 3510 + city: Konolfingen + tel_no: +41 31 792 00 40 + email: joerg.lehmann@nbit.ch +billing_address: + name: Coopers Group GmbH + street: Seestrasse 72b + zip: 6052 + city: Hergiswil +invoice_items: + - text: Arbeitseinsatz von Jörg Lehmann als Linux Engineer bei Post_CH_AG gemäss IT Beratungsdienstleistungsvertrag vom 28.1.2020 + quantity: 142 + price_per_unit: 115 + - text: + - text: Monat Mai 2020, Stunden gemäss beigelegtem Zeitnachweis diff --git a/README.md b/README.md index 41e8eaf..54e5d5a 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,12 @@ purpose to generate PDF-Invoices for nbit Informatik GmbH Font File can be generated with makefont +Payment Slip is generated using https://github.com/claudep/swiss-qr-bill + +Installation / Upgrade +$ pip install --user qrbill -U + +Usage (Example): +$ ./mkinvoice yaml/123456.yml + Joerg Lehmann, April 2020 diff --git a/logos/nbit-logo-40x20mm-400dpi.png b/logos/nbit-logo-40x20mm-400dpi.png new file mode 100644 index 0000000..7e3dd6d Binary files /dev/null and b/logos/nbit-logo-40x20mm-400dpi.png differ diff --git a/mkinvoice.go b/mkinvoice.go index dca9102..641a7fb 100644 --- a/mkinvoice.go +++ b/mkinvoice.go @@ -2,49 +2,52 @@ 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" - - "github.com/jung-kurt/gofpdf" - "gopkg.in/yaml.v2" + "strings" ) // Metadata type type Metadata struct { - InvoiceNr string - InvoiceInfo string - InvoiceDdate string - Vat float64 - Account string - DueDate string + 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"` } // Address type type Address struct { - Name string - Street string - Zip string - City string - TelNo string - Email string + 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 - Quantity float64 - PicePerUnit float64 + Text string `yaml:"text"` + Quantity float64 `yaml:"quantity"` + PricePerUnit float64 `yaml:"price_per_unit"` } // InvoiceData type type InvoiceData struct { - Metadata Metadata - SenderAddress Address - BillingAddress Address - Item []InvoiceItem + Metadata Metadata `yaml:"metadata"` + SenderAddress Address `yaml:"sender_address"` + BillingAddress Address `yaml:"billing_address"` + InvoiceItems []InvoiceItem `yaml:"invoice_items"` } /* global variable declaration */ @@ -52,21 +55,46 @@ var pdf *gofpdf.Fpdf var yPos float64 var invoiceData InvoiceData var progDir string +var currentPage int +var totalPages int +var totalNetAmount float64 +var totalInvoiceAmount float64 +const defaultFontSize = 9 +const fontSizeSmall = 7 const marginTop = 7 const logoTop = 6 const logoHeight = 20 const lineSpacing = 5 +const lineSpacingSmall = 3.5 const addressTop = 50 -const line1Top = 100 +const metadataTop = 70 const tabstopLeft = 20 const tabstopLeftAlt = 32 +const tabstopMetadata = 60 const tabstopAddress = 120 -const tabstopCount = 10 -const tabstopPrice = 10 -const tabstopTotal = 170 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 itemsTop = 105 +const totalsTop = 150 + +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) @@ -85,14 +113,14 @@ func readInvoiceData(filename string) { fmt.Printf("%s\n", invoiceData.BillingAddress.Name) } -func writeText(x float64, y float64, text string, alignStr ...string) { +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(0, 11, tr(text), "", 0, align, false, 0, "") + pdf.CellFormat(w, 11, tr(text), "", 0, align, false, 0, "") } func setupInvoice() { @@ -101,46 +129,115 @@ func setupInvoice() { pdf.SetFontLocation("fonts") pdf.AddFont("Dejavusans", "", "DejaVuSans.json") pdf.AddFont("Dejavusans-Bold", "", "DejaVuSans-Bold.json") - pdf.SetFont("Dejavusans", "", 10) + 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", "", 10) - writeText(tabstopLeft, yPos, "nbit Informatik GmbH") - pdf.SetFont("Dejavusans", "", 10) - yPos = yPos + lineSpacing - writeText(tabstopLeft, yPos, "Kirchweg 2") - yPos = yPos + lineSpacing - writeText(tabstopLeft, yPos, "3510 Konolfingen") - yPos = yPos + lineSpacing - yPos = yPos + lineSpacing - writeText(tabstopLeft, yPos, "Tel.") - writeText(tabstopLeftAlt, yPos, "+41 31 792 00 40") - yPos = yPos + lineSpacing - writeText(tabstopLeft, yPos, "EMail") - writeText(tabstopLeftAlt, yPos, "joerg.lehmann@nbit.ch") - yPos = yPos + lineSpacing + pdf.SetFont("Dejavusans-Bold", "", fontSizeSmall) + writeText(tabstopLeft, yPos, 0, "nbit Informatik GmbH") + pdf.SetFont("Dejavusans", "", fontSizeSmall) + yPos = yPos + lineSpacingSmall + writeText(tabstopLeft, yPos, 0, "Kirchweg 2") + yPos = yPos + lineSpacingSmall + writeText(tabstopLeft, yPos, 0, "3510 Konolfingen") + yPos = yPos + lineSpacingSmall + yPos = yPos + lineSpacingSmall + writeText(tabstopLeft, yPos, 0, "Tel.") + writeText(tabstopLeftAlt, yPos, 0, "+41 31 792 00 40") + yPos = yPos + lineSpacingSmall + writeText(tabstopLeft, yPos, 0, "EMail") + writeText(tabstopLeftAlt, yPos, 0, "joerg.lehmann@nbit.ch") + yPos = yPos + lineSpacingSmall opt.ImageType = "png" opt.ReadDpi = true - pdf.ImageOptions("logos/nbit-logo.png", tabstopLogo, logoTop, 0, logoHeight, false, opt, 0, "") + pdf.ImageOptions("logos/nbit-logo-40x20mm-400dpi.png", tabstopLogo, logoTop, 0, logoHeight, false, opt, 0, "") + pdf.SetFont("Dejavusans", "", defaultFontSize) } func printAddress() { - fmt.Printf("Blabla: %s\n", invoiceData.BillingAddress.Name) yPos = addressTop - writeText(tabstopAddress, yPos, invoiceData.BillingAddress.Name) + writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Name) yPos = yPos + lineSpacing - writeText(tabstopAddress, yPos, invoiceData.BillingAddress.Street) - yPos = yPos + lineSpacing - writeText(tabstopAddress, yPos, invoiceData.BillingAddress.Zip+" "+invoiceData.BillingAddress.City) + writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Street) yPos = yPos + lineSpacing + writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Zip+" "+invoiceData.BillingAddress.City) +} - pdf.Line(tabstopLeft, line1Top, tabstopRight, line1Top) +func printMetadataFirstPage() { + yPos = metadataTop + 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 + pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) + writeText(tabstopLeft, yPos, 0, "Rechnungsdatum:") + pdf.SetFont("Dejavusans", "", defaultFontSize) + writeText(tabstopMetadata, yPos, 0, invoiceData.Metadata.InvoiceDate) +} + +func printItemsHeader() { + yPos = itemsTop + 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 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 { + writeText(tabstopLeft, yPos, 0, strings.ReplaceAll(il, "_", " ")) + yPos = yPos + lineSpacing + } + } else { + yPos = yPos + lineSpacing + } + } +} + +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() { @@ -148,7 +245,7 @@ func printQR() { cmd := exec.Command(filepath.Join(progDir, "qrbill.sh"), "--account", invoiceData.Metadata.Account, - "--amount", "123.00", + "--amount", floatToString(totalInvoiceAmount), "--creditor-name", invoiceData.SenderAddress.Name, "--creditor-street", invoiceData.SenderAddress.Street, "--creditor-postalcode", invoiceData.SenderAddress.Zip, @@ -170,9 +267,22 @@ func printQR() { } fmt.Printf("%s\n", stdoutStderr) - opt.ImageType = "jpeg" + opt.ImageType = "png" opt.ReadDpi = true - pdf.ImageOptions("qr-images/"+invoiceData.Metadata.InvoiceNr+".jpg", 0, 200, 0, 0, false, opt, 0, "") + pdf.ImageOptions("qr-images/"+invoiceData.Metadata.InvoiceNr+".png", 0, 200, 0, 0, false, opt, 0, "") +} + +func CreateInvoice() { + totalNetAmount = 0 + readInvoiceData(os.Args[1]) + setupInvoice() + printPageHeader(true) + printAddress() + printMetadataFirstPage() + printItemsHeader() + printItems() + printTotals() + printQR() } func main() { @@ -187,11 +297,13 @@ func main() { os.Exit(1) } - readInvoiceData(os.Args[1]) - setupInvoice() - printPageHeader(true) - printAddress() - printQR() + // First Run to get total number of pages + CreateInvoice() + totalPages = pdf.PageNo() + fmt.Printf("Total Pages is: %d\n", totalPages) + + // Second Run + CreateInvoice() err = pdf.OutputFileAndClose(filepath.Join(progDir, "output", invoiceData.Metadata.InvoiceNr+".pdf")) if err == nil { diff --git a/qrbill.sh b/qrbill.sh index de90492..3098de0 100755 --- a/qrbill.sh +++ b/qrbill.sh @@ -30,7 +30,10 @@ if [ $? -ne 0 ]; then exit 2 fi -convert ${mydir}/temp/${INVNO}.svg ${mydir}/qr-images/${INVNO}.jpg +#convert ${mydir}/temp/${INVNO}.svg ${mydir}/qr-images/${INVNO}.jpg +inkscape ${mydir}/temp/${INVNO}.svg --export-width=794 --export-height=397 --export-filename ${mydir}/qr-images/${INVNO}.png +#cairosvg ${mydir}/temp/${INVNO}.svg -o ${mydir}/qr-images/${INVNO}.png if [ $? -eq 0 ]; then - rm ${mydir}/temp/${INVNO}.svg + echo blabla + #rm ${mydir}/temp/${INVNO}.svg fi