diff --git a/debian/control b/debian/control index 9f3c3fe..2ede999 100644 --- a/debian/control +++ b/debian/control @@ -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 diff --git a/pkg/invoices.go b/pkg/invoices.go index eb6eb3b..f68e665 100644 --- a/pkg/invoices.go +++ b/pkg/invoices.go @@ -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 } - mustRenderAppTemplate(w, r, "invoices/view.gohtml", invoice) + 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)) { diff --git a/pkg/router.go b/pkg/router.go index 3c56ede..eabc6d3 100644 --- a/pkg/router.go +++ b/pkg/router.go @@ -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) diff --git a/po/ca.po b/po/ca.po index c6cc89e..194fffb 100644 --- a/po/ca.po +++ b/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 \n" "Language-Team: Catalan \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." diff --git a/po/es.po b/po/es.po index d3a2596..41577b6 100644 --- a/po/es.po +++ b/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 \n" "Language-Team: Spanish \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." diff --git a/web/static/numerus.css b/web/static/numerus.css index 11c07d2..233e392 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -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 { diff --git a/web/template/invoices/index.gohtml b/web/template/invoices/index.gohtml index 2838866..986b667 100644 --- a/web/template/invoices/index.gohtml +++ b/web/template/invoices/index.gohtml @@ -39,7 +39,10 @@ {{ .StatusLabel }} {{ .Total|formatPrice }} - + {{- end }} {{ else }} diff --git a/web/template/invoices/view.gohtml b/web/template/invoices/view.gohtml index fc99b47..0ab61b8 100644 --- a/web/template/invoices/view.gohtml +++ b/web/template/invoices/view.gohtml @@ -10,7 +10,13 @@ {{( pgettext "Invoices" "title" )}} / {{ .Number }}

+

+ {{( pgettext "Download invoice" "action" )}} +

+