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
|
||||
Depends:
|
||||
${shlibs:Depends},
|
||||
${misc:Depends}
|
||||
${misc:Depends},
|
||||
weasyprint
|
||||
Built-Using: ${misc:Built-Using}
|
||||
Description: Simple invoicing and accounting web application
|
||||
A simple web application to keep invoice and accouting records, intended for
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -52,7 +57,7 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company
|
|||
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)
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
|
@ -65,16 +70,52 @@ func GetInvoiceForm(w http.ResponseWriter, r *http.Request, params httprouter.Pa
|
|||
return
|
||||
}
|
||||
|
||||
pdf := false
|
||||
if strings.HasSuffix(slug, ".pdf") {
|
||||
pdf = true
|
||||
slug = slug[:len(slug)-len(".pdf")]
|
||||
}
|
||||
invoice := mustGetInvoice(r.Context(), conn, company, slug)
|
||||
if invoice == nil {
|
||||
http.NotFound(w, r)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
type invoice struct {
|
||||
Number string
|
||||
Slug string
|
||||
Date time.Time
|
||||
Invoicer taxDetails
|
||||
Invoicee taxDetails
|
||||
|
@ -105,7 +146,9 @@ type invoiceProduct struct {
|
|||
}
|
||||
|
||||
func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug string) *invoice {
|
||||
inv := &invoice{}
|
||||
inv := &invoice{
|
||||
Slug: slug,
|
||||
}
|
||||
var invoiceId 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)) {
|
||||
|
|
|
@ -24,7 +24,7 @@ func NewRouter(db *Db) http.Handler {
|
|||
companyRouter.PUT("/products/:slug", HandleUpdateProduct)
|
||||
companyRouter.GET("/invoices", IndexInvoices)
|
||||
companyRouter.POST("/invoices", HandleAddInvoice)
|
||||
companyRouter.GET("/invoices/:slug", GetInvoiceForm)
|
||||
companyRouter.GET("/invoices/:slug", ServeInvoice)
|
||||
companyRouter.POST("/invoices/new/products", HandleAddProductsToInvoice)
|
||||
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
|
||||
|
|
73
po/ca.po
73
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\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"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||
|
@ -59,7 +59,7 @@ msgid "Name"
|
|||
msgstr "Nom"
|
||||
|
||||
#: 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"
|
||||
msgid "Price"
|
||||
msgstr "Preu"
|
||||
|
@ -74,13 +74,13 @@ msgctxt "action"
|
|||
msgid "Add products"
|
||||
msgstr "Afegeix productes"
|
||||
|
||||
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:44
|
||||
#: web/template/invoices/view.gohtml:71
|
||||
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:61
|
||||
#: web/template/invoices/view.gohtml:87
|
||||
msgctxt "title"
|
||||
msgid "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"
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
@ -100,7 +100,7 @@ msgctxt "invoice"
|
|||
msgid "All"
|
||||
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"
|
||||
msgid "Date"
|
||||
msgstr "Data"
|
||||
|
@ -135,21 +135,26 @@ msgctxt "title"
|
|||
msgid "Download"
|
||||
msgstr "Descàrrega"
|
||||
|
||||
#: web/template/invoices/index.gohtml:47
|
||||
#: web/template/invoices/index.gohtml:50
|
||||
msgid "No invoices added yet."
|
||||
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"
|
||||
msgid "Invoice %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"
|
||||
msgid "Concept"
|
||||
msgstr "Concepte"
|
||||
|
||||
#: web/template/invoices/view.gohtml:43
|
||||
#: web/template/invoices/view.gohtml:60
|
||||
msgctxt "title"
|
||||
msgid "Units"
|
||||
msgstr "Unitats"
|
||||
|
@ -364,40 +369,40 @@ msgstr "No podeu deixar la contrasenya en blanc."
|
|||
msgid "Invalid user or password."
|
||||
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"
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: pkg/products.go:171 pkg/invoices.go:397
|
||||
#: pkg/products.go:171 pkg/invoices.go:440
|
||||
msgctxt "input"
|
||||
msgid "Description"
|
||||
msgstr "Descripció"
|
||||
|
||||
#: pkg/products.go:176 pkg/invoices.go:401
|
||||
#: pkg/products.go:176 pkg/invoices.go:444
|
||||
msgctxt "input"
|
||||
msgid "Price"
|
||||
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"
|
||||
msgid "Taxes"
|
||||
msgstr "Imposts"
|
||||
|
||||
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:340
|
||||
#: pkg/invoices.go:463
|
||||
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:383
|
||||
#: pkg/invoices.go:506
|
||||
msgid "Name can not be empty."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
msgstr "Heu seleccionat un idioma que no és vàlid."
|
||||
|
||||
#: pkg/invoices.go:158
|
||||
#: pkg/invoices.go:201
|
||||
msgid "Select a customer to bill."
|
||||
msgstr "Escolliu un client a facturar."
|
||||
|
||||
#: pkg/invoices.go:233
|
||||
#: pkg/invoices.go:276
|
||||
msgid "Invalid action"
|
||||
msgstr "Acció invàlida."
|
||||
|
||||
#: pkg/invoices.go:284
|
||||
#: pkg/invoices.go:327
|
||||
msgctxt "input"
|
||||
msgid "Customer"
|
||||
msgstr "Client"
|
||||
|
||||
#: pkg/invoices.go:290
|
||||
#: pkg/invoices.go:333
|
||||
msgctxt "input"
|
||||
msgid "Number"
|
||||
msgstr "Número"
|
||||
|
||||
#: pkg/invoices.go:296
|
||||
#: pkg/invoices.go:339
|
||||
msgctxt "input"
|
||||
msgid "Invoice Date"
|
||||
msgstr "Data de factura"
|
||||
|
||||
#: pkg/invoices.go:302
|
||||
#: pkg/invoices.go:345
|
||||
msgctxt "input"
|
||||
msgid "Notes"
|
||||
msgstr "Notes"
|
||||
|
||||
#: pkg/invoices.go:341
|
||||
#: pkg/invoices.go:384
|
||||
msgid "Invoice date can not be empty."
|
||||
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."
|
||||
msgstr "La data de facturació ha de ser vàlida."
|
||||
|
||||
#: pkg/invoices.go:387
|
||||
#: pkg/invoices.go:430
|
||||
msgctxt "input"
|
||||
msgid "Id"
|
||||
msgstr "Identificador"
|
||||
|
||||
#: pkg/invoices.go:410
|
||||
#: pkg/invoices.go:453
|
||||
msgctxt "input"
|
||||
msgid "Quantity"
|
||||
msgstr "Quantitat"
|
||||
|
||||
#: pkg/invoices.go:418
|
||||
#: pkg/invoices.go:461
|
||||
msgctxt "input"
|
||||
msgid "Discount (%)"
|
||||
msgstr "Descompte (%)"
|
||||
|
||||
#: pkg/invoices.go:467
|
||||
#: pkg/invoices.go:510
|
||||
msgid "Quantity can not be empty."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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 ""
|
||||
"Project-Id-Version: numerus\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"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||
|
@ -59,7 +59,7 @@ msgid "Name"
|
|||
msgstr "Nombre"
|
||||
|
||||
#: 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"
|
||||
msgid "Price"
|
||||
msgstr "Precio"
|
||||
|
@ -74,13 +74,13 @@ msgctxt "action"
|
|||
msgid "Add products"
|
||||
msgstr "Añadir productos"
|
||||
|
||||
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:44
|
||||
#: web/template/invoices/view.gohtml:71
|
||||
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:61
|
||||
#: web/template/invoices/view.gohtml:87
|
||||
msgctxt "title"
|
||||
msgid "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"
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
@ -100,7 +100,7 @@ msgctxt "invoice"
|
|||
msgid "All"
|
||||
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"
|
||||
msgid "Date"
|
||||
msgstr "Fecha"
|
||||
|
@ -135,21 +135,26 @@ msgctxt "title"
|
|||
msgid "Download"
|
||||
msgstr "Descargar"
|
||||
|
||||
#: web/template/invoices/index.gohtml:47
|
||||
#: web/template/invoices/index.gohtml:50
|
||||
msgid "No invoices added yet."
|
||||
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"
|
||||
msgid "Invoice %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"
|
||||
msgid "Concept"
|
||||
msgstr "Concepto"
|
||||
|
||||
#: web/template/invoices/view.gohtml:43
|
||||
#: web/template/invoices/view.gohtml:60
|
||||
msgctxt "title"
|
||||
msgid "Units"
|
||||
msgstr "Unidades"
|
||||
|
@ -364,40 +369,40 @@ msgstr "No podéis dejar la contraseña en blanco."
|
|||
msgid "Invalid user or password."
|
||||
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"
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: pkg/products.go:171 pkg/invoices.go:397
|
||||
#: pkg/products.go:171 pkg/invoices.go:440
|
||||
msgctxt "input"
|
||||
msgid "Description"
|
||||
msgstr "Descripción"
|
||||
|
||||
#: pkg/products.go:176 pkg/invoices.go:401
|
||||
#: pkg/products.go:176 pkg/invoices.go:444
|
||||
msgctxt "input"
|
||||
msgid "Price"
|
||||
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"
|
||||
msgid "Taxes"
|
||||
msgstr "Impuestos"
|
||||
|
||||
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:340
|
||||
#: pkg/invoices.go:463
|
||||
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:383
|
||||
#: pkg/invoices.go:506
|
||||
msgid "Name can not be empty."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
msgstr "Escoged un cliente a facturar."
|
||||
|
||||
#: pkg/invoices.go:233
|
||||
#: pkg/invoices.go:276
|
||||
msgid "Invalid action"
|
||||
msgstr "Acción inválida."
|
||||
|
||||
#: pkg/invoices.go:284
|
||||
#: pkg/invoices.go:327
|
||||
msgctxt "input"
|
||||
msgid "Customer"
|
||||
msgstr "Cliente"
|
||||
|
||||
#: pkg/invoices.go:290
|
||||
#: pkg/invoices.go:333
|
||||
msgctxt "input"
|
||||
msgid "Number"
|
||||
msgstr "Número"
|
||||
|
||||
#: pkg/invoices.go:296
|
||||
#: pkg/invoices.go:339
|
||||
msgctxt "input"
|
||||
msgid "Invoice Date"
|
||||
msgstr "Fecha de factura"
|
||||
|
||||
#: pkg/invoices.go:302
|
||||
#: pkg/invoices.go:345
|
||||
msgctxt "input"
|
||||
msgid "Notes"
|
||||
msgstr "Notas"
|
||||
|
||||
#: pkg/invoices.go:341
|
||||
#: pkg/invoices.go:384
|
||||
msgid "Invoice date can not be empty."
|
||||
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."
|
||||
msgstr "La fecha de factura debe ser válida."
|
||||
|
||||
#: pkg/invoices.go:387
|
||||
#: pkg/invoices.go:430
|
||||
msgctxt "input"
|
||||
msgid "Id"
|
||||
msgstr "Identificador"
|
||||
|
||||
#: pkg/invoices.go:410
|
||||
#: pkg/invoices.go:453
|
||||
msgctxt "input"
|
||||
msgid "Quantity"
|
||||
msgstr "Cantidad"
|
||||
|
||||
#: pkg/invoices.go:418
|
||||
#: pkg/invoices.go:461
|
||||
msgctxt "input"
|
||||
msgid "Discount (%)"
|
||||
msgstr "Descuento (%)"
|
||||
|
||||
#: pkg/invoices.go:467
|
||||
#: pkg/invoices.go:510
|
||||
msgid "Quantity can not be empty."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
msgstr "El descuento tiene que ser un percentage entre 0 y 100."
|
||||
|
||||
|
|
|
@ -508,6 +508,15 @@ main > nav {
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.invoice-download {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invoice-download a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Remix Icon */
|
||||
|
||||
@font-face {
|
||||
|
|
|
@ -39,7 +39,10 @@
|
|||
<td class="invoice-status-{{ .Status }}">{{ .StatusLabel }}</td>
|
||||
<td></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>
|
||||
{{- end }}
|
||||
{{ else }}
|
||||
|
|
|
@ -10,7 +10,13 @@
|
|||
<a href="{{ companyURI "/invoices"}}">{{( pgettext "Invoices" "title" )}}</a> /
|
||||
<a>{{ .Number }}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="primary button"
|
||||
href="{{ companyURI "/invoices/" }}{{ .Slug }}.pdf"
|
||||
download="{{ .Number}}.pdf">{{( pgettext "Download invoice" "action" )}}</a>
|
||||
</p>
|
||||
</nav>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/static/invoice.css">
|
||||
<article class="invoice">
|
||||
<header>
|
||||
|
|
Loading…
Reference in New Issue