feat: pdf

This commit is contained in:
u80864958
2025-08-22 15:59:26 +02:00
parent 11e7b6445c
commit 7c6916f3a1
14 changed files with 445 additions and 84 deletions

1
README.md Normal file
View File

@@ -0,0 +1 @@
docker run --rm -p 3030:3000 gotenberg/gotenberg:8

12
go.mod
View File

@@ -5,18 +5,20 @@ go 1.24.5
require ( require (
code.gitea.io/sdk/gitea v0.21.0 code.gitea.io/sdk/gitea v0.21.0
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
github.com/google/uuid v1.6.0
github.com/jedib0t/go-pretty/v6 v6.6.8 github.com/jedib0t/go-pretty/v6 v6.6.8
golang.org/x/net v0.21.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/starwalkn/gotenberg-go-client/v8 v8.11.0
) )
require ( require (
github.com/42wim/httpsig v1.2.2 // indirect github.com/42wim/httpsig v1.2.3 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/crypto v0.33.0 // indirect golang.org/x/crypto v0.41.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.28.0 // indirect
) )

28
go.sum
View File

@@ -1,7 +1,7 @@
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
github.com/42wim/httpsig v1.2.2 h1:ofAYoHUNs/MJOLqQ8hIxeyz2QxOz8qdSVvp3PX/oPgA= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.2/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
@@ -10,6 +10,8 @@ github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
@@ -21,29 +23,31 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/starwalkn/gotenberg-go-client/v8 v8.11.0 h1:9hKCcoWegZ0yrZ0tEl5gKlMyH2YraYO0R7zDyL0ZL/U=
github.com/starwalkn/gotenberg-go-client/v8 v8.11.0/go.mod h1:5q9nAJ3/lub4hSCT6QYl8cOjnfQ/B+spEzC4TAPVXd8=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

Binary file not shown.

BIN
index.pdf Normal file

Binary file not shown.

118
invoice/invoice.go Normal file
View File

@@ -0,0 +1,118 @@
package invoice
import (
"bytes"
"encoding/base64"
"fmt"
"html/template"
"strings"
"github.com/skip2/go-qrcode"
)
// Invoice contains all necessary information for the generation of an invoice.
type Invoice struct {
ReceiverIBAN string `yaml:"receiver_iban" default:"CH44 3199 9123 0008 8901 2"`
IsQrIBAN bool `yaml:"is_qr_iban" default:"true"`
ReceiverName string `yaml:"receiver_name" default:"Robert Schneider AG"`
ReceiverStreet string `yaml:"receiver_street" default:"Rue du Lac"`
ReceiverNumber string `yaml:"receiver_number" default:"12"`
ReceiverZIPCode string `yaml:"receiver_zip_code" default:"2501"`
ReceiverPlace string `yaml:"receiver_place" default:"Biel"`
ReceiverCountry string `yaml:"receiver_country" default:"CH"`
PayeeName string `yaml:"payee_name" default:"Pia-Maria Rutschmann-Schnyder"`
PayeeStreet string `yaml:"payee_street" default:"Grosse Marktgasse"`
PayeeNumber string `yaml:"payee_number" default:"28"`
PayeeZIPCode string `yaml:"payee_zip_code" default:"9400"`
PayeePlace string `yaml:"payee_place" default:"Rorschach"`
PayeeCountry string `yaml:"payee_country" default:"CH"`
Reference string `yaml:"reference" default:"21 00000 00003 13947 14300 09017"`
AdditionalInfo string `yaml:"additional_info" default:"Rechnung Nr. 3139 für Gartenarbeiten"`
Amount string `yaml:"amount" default:"3 949.75"`
Currency string `yaml:"currency" default:"CHF"`
}
func (i Invoice) GetQR() (src string, err error) {
content, err := i.qrContent()
if err != nil {
return
}
qr, err := qrcode.New(content, qrcode.Medium)
if err != nil {
return
}
qr.DisableBorder = true
png, err := qr.PNG(512)
if err != nil {
return
}
b64 := base64.StdEncoding.EncodeToString(png)
src = fmt.Sprintf(`data:image/png;base64,%s`, b64)
return
}
// noPayee returns true if no fields of the payee are set
func (i Invoice) noPayee() bool {
return i.PayeeName == "" && i.PayeeStreet == "" && i.PayeeZIPCode == "" && i.PayeePlace == ""
}
func (i Invoice) qrContent() (string, error) {
qrTpl := `SPC\r
0200\r
1\r
{{ .iban }}\r
S\r
{{ .inv.ReceiverName }}\r
{{ .inv.ReceiverStreet }}\r
{{ .inv.ReceiverNumber }}\r
{{ .inv.ReceiverZIPCode }}\r
{{ .inv.ReceiverPlace }}\r
{{ .inv.ReceiverCountry }}\r
\r
\r
\r
\r
\r
\r
\r
{{ .amount }}\r
{{ .inv.Currency }}\r
{{ .payeeAdrType }}\r
{{ .inv.PayeeName }}\r
{{ .inv.PayeeStreet }}\r
{{ .inv.PayeeNumber }}\r
{{ .inv.PayeeZIPCode }}\r
{{ .inv.PayeePlace }}\r
{{ .inv.PayeeCountry }}\r
{{ .refType }}\r
{{ .reference }}\r
{{ .inv.AdditionalInfo }}\r
EPD
`
refType := "QRR"
if !i.IsQrIBAN {
refType = "SCOR"
}
payeeAdrType := "S"
if i.noPayee() {
payeeAdrType = ""
}
data := map[string]any{
"inv": i,
"iban": strings.ReplaceAll(i.ReceiverIBAN, " ", ""),
"amount": strings.ReplaceAll(i.Amount, " ", ""),
"payeeAdrType": payeeAdrType,
"reference": strings.ReplaceAll(i.Reference, " ", ""),
"refType": refType,
}
tpl, err := template.New("qr-content").Parse(qrTpl)
if err != nil {
return "", fmt.Errorf("error while creating qr template: %s", err)
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, data); err != nil {
return "", fmt.Errorf("error while applying data to qr template: %s", err)
}
rsl := strings.ReplaceAll(buf.String(), "\\r", "\r")
return rsl, nil
}

