forked from hswaw/hscloud
go/svc/invoice: add shitty multilanguage support
This commit is contained in:
parent
77c0162a6f
commit
a818ef2c16
7 changed files with 250 additions and 17 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
189
go/svc/invoice/templates/invoice_en.html
Normal file
189
go/svc/invoice/templates/invoice_en.html
Normal 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>
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue