first workable version, still to be tuned...

This commit is contained in:
Joerg Lehmann 2020-06-27 21:16:26 +02:00
parent 58b0759c78
commit 24e895102b
5 changed files with 210 additions and 61 deletions

26
6060900045.yml Normal file
View File

@ -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

View File

@ -5,4 +5,12 @@ purpose to generate PDF-Invoices for nbit Informatik GmbH
Font File can be generated with makefont 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 Joerg Lehmann, April 2020

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -2,49 +2,52 @@ package main
import ( import (
"fmt" "fmt"
"github.com/jung-kurt/gofpdf"
"golang.org/x/text/language"
"golang.org/x/text/message"
"gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"log" "log"
"math"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"github.com/jung-kurt/gofpdf"
"gopkg.in/yaml.v2"
) )
// Metadata type // Metadata type
type Metadata struct { type Metadata struct {
InvoiceNr string InvoiceNr string `yaml:"invoice_nr"`
InvoiceInfo string InvoiceInfo string `yaml:"invoice_info"`
InvoiceDdate string InvoiceDate string `yaml:"invoice_date"`
Vat float64 Vat float64 `yaml:"vat"`
Account string Account string `yaml:"account"`
DueDate string DueDate string `yaml:"due_date"`
} }
// Address type // Address type
type Address struct { type Address struct {
Name string Name string `yaml:"name"`
Street string Street string `yaml:"street"`
Zip string Zip string `yaml:"zip"`
City string City string `yaml:"city"`
TelNo string TelNo string `yaml:"tel_no"`
Email string Email string `yaml:"email"`
} }
// InvoiceItem type // InvoiceItem type
type InvoiceItem struct { type InvoiceItem struct {
Text string Text string `yaml:"text"`
Quantity float64 Quantity float64 `yaml:"quantity"`
PicePerUnit float64 PricePerUnit float64 `yaml:"price_per_unit"`
} }
// InvoiceData type // InvoiceData type
type InvoiceData struct { type InvoiceData struct {
Metadata Metadata Metadata Metadata `yaml:"metadata"`
SenderAddress Address SenderAddress Address `yaml:"sender_address"`
BillingAddress Address BillingAddress Address `yaml:"billing_address"`
Item []InvoiceItem InvoiceItems []InvoiceItem `yaml:"invoice_items"`
} }
/* global variable declaration */ /* global variable declaration */
@ -52,21 +55,46 @@ var pdf *gofpdf.Fpdf
var yPos float64 var yPos float64
var invoiceData InvoiceData var invoiceData InvoiceData
var progDir string 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 marginTop = 7
const logoTop = 6 const logoTop = 6
const logoHeight = 20 const logoHeight = 20
const lineSpacing = 5 const lineSpacing = 5
const lineSpacingSmall = 3.5
const addressTop = 50 const addressTop = 50
const line1Top = 100 const metadataTop = 70
const tabstopLeft = 20 const tabstopLeft = 20
const tabstopLeftAlt = 32 const tabstopLeftAlt = 32
const tabstopMetadata = 60
const tabstopAddress = 120 const tabstopAddress = 120
const tabstopCount = 10
const tabstopPrice = 10
const tabstopTotal = 170
const tabstopLogo = 155 const tabstopLogo = 155
const tabstopRight = 200 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) { func readInvoiceData(filename string) {
data, err := ioutil.ReadFile(filename) data, err := ioutil.ReadFile(filename)
@ -85,14 +113,14 @@ func readInvoiceData(filename string) {
fmt.Printf("%s\n", invoiceData.BillingAddress.Name) 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("") tr := pdf.UnicodeTranslatorFromDescriptor("")
align := "LT" align := "LT"
if len(alignStr) > 0 { if len(alignStr) > 0 {
align = alignStr[0] align = alignStr[0]
} }
pdf.SetXY(x, y) 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() { func setupInvoice() {
@ -101,46 +129,115 @@ func setupInvoice() {
pdf.SetFontLocation("fonts") pdf.SetFontLocation("fonts")
pdf.AddFont("Dejavusans", "", "DejaVuSans.json") pdf.AddFont("Dejavusans", "", "DejaVuSans.json")
pdf.AddFont("Dejavusans-Bold", "", "DejaVuSans-Bold.json") pdf.AddFont("Dejavusans-Bold", "", "DejaVuSans-Bold.json")
pdf.SetFont("Dejavusans", "", 10) pdf.SetFont("Dejavusans", "", defaultFontSize)
currentPage = 0
} }
func printPageHeader(firstPage bool) { func printPageHeader(firstPage bool) {
var opt gofpdf.ImageOptions var opt gofpdf.ImageOptions
currentPage = currentPage + 1
pdf.AddPage() pdf.AddPage()
yPos = marginTop yPos = marginTop
pdf.SetFont("Dejavusans-Bold", "", 10) pdf.SetFont("Dejavusans-Bold", "", fontSizeSmall)
writeText(tabstopLeft, yPos, "nbit Informatik GmbH") writeText(tabstopLeft, yPos, 0, "nbit Informatik GmbH")
pdf.SetFont("Dejavusans", "", 10) pdf.SetFont("Dejavusans", "", fontSizeSmall)
yPos = yPos + lineSpacing yPos = yPos + lineSpacingSmall
writeText(tabstopLeft, yPos, "Kirchweg 2") writeText(tabstopLeft, yPos, 0, "Kirchweg 2")
yPos = yPos + lineSpacing yPos = yPos + lineSpacingSmall
writeText(tabstopLeft, yPos, "3510 Konolfingen") writeText(tabstopLeft, yPos, 0, "3510 Konolfingen")
yPos = yPos + lineSpacing yPos = yPos + lineSpacingSmall
yPos = yPos + lineSpacing yPos = yPos + lineSpacingSmall
writeText(tabstopLeft, yPos, "Tel.") writeText(tabstopLeft, yPos, 0, "Tel.")
writeText(tabstopLeftAlt, yPos, "+41 31 792 00 40") writeText(tabstopLeftAlt, yPos, 0, "+41 31 792 00 40")
yPos = yPos + lineSpacing yPos = yPos + lineSpacingSmall
writeText(tabstopLeft, yPos, "EMail") writeText(tabstopLeft, yPos, 0, "EMail")
writeText(tabstopLeftAlt, yPos, "joerg.lehmann@nbit.ch") writeText(tabstopLeftAlt, yPos, 0, "joerg.lehmann@nbit.ch")
yPos = yPos + lineSpacing yPos = yPos + lineSpacingSmall
opt.ImageType = "png" opt.ImageType = "png"
opt.ReadDpi = true 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() { func printAddress() {
fmt.Printf("Blabla: %s\n", invoiceData.BillingAddress.Name)
yPos = addressTop yPos = addressTop
writeText(tabstopAddress, yPos, invoiceData.BillingAddress.Name) writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Name)
yPos = yPos + lineSpacing yPos = yPos + lineSpacing
writeText(tabstopAddress, yPos, invoiceData.BillingAddress.Street) writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Street)
yPos = yPos + lineSpacing
writeText(tabstopAddress, yPos, invoiceData.BillingAddress.Zip+" "+invoiceData.BillingAddress.City)
yPos = yPos + lineSpacing 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() { func printQR() {
@ -148,7 +245,7 @@ func printQR() {
cmd := exec.Command(filepath.Join(progDir, "qrbill.sh"), cmd := exec.Command(filepath.Join(progDir, "qrbill.sh"),
"--account", invoiceData.Metadata.Account, "--account", invoiceData.Metadata.Account,
"--amount", "123.00", "--amount", floatToString(totalInvoiceAmount),
"--creditor-name", invoiceData.SenderAddress.Name, "--creditor-name", invoiceData.SenderAddress.Name,
"--creditor-street", invoiceData.SenderAddress.Street, "--creditor-street", invoiceData.SenderAddress.Street,
"--creditor-postalcode", invoiceData.SenderAddress.Zip, "--creditor-postalcode", invoiceData.SenderAddress.Zip,
@ -170,9 +267,22 @@ func printQR() {
} }
fmt.Printf("%s\n", stdoutStderr) fmt.Printf("%s\n", stdoutStderr)
opt.ImageType = "jpeg" opt.ImageType = "png"
opt.ReadDpi = true 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() { func main() {
@ -187,11 +297,13 @@ func main() {
os.Exit(1) os.Exit(1)
} }
readInvoiceData(os.Args[1]) // First Run to get total number of pages
setupInvoice() CreateInvoice()
printPageHeader(true) totalPages = pdf.PageNo()
printAddress() fmt.Printf("Total Pages is: %d\n", totalPages)
printQR()
// Second Run
CreateInvoice()
err = pdf.OutputFileAndClose(filepath.Join(progDir, "output", invoiceData.Metadata.InvoiceNr+".pdf")) err = pdf.OutputFileAndClose(filepath.Join(progDir, "output", invoiceData.Metadata.InvoiceNr+".pdf"))
if err == nil { if err == nil {

View File

@ -30,7 +30,10 @@ if [ $? -ne 0 ]; then
exit 2 exit 2
fi 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 if [ $? -eq 0 ]; then
rm ${mydir}/temp/${INVNO}.svg echo blabla
#rm ${mydir}/temp/${INVNO}.svg
fi fi