46
main.go
View File

@@ -1,11 +1,13 @@
package main package main
import ( import (
"fmt" "io"
"os"
"time" "time"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"git.schreifuchs.ch/lou-taylor/accounting/issue" "git.schreifuchs.ch/lou-taylor/accounting/issue"
"git.schreifuchs.ch/lou-taylor/accounting/pdf"
"git.schreifuchs.ch/lou-taylor/accounting/report" "git.schreifuchs.ch/lou-taylor/accounting/report"
) )
@@ -27,7 +29,7 @@ func main() {
for _, repo := range []Repo{ for _, repo := range []Repo{
{"lou-taylor", "lou-taylor-web"}, {"lou-taylor", "lou-taylor-web"},
{"lou-taylor", "lou-taylor-api"}, {"lou-taylor", "lou-taylor-api"},
// {"lou-taylor", "accounting"}, {"lou-taylor", "accounting"},
} { } {
iss, _, err := client.ListRepoIssues( iss, _, err := client.ListRepoIssues(
repo.owner, repo.owner,
@@ -53,8 +55,44 @@ func main() {
}, },
) )
issues := issue.FromGiteas(is, time.Minute*15) issues := issue.FromGiteas(is, time.Minute*15)
r := report.Report{Issues: issues} r := report.New(
fmt.Print(r.ToHTML()) issues,
report.Entity{
Name: "schreifuchs.ch",
IBAN: "CH06 0079 0042 5877 0443 7",
Address: report.Address{
Street: "Kilchbergerweg",
Number: "1",
ZIPCode: "3052",
Place: "Zollikofen",
Country: "Schweiz",
},
Contact: "Niklas Breitenstein",
},
report.Entity{
Name: "Lou Taylor",
Address: report.Address{
Street: "Alpenstrasse",
Number: "22",
ZIPCode: "4950",
Place: "Huttwil",
Country: "Schweiz",
},
Contact: "Loana Groux",
},
16,
)
html := r.ToHTML()
// fmt.Print(html)
pdfs, err := pdf.New("http://localhost:3030")
if err != nil {
panic(err)
}
ducument, err := pdfs.HtmlToPdf(html)
io.Copy(os.Stdout, ducument)
} }
func Filter[T any](slice []T, ok func(T) bool) []T { func Filter[T any](slice []T, ok func(T) bool) []T {

44
pdf/resource.go Normal file
View File

@@ -0,0 +1,44 @@
package pdf
import (
"context"
"io"
"net/http"
"github.com/starwalkn/gotenberg-go-client/v8"
"github.com/starwalkn/gotenberg-go-client/v8/document"
)
type Service struct {
gotenberg *gotenberg.Client
}
func New(hostname string) (service Service, err error) {
service.gotenberg, err = gotenberg.NewClient(hostname, http.DefaultClient)
return
}
func (s Service) HtmlToPdf(html string) (pdf io.ReadCloser, err error) {
index, err := document.FromString("index.html", html)
if err != nil {
return
}
req := gotenberg.NewHTMLRequest(index)
req.PaperSize(gotenberg.A4)
req.Margins(gotenberg.PageMargins{
Top: 0.5,
Bottom: 0.5,
Left: 0.5,
Right: 0.6,
Unit: gotenberg.IN,
})
// Skips the IDLE events for faster PDF conversion.
req.SkipNetworkIdleEvent(true)
req.EmulatePrintMediaType()
resp, err := s.gotenberg.Send(context.Background(), req)
pdf = resp.Body
return
}

View File

@@ -38,5 +38,6 @@ func offsetHTags(amount int, html template.HTML) template.HTML {
return fmt.Sprintf("<%sh%d%s>", closingSlash, newLevel, attrs) return fmt.Sprintf("<%sh%d%s>", closingSlash, newLevel, attrs)
}) })
return template.HTML(result) return template.HTML(result)
} }

