feat: pdf
This commit is contained in:
@@ -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