diff --git a/README.md b/README.md index 54e5d5a..fdfb729 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,10 @@ 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 +To generate the QRCode, following library is used: +https://github.com/stapelberg/qrbill Usage (Example): $ ./mkinvoice yaml/123456.yml -Joerg Lehmann, April 2020 +Joerg Lehmann, December 2020 diff --git a/logos/Scissors_icon_black.svg b/logos/Scissors_icon_black.svg new file mode 100644 index 0000000..1be188c --- /dev/null +++ b/logos/Scissors_icon_black.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/logos/nbit-logo.png b/logos/nbit-logo.png deleted file mode 100644 index f171c3a..0000000 Binary files a/logos/nbit-logo.png and /dev/null differ diff --git a/logos/scissors-rotated.png b/logos/scissors-rotated.png new file mode 100644 index 0000000..bd14674 Binary files /dev/null and b/logos/scissors-rotated.png differ diff --git a/logos/scissors.png b/logos/scissors.png new file mode 100644 index 0000000..abc2ca9 Binary files /dev/null and b/logos/scissors.png differ diff --git a/mkinvoice.go b/mkinvoice.go index 30ee6ff..7564c3d 100644 --- a/mkinvoice.go +++ b/mkinvoice.go @@ -1,18 +1,21 @@ 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" - "os/exec" "path/filepath" "strings" + + "github.com/stapelberg/qrbill" ) // Metadata type @@ -26,12 +29,13 @@ type Metadata struct { // 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"` + 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 @@ -62,11 +66,17 @@ 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 lineSpacingSmall = 4.5 +const lineSpacingPaymentSlipBig = 6 +const lineSpacingPaymentSlipBelowHeading = 2.8 +const lineSpacingPaymentSlipSmall = 3.175 const addressTop = 50 const metadataTopFirstPage = 70 const metadataTopNotFirstPage = 30 @@ -87,14 +97,26 @@ const itemsTopFirstPage = 105 const itemsTopNotFirstPage = 50 const totalsTop = 158 const maxYPos = 265 +const paymentSlipTop = 190 +const paymentSlipWaehrung = 270 +const paymentSlipDashLeft = 63 +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) string { +func floatToString(f float64, sep string) string { p := message.NewPrinter(language.English) - s := strings.ReplaceAll(p.Sprintf("%.2f", f), ",", "'") + s := strings.ReplaceAll(p.Sprintf("%.2f", f), ",", sep) //fmt.Printf("--- s: @%s@\n", s) return s } @@ -128,6 +150,7 @@ func writeText(x float64, y float64, w float64, text string, alignStr ...string) 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") @@ -145,7 +168,7 @@ func printPageHeader(firstPage bool) { writeText(tabstopLeft, yPos, 0, invoiceData.SenderAddress.Name) pdf.SetFont("Dejavusans", "", defaultFontSize) yPos = yPos + lineSpacing - writeText(tabstopLeft, yPos, 0, invoiceData.SenderAddress.Street) + 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 { @@ -171,7 +194,7 @@ func printAddress() { yPos = addressTop writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Name) yPos = yPos + lineSpacing - writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Street) + writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Street+" "+invoiceData.BillingAddress.StreetNumber) yPos = yPos + lineSpacing writeText(tabstopAddress, yPos, 0, invoiceData.BillingAddress.Zip+" "+invoiceData.BillingAddress.City) } @@ -196,7 +219,7 @@ func printMetadata(firstPage bool) { 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) @@ -235,7 +258,7 @@ func printItems() { 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") + writeText(tabstopPrice, yPos, widthPrice, floatToString(itemNetAmount, "'"), "TR") } if i.Text != "" { lines := pdf.SplitText(i.Text, widthItemText) @@ -255,57 +278,186 @@ func printItems() { func printTotals() { yPos = totalsTop - pdf.Line(tabstopRight-widthPrice, yPos + (lineSpacing / 2), tabstopRight, yPos + (lineSpacing / 2)) + 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") + 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 + 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") + 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 - 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", "Rechnung Nummer " + invoiceData.Metadata.InvoiceNr, - "--debtor-name", invoiceData.BillingAddress.Name, - "--debtor-street", invoiceData.BillingAddress.Street, - "--debtor-postalcode", invoiceData.BillingAddress.Zip, - "--debtor-city", invoiceData.BillingAddress.City, - "--language", "de") - cmd.Env = append(os.Environ(), - "INVNO="+invoiceData.Metadata.InvoiceNr, - ) - stdoutStderr, err := cmd.CombinedOutput() - fmt.Printf("%s\n", stdoutStderr) + var b []byte + var err error + qrch := qrchFromInvoiceData(invoiceData) + bill, err := qrch.Encode() + + code, err := bill.EncodeToImage() if err != nil { - log.Fatal(err) + 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 = true - pdf.ImageOptions("qr-images/"+invoiceData.Metadata.InvoiceNr+".png", 0, 193, -1, -1, false, opt, 0, "") + opt.ReadDpi = false + + pdf.ImageOptions("qr-images/"+invoiceData.Metadata.InvoiceNr+".png", tabstopPaymentSlipMiddle-2, paymentSlipTop+13, 56, 56, false, opt, 0, "") } func CreateInvoice() { @@ -319,6 +471,7 @@ func CreateInvoice() { printItemsHeader() printItems() printTotals() + printPaymentSlip() printQR() } diff --git a/qrbill.sh b/qrbill.sh deleted file mode 100755 index 5ebea3f..0000000 --- a/qrbill.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -mydir="$(dirname $0)" -if [ -z "${INVNO}" ]; then - echo "ERROR: INVNO must be set as ENV variable" - exit 1 -fi - -echo "INVNO: ${INVNO}" - -# should be called with following arguments (example) -# INVNO must be set as ENV variable - -# --account "CH92 0023 5235 5662 3601 G" -# --amount 123.00 -# --creditor-name "nbit Informatik GmbH" -# --creditor-street "Kirchweg 2" -# --creditor-postalcode "3510" -# --creditor-city "Konolfingen" -# --extra-infos "Rechnung Nummer ${INVNO}" -# --debtor-name "Wilhelm Tell" -# --debtor-street "Marktgasse 28" -# --debtor-postalcode "9400" -# --debtor-city "Rorschach" -# --due-date "2019-10-31" -# --language "de" - -qrbill "$@" --output ${mydir}/temp/${INVNO}.svg -if [ $? -ne 0 ]; then - echo "ERROR: cannot create qrbill image" - exit 2 -fi - -#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 -inkscape ${mydir}/temp/${INVNO}.svg --export-dpi=300 --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 -fi diff --git a/testqrbill.sh b/testqrbill.sh deleted file mode 100755 index 326af6c..0000000 --- a/testqrbill.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -mydir="$(dirname $0)" -export INVNO=123456789 -${mydir}/qrbill.sh --account "CH92 0023 5235 5662 3601 G" \ - --amount 123.00 \ - --creditor-name "nbit Informatik GmbH" \ - --creditor-street "Kirchweg 2" \ - --creditor-postalcode "3510" \ - --creditor-city "Konolfingen" \ - --extra-infos "Rechnung Nummer 123456789" \ - --debtor-name "Wilhelm Tell" \ - --debtor-street "Marktgasse 28" \ - --debtor-postalcode "9400" \ - --debtor-city "Rorschach" \ - --due-date "2019-10-31" \ - --language "de"