feat: pdf
This commit is contained in:
12
go.mod
12
go.mod
@@ -5,18 +5,20 @@ go 1.24.5
|
||||
require (
|
||||
code.gitea.io/sdk/gitea v0.21.0
|
||||
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
|
||||
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 (
|
||||
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/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/sys v0.35.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/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
||||
github.com/42wim/httpsig v1.2.2 h1:ofAYoHUNs/MJOLqQ8hIxeyz2QxOz8qdSVvp3PX/oPgA=
|
||||
github.com/42wim/httpsig v1.2.2/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY=
|
||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/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/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/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
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/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-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.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
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-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-20190412213103-97732733099d/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.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"git.schreifuchs.ch/lou-taylor/accounting/issue"
|
||||
"git.schreifuchs.ch/lou-taylor/accounting/pdf"
|
||||
"git.schreifuchs.ch/lou-taylor/accounting/report"
|
||||
)
|
||||
|
||||
@@ -27,7 +29,7 @@ func main() {
|
||||
for _, repo := range []Repo{
|
||||
{"lou-taylor", "lou-taylor-web"},
|
||||
{"lou-taylor", "lou-taylor-api"},
|
||||
// {"lou-taylor", "accounting"},
|
||||
{"lou-taylor", "accounting"},
|
||||
} {
|
||||
iss, _, err := client.ListRepoIssues(
|
||||
repo.owner,
|
||||
@@ -53,8 +55,44 @@ func main() {
|
||||
},
|
||||
)
|
||||
issues := issue.FromGiteas(is, time.Minute*15)
|
||||
r := report.Report{Issues: issues}
|
||||
fmt.Print(r.ToHTML())
|
||||
r := report.New(
|
||||
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 {
|
||||
|
||||
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 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"
|
||||
|
||||
"git.schreifuchs.ch/lou-taylor/accounting/issue"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Report struct {
|
||||
Date time.Time
|
||||
Issues []issue.Issue
|
||||
Style template.CSS
|
||||
Total string
|
||||
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 {
|
||||
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;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 40px;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
@@ -70,3 +74,49 @@ footer {
|
||||
padding: 5px 10px;
|
||||
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"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -19,27 +21,46 @@ func (r Report) ToHTML() string {
|
||||
template.New("report").Funcs(template.FuncMap{
|
||||
"md": mdToHTML,
|
||||
"oh": offsetHTags,
|
||||
"price": applyRate,
|
||||
"time": fmtTime,
|
||||
"price": r.applyRate,
|
||||
"time": fmtDateTime,
|
||||
"date": fmtDate,
|
||||
"duration": fmtDuration,
|
||||
"brakes": func(text string) template.HTML {
|
||||
return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "<br>"))
|
||||
},
|
||||
}).Parse(htmlTemplate))
|
||||
|
||||
r.Style = style
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
tmpl.Execute(buf, r)
|
||||
_ = tmpl.Execute(buf, r)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func applyRate(dur time.Duration) string {
|
||||
cost := dur.Hours() * 16
|
||||
func (r Report) applyRate(dur time.Duration) string {
|
||||
cost := dur.Hours() * r.Rate
|
||||
cost = math.Round(cost/0.05) * 0.05
|
||||
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")
|
||||
}
|
||||
|
||||
func fmtDate(t time.Time) string {
|
||||
return t.Format("02.08.2006")
|
||||
}
|
||||
|
||||
func fmtDuration(d time.Duration) string {
|
||||
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>
|
||||
<meta charset="UTF-8" />
|
||||
<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" /> -->
|
||||
<style>
|
||||
{{ .Style }}
|
||||
@@ -12,68 +12,105 @@
|
||||
<body>
|
||||
<header>
|
||||
<div class="company">
|
||||
<h2>.CompanyName</h2>
|
||||
<h2>{{ .Company.Name }}</h2>
|
||||
<p>
|
||||
.CompanyAddress <br />
|
||||
.CompanyContact
|
||||
{{ .Company.Address.Street}} {{.Company.Address.Number}} <br />
|
||||
{{ .Company.Address.ZIPCode }} {{ .Company.Address.Place }} <br />
|
||||
{{ .Company.Contact }}
|
||||
</p>
|
||||
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="invoice-info">
|
||||
<p>
|
||||
<strong>Rechnung:</strong> .InvoiceNumber <br />
|
||||
<strong>Datum:</strong> .Date <br />
|
||||
<strong>Fällig am:</strong> .DueDate
|
||||
<strong>Rechnung:</strong> {{ .ID }} <br />
|
||||
<strong>Datum:</strong> {{ .Date | date }} <br />
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<section class="client">
|
||||
<h2>Rechnung an:</h2>
|
||||
<p>
|
||||
.ClientName <br />
|
||||
.ClientAddress <br />
|
||||
.ClientContact
|
||||
{{ .Client.Name }} <br />
|
||||
{{ .Client.Address.Street}} {{.Client.Address.Number}} <br />
|
||||
{{ .Client.Address.ZIPCode }} {{ .Client.Address.Place }} <br />
|
||||
{{ .Client.Contact }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<table>
|
||||
<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">
|
||||
<section class="page p1">
|
||||
<article>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Gesamtbetrag:</td>
|
||||
<td>{{ .Total }} CHF</td>
|
||||
</tr>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<hr />
|
||||
<section>
|
||||
<h2>Details zu den Features</h2>
|
||||
|
||||
@@ -84,13 +121,5 @@
|
||||
</article>
|
||||
{{ end }}
|
||||
</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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user