25
report/invoice.go Normal file
View File

@@ -0,0 +1,25 @@
package report
import (
"html/template"
"git.schreifuchs.ch/lou-taylor/accounting/invoice"
)
func (r Report) QRInvoice() template.URL {
i := invoice.Invoice{
ReceiverIBAN: r.Company.IBAN,
IsQrIBAN: false,
ReceiverName: r.Company.Name,
ReceiverStreet: r.Company.Address.Street,
ReceiverNumber: r.Company.Address.Number,
ReceiverZIPCode: r.Company.Address.ZIPCode,
ReceiverPlace: r.Company.Address.Place,
ReceiverCountry: r.Company.Address.Country,
Reference: "21 00000 00003 13947 14300 09017",
Amount: "25",
Currency: "CHF",
}
src, _ := i.GetQR()
return template.URL(src)
}

View File

@@ -5,11 +5,39 @@ import (
"time" "time"
"git.schreifuchs.ch/lou-taylor/accounting/issue" "git.schreifuchs.ch/lou-taylor/accounting/issue"
"github.com/google/uuid"
) )
func New(issues []issue.Issue, company, client Entity, rate float64) *Report {
return &Report{
Date: time.Now(),
Issues: issues,
Rate: rate,
Company: company,
Client: client,
ID: uuid.NewString(),
}
}
type Report struct { type Report struct {
Date time.Time Date time.Time
Issues []issue.Issue Issues []issue.Issue
Style template.CSS Style template.CSS
Total string Rate float64
ID string
Company Entity
Client Entity
}
type Entity struct {
Name string
Address Address
Contact string
IBAN string
}
type Address struct {
Street string
Number string
ZIPCode string
Place string
Country string
} }

View File

