Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many people will not even try an assume that they can not create a PDF for the invoice. I thought of using Groff or TeX to create the PDF, but it would mean maintaining two templates in two different systems (HTML and whatever i would use), and would probably look very different, because i do not know Groff or TeX that well. I wish there was a way to tell the browser to print to PDF, and it can be done, but only with the Chrome Protocol to a server-side running Chrome instance. This works, but i would need a Chrome running as a daemon. I also wrote a Qt application that uses QWebEngine to print the PDF, much like wkhtmltopdf, but with support for more recent HTML and CSS standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow break-page-inside as well as WeasyPrint does. To use WeasyPrint, at first i wanted to reach the same URL as the user, passing the cookie to WeasyPrint so that i can access the same invoice as the user, something that can be done with wkhtmltopdf, but WeasyPrint does not have such option. I did it with a custom Python script, but then i need to package and install that script, that is not that much work, but using the Debian-provided script is even less work, and less likely to drift when WeasyPrint changes API. Also, it is unnecessary to do a network round-trip from Go to Python back to Go, because i can already write the invoice HTML as is to WeasyPrint’s stdin.
This commit is contained in:
parent
47d3e1940c
commit
4d2379555e
|
@ -41,7 +41,8 @@ Package: numerus
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Depends:
|
Depends:
|
||||||
${shlibs:Depends},
|
${shlibs:Depends},
|
||||||
${misc:Depends}
|
${misc:Depends},
|
||||||
|
weasyprint
|
||||||
Built-Using: ${misc:Built-Using}
|
Built-Using: ${misc:Built-Using}
|
||||||
Description: Simple invoicing and accounting web application
|
Description: Simple invoicing and accounting web application
|
||||||
A simple web application to keep invoice and accouting records, intended for
|
A simple web application to keep invoice and accouting records, intended for
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,7 +57,7 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInvoiceForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
locale := getLocale(r)
|
locale := getLocale(r)
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
company := mustGetCompany(r)
|
company := mustGetCompany(r)
|
||||||
|
@ -65,16 +70,52 @@ func GetInvoiceForm(w http.ResponseWriter, r *http.Request, params httprouter.Pa
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pdf := false
|
||||||
|
if strings.HasSuffix(slug, ".pdf") {
|
||||||
|
pdf = true
|
||||||
|
slug = slug[:len(slug)-len(".pdf")]
|
||||||
|
}
|
||||||
invoice := mustGetInvoice(r.Context(), conn, company, slug)
|
invoice := mustGetInvoice(r.Context(), conn, company, slug)
|
||||||
if invoice == nil {
|
if invoice == nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if pdf {
|
||||||
|
cmd := exec.Command("weasyprint", "--stylesheet", "web/static/invoice.css", "-", "-")
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer stdout.Close()
|
||||||
|
if err = cmd.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer stdin.Close()
|
||||||
|
mustRenderAppTemplate(stdin, r, "invoices/view.gohtml", invoice)
|
||||||
|
}()
|
||||||
|
w.Header().Set("Content-Type", "application/pdf")
|
||||||
|
if _, err = io.Copy(w, stdout); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
log.Printf("ERR - %v\n", stderr.String())
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
mustRenderAppTemplate(w, r, "invoices/view.gohtml", invoice)
|
mustRenderAppTemplate(w, r, "invoices/view.gohtml", invoice)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type invoice struct {
|
type invoice struct {
|
||||||
Number string
|
Number string
|
||||||
|
Slug string
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Invoicer taxDetails
|
Invoicer taxDetails
|
||||||
Invoicee taxDetails
|
Invoicee taxDetails
|
||||||
|
@ -105,7 +146,9 @@ type invoiceProduct struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug string) *invoice {
|
func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug string) *invoice {
|
||||||
inv := &invoice{}
|
inv := &invoice{
|
||||||
|
Slug: slug,
|
||||||
|
}
|
||||||
var invoiceId int
|
var invoiceId int
|
||||||
var decimalDigits int
|
var decimalDigits int
|
||||||
if notFoundErrorOrPanic(conn.QueryRow(ctx, "select invoice_id, decimal_digits, invoice_number, invoice_date, notes, business_name, vatin, phone, email, address, city, province, postal_code, to_price(subtotal, decimal_digits), to_price(total, decimal_digits) from invoice join contact using (contact_id) join invoice_amount using (invoice_id) join currency using (currency_code) where invoice.slug = $1", slug).Scan(&invoiceId, &decimalDigits, &inv.Number, &inv.Date, &inv.Notes, &inv.Invoicee.Name, &inv.Invoicee.VATIN, &inv.Invoicee.Phone, &inv.Invoicee.Email, &inv.Invoicee.Address, &inv.Invoicee.City, &inv.Invoicee.Province, &inv.Invoicee.PostalCode, &inv.Subtotal, &inv.Total)) {
|
if notFoundErrorOrPanic(conn.QueryRow(ctx, "select invoice_id, decimal_digits, invoice_number, invoice_date, notes, business_name, vatin, phone, email, address, city, province, postal_code, to_price(subtotal, decimal_digits), to_price(total, decimal_digits) from invoice join contact using (contact_id) join invoice_amount using (invoice_id) join currency using (currency_code) where invoice.slug = $1", slug).Scan(&invoiceId, &decimalDigits, &inv.Number, &inv.Date, &inv.Notes, &inv.Invoicee.Name, &inv.Invoicee.VATIN, &inv.Invoicee.Phone, &inv.Invoicee.Email, &inv.Invoicee.Address, &inv.Invoicee.City, &inv.Invoicee.Province, &inv.Invoicee.PostalCode, &inv.Subtotal, &inv.Total)) {
|
||||||
|
|
|
@ -24,7 +24,7 @@ func NewRouter(db *Db) http.Handler {
|
||||||
companyRouter.PUT("/products/:slug", HandleUpdateProduct)
|
companyRouter.PUT("/products/:slug", HandleUpdateProduct)
|
||||||
companyRouter.GET("/invoices", IndexInvoices)
|
companyRouter.GET("/invoices", IndexInvoices)
|
||||||
companyRouter.POST("/invoices", HandleAddInvoice)
|
companyRouter.POST("/invoices", HandleAddInvoice)
|
||||||
companyRouter.GET("/invoices/:slug", GetInvoiceForm)
|
companyRouter.GET("/invoices/:slug", ServeInvoice)
|
||||||
companyRouter.POST("/invoices/new/products", HandleAddProductsToInvoice)
|
companyRouter.POST("/invoices/new/products", HandleAddProductsToInvoice)
|
||||||
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
|
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
|
||||||
|
|
73
po/ca.po
73
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: numerus\n"
|
"Project-Id-Version: numerus\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2023-02-24 11:52+0100\n"
|
"POT-Creation-Date: 2023-02-26 17:11+0100\n"
|
||||||
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
|
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
|
||||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||||
|
@ -59,7 +59,7 @@ msgid "Name"
|
||||||
msgstr "Nom"
|
msgstr "Nom"
|
||||||
|
|
||||||
#: web/template/invoices/products.gohtml:43
|
#: web/template/invoices/products.gohtml:43
|
||||||
#: web/template/invoices/view.gohtml:42 web/template/products/index.gohtml:23
|
#: web/template/invoices/view.gohtml:59 web/template/products/index.gohtml:23
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Price"
|
msgid "Price"
|
||||||
msgstr "Preu"
|
msgstr "Preu"
|
||||||
|
@ -74,13 +74,13 @@ msgctxt "action"
|
||||||
msgid "Add products"
|
msgid "Add products"
|
||||||
msgstr "Afegeix productes"
|
msgstr "Afegeix productes"
|
||||||
|
|
||||||
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:44
|
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:61
|
||||||
#: web/template/invoices/view.gohtml:71
|
#: web/template/invoices/view.gohtml:87
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Subtotal"
|
msgid "Subtotal"
|
||||||
msgstr "Subtotal"
|
msgstr "Subtotal"
|
||||||
|
|
||||||
#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:81
|
#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:97
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Total"
|
msgid "Total"
|
||||||
msgstr "Total"
|
msgstr "Total"
|
||||||
|
@ -100,7 +100,7 @@ msgctxt "invoice"
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Totes"
|
msgstr "Totes"
|
||||||
|
|
||||||
#: web/template/invoices/index.gohtml:22 web/template/invoices/view.gohtml:18
|
#: web/template/invoices/index.gohtml:22 web/template/invoices/view.gohtml:25
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr "Data"
|
msgstr "Data"
|
||||||
|
@ -135,21 +135,26 @@ msgctxt "title"
|
||||||
msgid "Download"
|
msgid "Download"
|
||||||
msgstr "Descàrrega"
|
msgstr "Descàrrega"
|
||||||
|
|
||||||
#: web/template/invoices/index.gohtml:47
|
#: web/template/invoices/index.gohtml:50
|
||||||
msgid "No invoices added yet."
|
msgid "No invoices added yet."
|
||||||
msgstr "No hi ha cap factura."
|
msgstr "No hi ha cap factura."
|
||||||
|
|
||||||
#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:17
|
#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:24
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Invoice %s"
|
msgid "Invoice %s"
|
||||||
msgstr "Factura %s"
|
msgstr "Factura %s"
|
||||||
|
|
||||||
#: web/template/invoices/view.gohtml:41
|
#: web/template/invoices/view.gohtml:16
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Download invoice"
|
||||||
|
msgstr "Descarrega factura"
|
||||||
|
|
||||||
|
#: web/template/invoices/view.gohtml:58
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Concept"
|
msgid "Concept"
|
||||||
msgstr "Concepte"
|
msgstr "Concepte"
|
||||||
|
|
||||||
#: web/template/invoices/view.gohtml:43
|
#: web/template/invoices/view.gohtml:60
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Units"
|
msgid "Units"
|
||||||
msgstr "Unitats"
|
msgstr "Unitats"
|
||||||
|
@ -364,40 +369,40 @@ msgstr "No podeu deixar la contrasenya en blanc."
|
||||||
msgid "Invalid user or password."
|
msgid "Invalid user or password."
|
||||||
msgstr "Nom d’usuari o contrasenya incorrectes."
|
msgstr "Nom d’usuari o contrasenya incorrectes."
|
||||||
|
|
||||||
#: pkg/products.go:165 pkg/invoices.go:392
|
#: pkg/products.go:165 pkg/invoices.go:435
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "Nom"
|
msgstr "Nom"
|
||||||
|
|
||||||
#: pkg/products.go:171 pkg/invoices.go:397
|
#: pkg/products.go:171 pkg/invoices.go:440
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Descripció"
|
msgstr "Descripció"
|
||||||
|
|
||||||
#: pkg/products.go:176 pkg/invoices.go:401
|
#: pkg/products.go:176 pkg/invoices.go:444
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Price"
|
msgid "Price"
|
||||||
msgstr "Preu"
|
msgstr "Preu"
|
||||||
|
|
||||||
#: pkg/products.go:186 pkg/invoices.go:307 pkg/invoices.go:427
|
#: pkg/products.go:186 pkg/invoices.go:350 pkg/invoices.go:470
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Taxes"
|
msgid "Taxes"
|
||||||
msgstr "Imposts"
|
msgstr "Imposts"
|
||||||
|
|
||||||
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:340
|
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:383
|
||||||
#: pkg/invoices.go:463
|
#: pkg/invoices.go:506
|
||||||
msgid "Name can not be empty."
|
msgid "Name can not be empty."
|
||||||
msgstr "No podeu deixar el nom en blanc."
|
msgstr "No podeu deixar el nom en blanc."
|
||||||
|
|
||||||
#: pkg/products.go:207 pkg/invoices.go:464
|
#: pkg/products.go:207 pkg/invoices.go:507
|
||||||
msgid "Price can not be empty."
|
msgid "Price can not be empty."
|
||||||
msgstr "No podeu deixar el preu en blanc."
|
msgstr "No podeu deixar el preu en blanc."
|
||||||
|
|
||||||
#: pkg/products.go:208 pkg/invoices.go:465
|
#: pkg/products.go:208 pkg/invoices.go:508
|
||||||
msgid "Price must be a number greater than zero."
|
msgid "Price must be a number greater than zero."
|
||||||
msgstr "El preu ha de ser un número major a zero."
|
msgstr "El preu ha de ser un número major a zero."
|
||||||
|
|
||||||
#: pkg/products.go:210 pkg/invoices.go:344 pkg/invoices.go:473
|
#: pkg/products.go:210 pkg/invoices.go:387 pkg/invoices.go:516
|
||||||
msgid "Selected tax is not valid."
|
msgid "Selected tax is not valid."
|
||||||
msgstr "Heu seleccionat un impost que no és vàlid."
|
msgstr "Heu seleccionat un impost que no és vàlid."
|
||||||
|
|
||||||
|
@ -460,70 +465,70 @@ msgstr "La confirmació no és igual a la contrasenya."
|
||||||
msgid "Selected language is not valid."
|
msgid "Selected language is not valid."
|
||||||
msgstr "Heu seleccionat un idioma que no és vàlid."
|
msgstr "Heu seleccionat un idioma que no és vàlid."
|
||||||
|
|
||||||
#: pkg/invoices.go:158
|
#: pkg/invoices.go:201
|
||||||
msgid "Select a customer to bill."
|
msgid "Select a customer to bill."
|
||||||
msgstr "Escolliu un client a facturar."
|
msgstr "Escolliu un client a facturar."
|
||||||
|
|
||||||
#: pkg/invoices.go:233
|
#: pkg/invoices.go:276
|
||||||
msgid "Invalid action"
|
msgid "Invalid action"
|
||||||
msgstr "Acció invàlida."
|
msgstr "Acció invàlida."
|
||||||
|
|
||||||
#: pkg/invoices.go:284
|
#: pkg/invoices.go:327
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Customer"
|
msgid "Customer"
|
||||||
msgstr "Client"
|
msgstr "Client"
|
||||||
|
|
||||||
#: pkg/invoices.go:290
|
#: pkg/invoices.go:333
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Number"
|
msgid "Number"
|
||||||
msgstr "Número"
|
msgstr "Número"
|
||||||
|
|
||||||
#: pkg/invoices.go:296
|
#: pkg/invoices.go:339
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Invoice Date"
|
msgid "Invoice Date"
|
||||||
msgstr "Data de factura"
|
msgstr "Data de factura"
|
||||||
|
|
||||||
#: pkg/invoices.go:302
|
#: pkg/invoices.go:345
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr "Notes"
|
msgstr "Notes"
|
||||||
|
|
||||||
#: pkg/invoices.go:341
|
#: pkg/invoices.go:384
|
||||||
msgid "Invoice date can not be empty."
|
msgid "Invoice date can not be empty."
|
||||||
msgstr "No podeu deixar la data de la factura en blanc."
|
msgstr "No podeu deixar la data de la factura en blanc."
|
||||||
|
|
||||||
#: pkg/invoices.go:342
|
#: pkg/invoices.go:385
|
||||||
msgid "Invoice date must be a valid date."
|
msgid "Invoice date must be a valid date."
|
||||||
msgstr "La data de facturació ha de ser vàlida."
|
msgstr "La data de facturació ha de ser vàlida."
|
||||||
|
|
||||||
#: pkg/invoices.go:387
|
#: pkg/invoices.go:430
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr "Identificador"
|
msgstr "Identificador"
|
||||||
|
|
||||||
#: pkg/invoices.go:410
|
#: pkg/invoices.go:453
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Quantity"
|
msgid "Quantity"
|
||||||
msgstr "Quantitat"
|
msgstr "Quantitat"
|
||||||
|
|
||||||
#: pkg/invoices.go:418
|
#: pkg/invoices.go:461
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Discount (%)"
|
msgid "Discount (%)"
|
||||||
msgstr "Descompte (%)"
|
msgstr "Descompte (%)"
|
||||||
|
|
||||||
#: pkg/invoices.go:467
|
#: pkg/invoices.go:510
|
||||||
msgid "Quantity can not be empty."
|
msgid "Quantity can not be empty."
|
||||||
msgstr "No podeu deixar la quantitat en blanc."
|
msgstr "No podeu deixar la quantitat en blanc."
|
||||||
|
|
||||||
#: pkg/invoices.go:468
|
#: pkg/invoices.go:511
|
||||||
msgid "Quantity must be a number greater than zero."
|
msgid "Quantity must be a number greater than zero."
|
||||||
msgstr "La quantitat ha de ser un número major a zero."
|
msgstr "La quantitat ha de ser un número major a zero."
|
||||||
|
|
||||||
#: pkg/invoices.go:470
|
#: pkg/invoices.go:513
|
||||||
msgid "Discount can not be empty."
|
msgid "Discount can not be empty."
|
||||||
msgstr "No podeu deixar el descompte en blanc."
|
msgstr "No podeu deixar el descompte en blanc."
|
||||||
|
|
||||||
#: pkg/invoices.go:471
|
#: pkg/invoices.go:514
|
||||||
msgid "Discount must be a percentage between 0 and 100."
|
msgid "Discount must be a percentage between 0 and 100."
|
||||||
msgstr "El descompte ha de ser un percentatge entre 0 i 100."
|
msgstr "El descompte ha de ser un percentatge entre 0 i 100."
|
||||||
|
|
||||||
|
|
73
po/es.po
73
po/es.po
|
@ -7,7 +7,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: numerus\n"
|
"Project-Id-Version: numerus\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2023-02-24 11:52+0100\n"
|
"POT-Creation-Date: 2023-02-26 17:11+0100\n"
|
||||||
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
|
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
|
||||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||||
|
@ -59,7 +59,7 @@ msgid "Name"
|
||||||
msgstr "Nombre"
|
msgstr "Nombre"
|
||||||
|
|
||||||
#: web/template/invoices/products.gohtml:43
|
#: web/template/invoices/products.gohtml:43
|
||||||
#: web/template/invoices/view.gohtml:42 web/template/products/index.gohtml:23
|
#: web/template/invoices/view.gohtml:59 web/template/products/index.gohtml:23
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Price"
|
msgid "Price"
|
||||||
msgstr "Precio"
|
msgstr "Precio"
|
||||||
|
@ -74,13 +74,13 @@ msgctxt "action"
|
||||||
msgid "Add products"
|
msgid "Add products"
|
||||||
msgstr "Añadir productos"
|
msgstr "Añadir productos"
|
||||||
|
|
||||||
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:44
|
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:61
|
||||||
#: web/template/invoices/view.gohtml:71
|
#: web/template/invoices/view.gohtml:87
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Subtotal"
|
msgid "Subtotal"
|
||||||
msgstr "Subtotal"
|
msgstr "Subtotal"
|
||||||
|
|
||||||
#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:81
|
#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:97
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Total"
|
msgid "Total"
|
||||||
msgstr "Total"
|
msgstr "Total"
|
||||||
|
@ -100,7 +100,7 @@ msgctxt "invoice"
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Todas"
|
msgstr "Todas"
|
||||||
|
|
||||||
#: web/template/invoices/index.gohtml:22 web/template/invoices/view.gohtml:18
|
#: web/template/invoices/index.gohtml:22 web/template/invoices/view.gohtml:25
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr "Fecha"
|
msgstr "Fecha"
|
||||||
|
@ -135,21 +135,26 @@ msgctxt "title"
|
||||||
msgid "Download"
|
msgid "Download"
|
||||||
msgstr "Descargar"
|
msgstr "Descargar"
|
||||||
|
|
||||||
#: web/template/invoices/index.gohtml:47
|
#: web/template/invoices/index.gohtml:50
|
||||||
msgid "No invoices added yet."
|
msgid "No invoices added yet."
|
||||||
msgstr "No hay facturas."
|
msgstr "No hay facturas."
|
||||||
|
|
||||||
#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:17
|
#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:24
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Invoice %s"
|
msgid "Invoice %s"
|
||||||
msgstr "Factura %s"
|
msgstr "Factura %s"
|
||||||
|
|
||||||
#: web/template/invoices/view.gohtml:41
|
#: web/template/invoices/view.gohtml:16
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Download invoice"
|
||||||
|
msgstr "Descargar factura"
|
||||||
|
|
||||||
|
#: web/template/invoices/view.gohtml:58
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Concept"
|
msgid "Concept"
|
||||||
msgstr "Concepto"
|
msgstr "Concepto"
|
||||||
|
|
||||||
#: web/template/invoices/view.gohtml:43
|
#: web/template/invoices/view.gohtml:60
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Units"
|
msgid "Units"
|
||||||
msgstr "Unidades"
|
msgstr "Unidades"
|
||||||
|
@ -364,40 +369,40 @@ msgstr "No podéis dejar la contraseña en blanco."
|
||||||
msgid "Invalid user or password."
|
msgid "Invalid user or password."
|
||||||
msgstr "Nombre de usuario o contraseña inválido."
|
msgstr "Nombre de usuario o contraseña inválido."
|
||||||
|
|
||||||
#: pkg/products.go:165 pkg/invoices.go:392
|
#: pkg/products.go:165 pkg/invoices.go:435
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "Nombre"
|
msgstr "Nombre"
|
||||||
|
|
||||||
#: pkg/products.go:171 pkg/invoices.go:397
|
#: pkg/products.go:171 pkg/invoices.go:440
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Descripción"
|
msgstr "Descripción"
|
||||||
|
|
||||||
#: pkg/products.go:176 pkg/invoices.go:401
|
#: pkg/products.go:176 pkg/invoices.go:444
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Price"
|
msgid "Price"
|
||||||
msgstr "Precio"
|
msgstr "Precio"
|
||||||
|
|
||||||
#: pkg/products.go:186 pkg/invoices.go:307 pkg/invoices.go:427
|
#: pkg/products.go:186 pkg/invoices.go:350 pkg/invoices.go:470
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Taxes"
|
msgid "Taxes"
|
||||||
msgstr "Impuestos"
|
msgstr "Impuestos"
|
||||||
|
|
||||||
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:340
|
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:383
|
||||||
#: pkg/invoices.go:463
|
#: pkg/invoices.go:506
|
||||||
msgid "Name can not be empty."
|
msgid "Name can not be empty."
|
||||||
msgstr "No podéis dejar el nombre en blanco."
|
msgstr "No podéis dejar el nombre en blanco."
|
||||||
|
|
||||||
#: pkg/products.go:207 pkg/invoices.go:464
|
#: pkg/products.go:207 pkg/invoices.go:507
|
||||||
msgid "Price can not be empty."
|
msgid "Price can not be empty."
|
||||||
msgstr "No podéis dejar el precio en blanco."
|
msgstr "No podéis dejar el precio en blanco."
|
||||||
|
|
||||||
#: pkg/products.go:208 pkg/invoices.go:465
|
#: pkg/products.go:208 pkg/invoices.go:508
|
||||||
msgid "Price must be a number greater than zero."
|
msgid "Price must be a number greater than zero."
|
||||||
msgstr "El precio tiene que ser un número mayor a cero."
|
msgstr "El precio tiene que ser un número mayor a cero."
|
||||||
|
|
||||||
#: pkg/products.go:210 pkg/invoices.go:344 pkg/invoices.go:473
|
#: pkg/products.go:210 pkg/invoices.go:387 pkg/invoices.go:516
|
||||||
msgid "Selected tax is not valid."
|
msgid "Selected tax is not valid."
|
||||||
msgstr "Habéis escogido un impuesto que no es válido."
|
msgstr "Habéis escogido un impuesto que no es válido."
|
||||||
|
|
||||||
|
@ -460,70 +465,70 @@ msgstr "La confirmación no corresponde con la contraseña."
|
||||||
msgid "Selected language is not valid."
|
msgid "Selected language is not valid."
|
||||||
msgstr "Habéis escogido un idioma que no es válido."
|
msgstr "Habéis escogido un idioma que no es válido."
|
||||||
|
|
||||||
#: pkg/invoices.go:158
|
#: pkg/invoices.go:201
|
||||||
msgid "Select a customer to bill."
|
msgid "Select a customer to bill."
|
||||||
msgstr "Escoged un cliente a facturar."
|
msgstr "Escoged un cliente a facturar."
|
||||||
|
|
||||||
#: pkg/invoices.go:233
|
#: pkg/invoices.go:276
|
||||||
msgid "Invalid action"
|
msgid "Invalid action"
|
||||||
msgstr "Acción inválida."
|
msgstr "Acción inválida."
|
||||||
|
|
||||||
#: pkg/invoices.go:284
|
#: pkg/invoices.go:327
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Customer"
|
msgid "Customer"
|
||||||
msgstr "Cliente"
|
msgstr "Cliente"
|
||||||
|
|
||||||
#: pkg/invoices.go:290
|
#: pkg/invoices.go:333
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Number"
|
msgid "Number"
|
||||||
msgstr "Número"
|
msgstr "Número"
|
||||||
|
|
||||||
#: pkg/invoices.go:296
|
#: pkg/invoices.go:339
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Invoice Date"
|
msgid "Invoice Date"
|
||||||
msgstr "Fecha de factura"
|
msgstr "Fecha de factura"
|
||||||
|
|
||||||
#: pkg/invoices.go:302
|
#: pkg/invoices.go:345
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr "Notas"
|
msgstr "Notas"
|
||||||
|
|
||||||
#: pkg/invoices.go:341
|
#: pkg/invoices.go:384
|
||||||
msgid "Invoice date can not be empty."
|
msgid "Invoice date can not be empty."
|
||||||
msgstr "No podéis dejar la fecha de la factura en blanco."
|
msgstr "No podéis dejar la fecha de la factura en blanco."
|
||||||
|
|
||||||
#: pkg/invoices.go:342
|
#: pkg/invoices.go:385
|
||||||
msgid "Invoice date must be a valid date."
|
msgid "Invoice date must be a valid date."
|
||||||
msgstr "La fecha de factura debe ser válida."
|
msgstr "La fecha de factura debe ser válida."
|
||||||
|
|
||||||
#: pkg/invoices.go:387
|
#: pkg/invoices.go:430
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr "Identificador"
|
msgstr "Identificador"
|
||||||
|
|
||||||
#: pkg/invoices.go:410
|
#: pkg/invoices.go:453
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Quantity"
|
msgid "Quantity"
|
||||||
msgstr "Cantidad"
|
msgstr "Cantidad"
|
||||||
|
|
||||||
#: pkg/invoices.go:418
|
#: pkg/invoices.go:461
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Discount (%)"
|
msgid "Discount (%)"
|
||||||
msgstr "Descuento (%)"
|
msgstr "Descuento (%)"
|
||||||
|
|
||||||
#: pkg/invoices.go:467
|
#: pkg/invoices.go:510
|
||||||
msgid "Quantity can not be empty."
|
msgid "Quantity can not be empty."
|
||||||
msgstr "No podéis dejar la cantidad en blanco."
|
msgstr "No podéis dejar la cantidad en blanco."
|
||||||
|
|
||||||
#: pkg/invoices.go:468
|
#: pkg/invoices.go:511
|
||||||
msgid "Quantity must be a number greater than zero."
|
msgid "Quantity must be a number greater than zero."
|
||||||
msgstr "La cantidad tiene que ser un número mayor a cero."
|
msgstr "La cantidad tiene que ser un número mayor a cero."
|
||||||
|
|
||||||
#: pkg/invoices.go:470
|
#: pkg/invoices.go:513
|
||||||
msgid "Discount can not be empty."
|
msgid "Discount can not be empty."
|
||||||
msgstr "No podéis dejar el descuento en blanco."
|
msgstr "No podéis dejar el descuento en blanco."
|
||||||
|
|
||||||
#: pkg/invoices.go:471
|
#: pkg/invoices.go:514
|
||||||
msgid "Discount must be a percentage between 0 and 100."
|
msgid "Discount must be a percentage between 0 and 100."
|
||||||
msgstr "El descuento tiene que ser un percentage entre 0 y 100."
|
msgstr "El descuento tiene que ser un percentage entre 0 y 100."
|
||||||
|
|
||||||
|
|
|
@ -508,6 +508,15 @@ main > nav {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-download {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-download a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Remix Icon */
|
/* Remix Icon */
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|
|
@ -39,7 +39,10 @@
|
||||||
<td class="invoice-status-{{ .Status }}">{{ .StatusLabel }}</td>
|
<td class="invoice-status-{{ .Status }}">{{ .StatusLabel }}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td class="numeric">{{ .Total|formatPrice }}</td>
|
<td class="numeric">{{ .Total|formatPrice }}</td>
|
||||||
<td></td>
|
<td class="invoice-download"><a href="{{ companyURI "/invoices/"}}{{ .Slug }}.pdf" download="{{ .Number}}.pdf"
|
||||||
|
title="{{( pgettext "Download invoice" "action" )}}"
|
||||||
|
aria-label="{{( pgettext "Download invoice %s" "action" )}}"><i
|
||||||
|
class="ri-download-line"></i></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
|
|
@ -10,7 +10,13 @@
|
||||||
<a href="{{ companyURI "/invoices"}}">{{( pgettext "Invoices" "title" )}}</a> /
|
<a href="{{ companyURI "/invoices"}}">{{( pgettext "Invoices" "title" )}}</a> /
|
||||||
<a>{{ .Number }}</a>
|
<a>{{ .Number }}</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<a class="primary button"
|
||||||
|
href="{{ companyURI "/invoices/" }}{{ .Slug }}.pdf"
|
||||||
|
download="{{ .Number}}.pdf">{{( pgettext "Download invoice" "action" )}}</a>
|
||||||
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/invoice.css">
|
<link rel="stylesheet" type="text/css" href="/static/invoice.css">
|
||||||
<article class="invoice">
|
<article class="invoice">
|
||||||
<header>
|
<header>
|
||||||
|
|
Loading…
Reference in New Issue