feat: pdf
This commit is contained in:
12
go.mod
12
go.mod
@@ -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
28
go.sum
@@ -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=
|
||||||
|
|||||||
BIN
index.html
BIN
index.html
Binary file not shown.
118
invoice/invoice.go
Normal file
118
invoice/invoice.go
Normal 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
46
main.go
@@ -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
44
pdf/resource.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
25
report/invoice.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Report struct {
|
func New(issues []issue.Issue, company, client Entity, rate float64) *Report {
|
||||||
Date time.Time
|
return &Report{
|
||||||
Issues []issue.Issue
|
Date: time.Now(),
|
||||||
Style template.CSS
|
Issues: issues,
|
||||||
Total string
|
Rate: rate,
|
||||||
|
Company: company,
|
||||||
|
Client: client,
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Report struct {
|
||||||
|
Date time.Time
|
||||||
|
Issues []issue.Issue
|
||||||
|
Style template.CSS
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,68 +12,105 @@
|
|||||||
<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">
|
||||||
<table>
|
<article>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>FID</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Zeitaufwand</th>
|
|
||||||
<th>Stundensatz</th>
|
|
||||||
<th>Preis CHF</th>
|
|
||||||
<th>Fertiggestellt</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{ range .Issues }}
|
|
||||||
<tr>
|
|
||||||
<td>{{ .Index }}</td>
|
|
||||||
<td>{{ .Title }}</td>
|
|
||||||
<td>{{ .Duration | duration }}</td>
|
|
||||||
<td>16 CHF/h</td>
|
|
||||||
<td>{{ .Duration | price }} CHF</td>
|
|
||||||
|
|
||||||
<td>{{ .Closed | time }}</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="totals">
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<thead>
|
||||||
<td>Gesamtbetrag:</td>
|
<tr>
|
||||||
<td>{{ .Total }} CHF</td>
|
<th>FID</th>
|
||||||
</tr>
|
<th>Name</th>
|
||||||
|
<th>Zeitaufwand</th>
|
||||||
|
<th>Stundensatz</th>
|
||||||
|
<th>Preis CHF</th>
|
||||||
|
<th>Fertiggestellt</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range .Issues }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ .Index }}</td>
|
||||||
|
<td>{{ .Title }}</td>
|
||||||
|
<td>{{ .Duration | duration }}</td>
|
||||||
|
<td>16 CHF/h</td>
|
||||||
|
<td>{{ .Duration | price }} CHF</td>
|
||||||
|
|
||||||
|
<td>{{ .Closed | time }}</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
|
<div class="totals">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Gesamtbetrag:</td>
|
||||||
|
<td>{{ .Total }} CHF</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user