@@ -14,6 +14,10 @@ header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 40px; margin-bottom: 40px;
h2 {
margin-top: 0;
}
} }
h1 { h1 {
margin: 0; margin: 0;
@@ -70,3 +74,49 @@ footer {
padding: 5px 10px; padding: 5px 10px;
text-align: right; text-align: right;
} }
@media print {
.page,
.page-break {
break-after: page;
}
}
.qr-section {
position: absolute;
bottom: 0;
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 1fr;
gap: 1em;
width: 100%;
/* border-top: 2px solid #000; */
/* padding-top: 1rem; */
h4 {
margin-bottom: 0.1em;
}
.qr-code {
display: flex;
flex-direction: column;
justify-content: space-between;
img {
width: 100%;
}
div {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
justify-items: center;
}
}
.payment-details {
font-size: 0.9rem;
line-height: 1.4;
}
}

View File

@@ -5,6 +5,8 @@ import (
_ "embed" _ "embed"
"fmt" "fmt"
"html/template" "html/template"
"math"
"strings"
"time" "time"
) )
@@ -19,27 +21,46 @@ func (r Report) ToHTML() string {
template.New("report").Funcs(template.FuncMap{ template.New("report").Funcs(template.FuncMap{
"md": mdToHTML, "md": mdToHTML,
"oh": offsetHTags, "oh": offsetHTags,
"price": applyRate, "price": r.applyRate,
"time": fmtTime, "time": fmtDateTime,
"date": fmtDate,
"duration": fmtDuration, "duration": fmtDuration,
"brakes": func(text string) template.HTML {
return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "<br>"))
},
}).Parse(htmlTemplate)) }).Parse(htmlTemplate))
r.Style = style r.Style = style
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
tmpl.Execute(buf, r) _ = tmpl.Execute(buf, r)
return buf.String() return buf.String()
} }
func applyRate(dur time.Duration) string { func (r Report) applyRate(dur time.Duration) string {
cost := dur.Hours() * 16 cost := dur.Hours() * r.Rate
cost = math.Round(cost/0.05) * 0.05
return fmt.Sprintf("%.2f", cost) return fmt.Sprintf("%.2f", cost)
} }
func fmtTime(t time.Time) string { func fmtDateTime(t time.Time) string {
return t.Format("02.08.2006 15:04") return t.Format("02.08.2006 15:04")
} }
func fmtDate(t time.Time) string {
return t.Format("02.08.2006")
}
func fmtDuration(d time.Duration) string { func fmtDuration(d time.Duration) string {
return fmt.Sprintf("%.2f h", d.Hours()) return fmt.Sprintf("%.2f h", d.Hours())
} }
func (r Report) Total() string {
dur := time.Duration(0)
for _, i := range r.Issues {
dur += i.Duration
}
return r.applyRate(dur)
}

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Rechnung vom {{ .Date }}</title> <title>Rechnung vom {{ .Date | date }}</title>
<!-- <link href="css/style.css" rel="stylesheet" /> --> <!-- <link href="css/style.css" rel="stylesheet" /> -->
<style> <style>
{{ .Style }} {{ .Style }}
@@ -12,32 +12,34 @@
<body> <body>
<header> <header>
<div class="company"> <div class="company">
<h2>.CompanyName</h2> <h2>{{ .Company.Name }}</h2>
<p> <p>
.CompanyAddress <br /> {{ .Company.Address.Street}} {{.Company.Address.Number}} <br />
.CompanyContact {{ .Company.Address.ZIPCode }} {{ .Company.Address.Place }} <br />
{{ .Company.Contact }}
</p> </p>
<p></p> <p></p>
</div> </div>
<div class="invoice-info"> <div class="invoice-info">
<p> <p>
<strong>Rechnung:</strong> .InvoiceNumber <br /> <strong>Rechnung:</strong> {{ .ID }} <br />
<strong>Datum:</strong> .Date <br /> <strong>Datum:</strong> {{ .Date | date }} <br />
<strong>Fällig am:</strong> .DueDate
</p> </p>
</div> </div>
</header> </header>
<section class="client"> <section class="client">
<h2>Rechnung an:</h2> <h2>Rechnung an:</h2>
<p> <p>
.ClientName <br /> {{ .Client.Name }} <br />
.ClientAddress <br /> {{ .Client.Address.Street}} {{.Client.Address.Number}} <br />
.ClientContact {{ .Client.Address.ZIPCode }} {{ .Client.Address.Place }} <br />
{{ .Client.Contact }}
</p> </p>
</section> </section>
<section> <section class="page p1">
<article>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -72,8 +74,43 @@
</tr> </tr>
</table> </table>
</div> </div>
</article>
<article class="qr-section">
<div class="qr-code">
<!-- Replace with your QR code image or generated SVG -->
<image src="{{ .QRInvoice }}"></image>
<div>
<h4>Währung</h4>
<h4>Betrag</h4>
<p>CHF</p>
<p>{{ .Total}}</p>
</div>
</div>
<div class="payment-details">
<div>
<h4 style="margin-top: 0">Konto / Zahlbar an</h4>
<p>
{{ .Company.IBAN }} <br />
{{ .Company.Contact }} <br />
{{ .Company.Address.Street}} {{.Company.Address.Number}} <br />
{{ .Company.Address.ZIPCode }} {{ .Company.Address.Place }}
</p>
</div>
<div>
<h4>Referenz</h4>
<p></p>
</div>
<div>
<h4>Zahlbar durch</h4>
<p>
{{ .Client.Contact }} <br />
{{ .Client.Address.Street}} {{.Client.Address.Number}} <br />
{{ .Client.Address.ZIPCode }} {{ .Client.Address.Place }}
</p>
</div>
</div>
</article>
</section> </section>
<hr />
<section> <section>
<h2>Details zu den Features</h2> <h2>Details zu den Features</h2>
@@ -84,13 +121,5 @@
</article> </article>
{{ end }} {{ end }}
</section> </section>
<footer>
<p>
Bitte überweisen Sie den Gesamtbetrag bis zum Fälligkeitsdatum auf
folgendes Konto:
</p>
<p>.BankDetails</p>
<p>Vielen Dank für Ihr Vertrauen!</p>
</footer>
</body> </body>
</html> </html>