go/svc/invoice: add shitty multilanguage support

This commit is contained in:
q3k 2019-06-07 10:37:22 +02:00
parent 77c0162a6f
commit a818ef2c16
7 changed files with 250 additions and 17 deletions

View file

@ -95,7 +95,7 @@ func newService(m *model) *service {
}
}
func (s *service) invoicePDF(ctx context.Context, uid string) ([]byte, error) {
func (s *service) invoicePDF(ctx context.Context, uid, language string) ([]byte, error) {
sealed, err := s.m.getSealedUid(ctx, uid)
if err != nil {
return nil, err
@ -115,7 +115,7 @@ func (s *service) invoicePDF(ctx context.Context, uid string) ([]byte, error) {
return nil, err
}
rendered, err = renderInvoicePDF(invoice)
rendered, err = renderInvoicePDF(invoice, language)
if err != nil {
return nil, err
}
@ -124,7 +124,7 @@ func (s *service) invoicePDF(ctx context.Context, uid string) ([]byte, error) {
}
func (s *service) RenderInvoice(req *pb.RenderInvoiceRequest, srv pb.Invoicer_RenderInvoiceServer) error {
rendered, err := s.invoicePDF(srv.Context(), req.Uid)
rendered, err := s.invoicePDF(srv.Context(), req.Uid, req.Language)
if err != nil {
if _, ok := status.FromError(err); ok {
return err
@ -150,7 +150,11 @@ func (s *service) RenderInvoice(req *pb.RenderInvoiceRequest, srv pb.Invoicer_Re
}
func (s *service) SealInvoice(ctx context.Context, req *pb.SealInvoiceRequest) (*pb.SealInvoiceResponse, error) {
if err := s.m.sealInvoice(ctx, req.Uid); err != nil {
useProformaTime := false
if req.DateSource == pb.SealInvoiceRequest_DATE_SOURCE_PROFORMA {
useProformaTime = true
}
if err := s.m.sealInvoice(ctx, req.Uid, req.Language, useProformaTime); err != nil {
if _, ok := status.FromError(err); ok {
return nil, err
}

View file

@ -54,7 +54,7 @@ func (m *model) init() error {
return err
}
func (m *model) sealInvoice(ctx context.Context, uid string) error {
func (m *model) sealInvoice(ctx context.Context, uid, language string, useProformaTime bool) error {
id, err := strconv.Atoi(uid)
if err != nil {
return status.Error(codes.InvalidArgument, "invalid uid")
@ -81,6 +81,9 @@ func (m *model) sealInvoice(ctx context.Context, uid string) error {
`
sealTime := time.Now()
if useProformaTime {
sealTime = time.Unix(0, invoice.Date)
}
res, err := tx.Exec(q, id, sealTime.UnixNano())
if err != nil {
return err
@ -101,11 +104,16 @@ func (m *model) sealInvoice(ctx context.Context, uid string) error {
}
invoice.State = pb.Invoice_STATE_SEALED
invoice.FinalUid = fmt.Sprintf("FV/%s", finalUid)
// TODO(q3k): this should be configurable.
if language == "pl" {
invoice.FinalUid = fmt.Sprintf("FV/%s", finalUid)
} else {
invoice.FinalUid = fmt.Sprintf("%s", finalUid)
}
invoice.Date = sealTime.UnixNano()
calculateInvoiceData(invoice)
pdfBlob, err := renderInvoicePDF(invoice)
pdfBlob, err := renderInvoicePDF(invoice, language)
if err != nil {
return err
}
@ -142,7 +150,13 @@ func (m *model) createInvoice(ctx context.Context, id *pb.InvoiceData) (string,
?, ?
)
`
res, err := m.db.Exec(sql, data, time.Now().UnixNano())
t := time.Now()
if id.Date != 0 {
t = time.Unix(0, id.Date)
}
res, err := m.db.Exec(sql, data, t.UnixNano())
if err != nil {
return "", err
}
@ -216,7 +230,7 @@ func (s *sqlInvoiceSealRow) Proto() (*pb.Invoice, error) {
}
if s.finalUid.Valid {
p.State = pb.Invoice_STATE_SEALED
p.FinalUid = fmt.Sprintf("FV/%s", s.finalUid.String)
p.FinalUid = fmt.Sprintf("%s", s.finalUid.String)
p.Date = s.sealedTime.Int64
} else {
p.State = pb.Invoice_STATE_PROFORMA

View file

@ -13,18 +13,25 @@ import (
)
var (
invTmpl *template.Template
invTmpl map[string]*template.Template
languages = []string{"en", "pl"}
defaultLanguage = "en"
)
func init() {
a, err := templates.Asset("invoice.html")
if err != nil {
panic(err)
invTmpl = make(map[string]*template.Template)
for _, language := range languages {
filename := fmt.Sprintf("invoice_%s.html", language)
a, err := templates.Asset(filename)
if err != nil {
panic(err)
}
invTmpl[language] = template.Must(template.New(filename).Parse(string(a)))
}
invTmpl = template.Must(template.New("invoice.html").Parse(string(a)))
}
func renderInvoicePDF(i *pb.Invoice) ([]byte, error) {
func renderInvoicePDF(i *pb.Invoice, language string) ([]byte, error) {
type item struct {
Title string
UnitPrice string
@ -95,8 +102,12 @@ func renderInvoicePDF(i *pb.Invoice) ([]byte, error) {
data.Total = fmt.Sprintf(unit+"%.2f", float64(i.Total)/100)
data.DeliveryCharge = fmt.Sprintf(unit+"%.2f", float64(0))
if _, ok := invTmpl[language]; !ok {
language = defaultLanguage
}
var b bytes.Buffer
err := invTmpl.Execute(&b, &data)
err := invTmpl[language].Execute(&b, &data)
if err != nil {
return []byte{}, err
}

View file

@ -50,7 +50,12 @@ const invoicesFragment = `
<td>{{ index .Data.CustomerBilling 0 }}</td>
<td>{{ .TotalNetPretty }}</td>
<td>
{{ if eq .State 2 }}
<a href="/debug/view?id={{ .Uid }}">View</a>
{{ else }}
<a href="/debug/view?id={{ .Uid }}&language=en">Preview (en)</a> |
<a href="/debug/view?id={{ .Uid }}&language=pl">Preview (pl)</a>
{{ end }}
</td>
</tr>
{{ end }}
@ -90,7 +95,7 @@ func (s *service) setupStatusz(m *mirko.Mirko) {
})
m.HTTPMux().HandleFunc("/debug/view", func(w http.ResponseWriter, r *http.Request) {
rendered, err := s.invoicePDF(r.Context(), r.URL.Query().Get("id"))
rendered, err := s.invoicePDF(r.Context(), r.URL.Query().Get("id"), r.URL.Query().Get("language"))
if err != nil {
fmt.Fprintf(w, "error: %v", err)
}

View file

@ -0,0 +1,189 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Invoice 0001</title>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,700" rel="stylesheet">
<style type="text/css">
body {
background-color: #fff;
font-family: 'Roboto', sans-serif;
font-size: 1em;
padding: 2em;
}
ul {
list-style: none;
padding: 0;
}
ul li {
margin-bottom: 0.2em;
}
@page {
size: A4;
margin: 0;
}
div.rhs {
float: right;
width: 50%;
text-align: right;
}
div.lhs {
float: left;
text-align: left;
width: 50%;
min-height: 35em;
}
div.metadata {
margin-top: 2em;
}
div.invoicee {
margin-top: 9em;
}
h1 {
font-size: 1.5em;
margin: 0;
text-transform: uppercase;
}
h2 {
font-size: 1.2em;
margin: 0;
}
table.items {
text-align: right;
border-spacing: 0px;
border-collapse: collapse;
border: 0;
width: 100%;
}
table.items td,th {
border: 1px solid black;
}
table.items tr:first-child {
background-color: #eee;
color: #111;
padding: 0.8em;
text-align: left;
}
table.items td {
background-color: #fff;
}
table.items td,th {
padding: 0.5em 1em 0.5em 1em;
}
td.lhead {
border: 0 !important;
text-align: right;
text-transform: uppercase;
background: rgba(0, 0, 0, 0) !important;
}
div.bgtext {
z-index: -10;
position: absolute;
top: 140mm;
left: 0;
width: 100%;
}
div.bgtext div {
text-align: center;
font-size: 10em;
color: #ddd;
-webkit-transform: rotate(-45deg);
text-transform: uppercase;
}
</style>
</head>
<body>
{{ if .Proforma }}
<div class="bgtext"><div>Proforma</div></div>
{{ end }}
<div class="rhs">
<div class="invoicer">
<ul>
{{ range $i, $e := .InvoicerBilling }}
{{ if eq $i 0 }}
<li><b>{{ $e }}</b></li>
{{ else }}
<li>{{ $e }}</li>
{{ end }}
{{ end }}
{{ if .InvoicerCompanyNumber }}
<li>{{ .InvoicerCompanyNumber }}</li>
{{ end }}
<li><b>Tax Number:</b> {{ .InvoicerVAT }}</li>
</ul>
</div>
<div class="metadata">
<ul>
<li><b>Invoice number:</b> {{ .InvoiceNumber }}</li>
<li><b>Date:</b> {{ .Date.Format "2006/01/02" }}</li>
<li><b>Due date:</b> {{ .DueDate.Format "2006/01/02" }}</li>
<li><b>IBAN:</b> {{ .IBAN }}</li>
<li><b>SWIFT/BIC:</b> {{ .SWIFT }}</li>
</ul>
</div>
</div>
<div class="lhs">
<div class="invoicee">
{{ if .Proforma }}
<h1>Proforma Invoice</h1>
{{ else }}
<h1>VAT Invoice</h1>
{{ end }}
<ul>
{{ range $i, $e := .InvoiceeBilling }}
{{ if eq $i 0 }}
<li><b>{{ $e }}</b></li>
{{ else }}
<li>{{ $e }}</li>
{{ end }}
{{ end }}
{{ if .USCustomer }}
<li>EIN: {{ .InvoiceeVAT }}</li>
<li><b>(VAT zero rate)</b></li>
{{ else }}
<li><b>NIP:</b> {{ .InvoiceeVAT }}</li>
{{ end }}
{{ if .ReverseVAT }}
<li><b>(reverse charge applies)</b></li>
{{ end }}
</ul>
</div>
</div>
<div style="clear: both; height: 1em;"></div>
<table class="items">
<tr>
<th style="width: 60%;">Description</th>
<th>Price<br />(ex. VAT)</th>
<th>Qty</th>
<th>VAT rate</th>
<th>Total<br />(net)</th>
<th>Total<br />(inc. VAT)</th>
</tr>
{{ range .Items }}
<tr>
<td style="text-align: left;">{{ .Title }}</td>
<td>{{ .UnitPrice }}</td>
<td>{{ .Qty }}</td>
<td>{{ .VATRate }}</td>
<td>{{ .TotalNet }}</td>
<td>{{ .Total }}</td>
</tr>
{{ end }}
<tr>
<td colspan="5" class="lhead">Subtotal without VAT</td>
<td>{{ .TotalNet }}</td>
</tr>
<tr>
<td colspan="5" class="lhead">VAT Total{{ if .ReverseVAT }} (reverse charge applies){{ end }} {{ if .USCustomer }}(VAT zero rate){{ end }}</td>
<td>{{ .VATTotal }}</td>
</tr>
<tr>
<td colspan="5" class="lhead"><b>Total</b></td>
<td><b>{{ .Total }}</b></td>
</tr>
</table>
</body>
</html>

View file

@ -29,10 +29,13 @@ message InvoiceData {
string customer_vat_id = 6;
bool reverse_vat = 7;
bool us_customer = 11;
// Optional, if not given the proforma will be created with the current time.
int64 date = 14;
int64 days_due = 8;
string iban = 9;
string swift = 10;
string unit = 13;
// Next tag: 15
}
message Invoice {
@ -74,6 +77,7 @@ message GetInvoiceResponse {
message RenderInvoiceRequest {
string uid = 1;
string language = 2;
}
message RenderInvoiceResponse {
@ -82,6 +86,12 @@ message RenderInvoiceResponse {
message SealInvoiceRequest {
string uid = 1;
enum DateSource {
DATE_SOURCE_NOW = 0;
DATE_SOURCE_PROFORMA = 1;
}
DateSource date_source = 2;
string language = 3;
}
message SealInvoiceResponse {