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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

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