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

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

View File

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

View File

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

View File

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