From 5dedaefc22908a14d1696b889ad645d8122dd2f0 Mon Sep 17 00:00:00 2001
From: jordi fita mas
Date: Thu, 9 Mar 2023 12:11:53 +0100
Subject: [PATCH] Add button to download many invoices as PDF in a ZIP archive
---
pkg/invoices.go | 119 ++++++++++++++++++++++-------
pkg/router.go | 1 +
po/ca.po | 94 +++++++++++++----------
po/es.po | 94 +++++++++++++----------
web/template/invoices/index.gohtml | 21 +++--
5 files changed, 215 insertions(+), 114 deletions(-)
diff --git a/pkg/invoices.go b/pkg/invoices.go
index c203950..fe39bf5 100644
--- a/pkg/invoices.go
+++ b/pkg/invoices.go
@@ -1,8 +1,10 @@
package pkg
import (
+ "archive/zip"
"bytes"
"context"
+ "errors"
"fmt"
"github.com/julienschmidt/httprouter"
"html/template"
@@ -10,6 +12,7 @@ import (
"log"
"math"
"net/http"
+ "os"
"os/exec"
"sort"
"strconv"
@@ -103,41 +106,50 @@ func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Para
pdf = true
slug = slug[:len(slug)-len(".pdf")]
}
- invoice := mustGetInvoice(r.Context(), conn, company, slug)
- if invoice == nil {
+ inv := mustGetInvoice(r.Context(), conn, company, slug)
+ if inv == nil {
http.NotFound(w, r)
return
}
if pdf {
- cmd := exec.Command("weasyprint", "--format", "pdf", "--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 mustClose(stdout)
- if err = cmd.Start(); err != nil {
- panic(err)
- }
- go func() {
- defer mustClose(stdin)
- 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)
- }
+ mustWriteInvoicePdf(w, r, inv)
} else {
- mustRenderAppTemplate(w, r, "invoices/view.gohtml", invoice)
+ mustRenderAppTemplate(w, r, "invoices/view.gohtml", inv)
+ }
+}
+
+func mustWriteInvoicePdf(w io.Writer, r *http.Request, inv *invoice) {
+ cmd := exec.Command("weasyprint", "--format", "pdf", "--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 func() {
+ err := stdout.Close()
+ if !errors.Is(err, os.ErrClosed) {
+ panic(err)
+ }
+ }()
+ if err = cmd.Start(); err != nil {
+ panic(err)
+ }
+ go func() {
+ defer mustClose(stdin)
+ mustRenderAppTemplate(stdin, r, "invoices/view.gohtml", inv)
+ }()
+ 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)
}
}
@@ -352,6 +364,55 @@ func HandleNewInvoiceAction(w http.ResponseWriter, r *http.Request, _ httprouter
}
}
+func HandleBatchInvoiceAction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if err := verifyCsrfTokenValid(r); err != nil {
+ http.Error(w, err.Error(), http.StatusForbidden)
+ return
+ }
+ slugs := r.Form["invoice"]
+ if len(slugs) == 0 {
+ http.Redirect(w, r, companyURI(mustGetCompany(r), "/invoices"), http.StatusSeeOther)
+ return
+ }
+ locale := getLocale(r)
+ switch r.Form.Get("action") {
+ case "download":
+ invoices := mustWriteInvoicesPdf(r, slugs)
+ w.Header().Set("Content-Type", "application/zip")
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", gettext("invoices.zip", locale)))
+ w.WriteHeader(http.StatusOK)
+ if _, err := w.Write(invoices); err != nil {
+ panic(err)
+ }
+ default:
+ http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
+ }
+}
+
+func mustWriteInvoicesPdf(r *http.Request, slugs []string) []byte {
+ conn := getConn(r)
+ company := mustGetCompany(r)
+ buf := new(bytes.Buffer)
+ w := zip.NewWriter(buf)
+ for _, slug := range slugs {
+ inv := mustGetInvoice(r.Context(), conn, company, slug)
+ if inv == nil {
+ continue
+ }
+ f, err := w.Create(inv.Number + ".pdf")
+ if err != nil {
+ panic(err)
+ }
+ mustWriteInvoicePdf(f, r, inv)
+ }
+ mustClose(w)
+ return buf.Bytes()
+}
+
type invoiceForm struct {
locale *Locale
company *Company
diff --git a/pkg/router.go b/pkg/router.go
index 522434a..1dcad24 100644
--- a/pkg/router.go
+++ b/pkg/router.go
@@ -29,6 +29,7 @@ func NewRouter(db *Db) http.Handler {
companyRouter.GET("/invoices/:slug", ServeInvoice)
companyRouter.PUT("/invoices/:slug", HandleUpdateInvoice)
companyRouter.POST("/invoices/new", HandleNewInvoiceAction)
+ companyRouter.POST("/invoices/batch", HandleBatchInvoiceAction)
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 12021e4..c7cc2ae 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-03-08 11:51+0100\n"
+"POT-Creation-Date: 2023-03-09 12:08+0100\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas \n"
"Language-Team: Catalan \n"
@@ -90,62 +90,72 @@ msgctxt "action"
msgid "Update"
msgstr "Actualitza"
-#: web/template/invoices/new.gohtml:66 web/template/invoices/index.gohtml:13
+#: web/template/invoices/new.gohtml:66 web/template/invoices/index.gohtml:19
msgctxt "action"
msgid "New invoice"
msgstr "Nova factura"
-#: web/template/invoices/index.gohtml:21
+#: web/template/invoices/index.gohtml:17
+msgctxt "action"
+msgid "Download invoices"
+msgstr "Descarrega factures"
+
+#: web/template/invoices/index.gohtml:28
msgctxt "invoice"
msgid "All"
msgstr "Totes"
-#: web/template/invoices/index.gohtml:22 web/template/invoices/view.gohtml:26
+#: web/template/invoices/index.gohtml:29 web/template/invoices/view.gohtml:26
msgctxt "title"
msgid "Date"
msgstr "Data"
-#: web/template/invoices/index.gohtml:23
+#: web/template/invoices/index.gohtml:30
msgctxt "title"
msgid "Invoice Num."
msgstr "Núm. factura"
-#: web/template/invoices/index.gohtml:24 web/template/contacts/index.gohtml:22
+#: web/template/invoices/index.gohtml:31 web/template/contacts/index.gohtml:22
msgctxt "title"
msgid "Customer"
msgstr "Client"
-#: web/template/invoices/index.gohtml:25
+#: web/template/invoices/index.gohtml:32
msgctxt "title"
msgid "Status"
msgstr "Estat"
-#: web/template/invoices/index.gohtml:26
+#: web/template/invoices/index.gohtml:33
msgctxt "title"
msgid "Label"
msgstr "Etiqueta"
-#: web/template/invoices/index.gohtml:27
+#: web/template/invoices/index.gohtml:34
msgctxt "title"
msgid "Amount"
msgstr "Import"
-#: web/template/invoices/index.gohtml:28
+#: web/template/invoices/index.gohtml:35
msgctxt "title"
msgid "Download"
msgstr "Descàrrega"
-#: web/template/invoices/index.gohtml:29
+#: web/template/invoices/index.gohtml:36
msgctxt "title"
msgid "Actions"
msgstr "Accions"
-#: web/template/invoices/index.gohtml:75 web/template/invoices/view.gohtml:14
+#: web/template/invoices/index.gohtml:43
+msgctxt "action"
+msgid "Select invoice %v"
+msgstr "Selecciona factura %v"
+
+#: web/template/invoices/index.gohtml:86 web/template/invoices/view.gohtml:14
msgctxt "action"
msgid "Duplicate"
msgstr "Duplica"
-#: web/template/invoices/index.gohtml:85
+#: web/template/invoices/index.gohtml:96
msgid "No invoices added yet."
msgstr "No hi ha cap factura."
@@ -418,44 +428,44 @@ 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:517
+#: pkg/products.go:165 pkg/invoices.go:578
msgctxt "input"
msgid "Name"
msgstr "Nom"
-#: pkg/products.go:171 pkg/invoices.go:522
+#: pkg/products.go:171 pkg/invoices.go:583
msgctxt "input"
msgid "Description"
msgstr "Descripció"
-#: pkg/products.go:176 pkg/invoices.go:526
+#: pkg/products.go:176 pkg/invoices.go:587
msgctxt "input"
msgid "Price"
msgstr "Preu"
-#: pkg/products.go:186 pkg/invoices.go:552
+#: pkg/products.go:186 pkg/invoices.go:613
msgctxt "input"
msgid "Taxes"
msgstr "Imposts"
-#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:431
-#: pkg/invoices.go:588
+#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:492
+#: pkg/invoices.go:649
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
-#: pkg/products.go:207 pkg/invoices.go:589
+#: pkg/products.go:207 pkg/invoices.go:650
msgid "Price can not be empty."
msgstr "No podeu deixar el preu en blanc."
-#: pkg/products.go:208 pkg/invoices.go:590
+#: pkg/products.go:208 pkg/invoices.go:651
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:598
+#: pkg/products.go:210 pkg/invoices.go:659
msgid "Selected tax is not valid."
msgstr "Heu seleccionat un impost que no és vàlid."
-#: pkg/products.go:211 pkg/invoices.go:599
+#: pkg/products.go:211 pkg/invoices.go:660
msgid "You can only select a tax of each class."
msgstr "Només podeu seleccionar un impost de cada classe."
@@ -563,79 +573,83 @@ 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:258
+#: pkg/invoices.go:270
msgid "Select a customer to bill."
msgstr "Escolliu un client a facturar."
-#: pkg/invoices.go:351
+#: pkg/invoices.go:363 pkg/invoices.go:392
msgid "Invalid action"
msgstr "Acció invàlida."
-#: pkg/invoices.go:372
+#: pkg/invoices.go:386
+msgid "invoices.zip"
+msgstr "factures.zip"
+
+#: pkg/invoices.go:433
msgctxt "input"
msgid "Customer"
msgstr "Client"
-#: pkg/invoices.go:378
+#: pkg/invoices.go:439
msgctxt "input"
msgid "Number"
msgstr "Número"
-#: pkg/invoices.go:384
+#: pkg/invoices.go:445
msgctxt "input"
msgid "Invoice Date"
msgstr "Data de factura"
-#: pkg/invoices.go:390
+#: pkg/invoices.go:451
msgctxt "input"
msgid "Notes"
msgstr "Notes"
-#: pkg/invoices.go:396
+#: pkg/invoices.go:457
msgctxt "input"
msgid "Payment Method"
msgstr "Mètode de pagament"
-#: pkg/invoices.go:432
+#: pkg/invoices.go:493
msgid "Invoice date can not be empty."
msgstr "No podeu deixar la data de la factura en blanc."
-#: pkg/invoices.go:433
+#: pkg/invoices.go:494
msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida."
-#: pkg/invoices.go:435
+#: pkg/invoices.go:496
msgid "Selected payment method is not valid."
msgstr "Heu seleccionat un mètode de pagament que no és vàlid."
-#: pkg/invoices.go:512
+#: pkg/invoices.go:573
msgctxt "input"
msgid "Id"
msgstr "Identificador"
-#: pkg/invoices.go:535
+#: pkg/invoices.go:596
msgctxt "input"
msgid "Quantity"
msgstr "Quantitat"
-#: pkg/invoices.go:543
+#: pkg/invoices.go:604
msgctxt "input"
msgid "Discount (%)"
msgstr "Descompte (%)"
-#: pkg/invoices.go:592
+#: pkg/invoices.go:653
msgid "Quantity can not be empty."
msgstr "No podeu deixar la quantitat en blanc."
-#: pkg/invoices.go:593
+#: pkg/invoices.go:654
msgid "Quantity must be a number greater than zero."
msgstr "La quantitat ha de ser un número major a zero."
-#: pkg/invoices.go:595
+#: pkg/invoices.go:656
msgid "Discount can not be empty."
msgstr "No podeu deixar el descompte en blanc."
-#: pkg/invoices.go:596
+#: pkg/invoices.go:657
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 27b4c5d..54e38f4 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-03-08 11:51+0100\n"
+"POT-Creation-Date: 2023-03-09 12:08+0100\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas \n"
"Language-Team: Spanish \n"
@@ -90,62 +90,72 @@ msgctxt "action"
msgid "Update"
msgstr "Actualizar"
-#: web/template/invoices/new.gohtml:66 web/template/invoices/index.gohtml:13
+#: web/template/invoices/new.gohtml:66 web/template/invoices/index.gohtml:19
msgctxt "action"
msgid "New invoice"
msgstr "Nueva factura"
-#: web/template/invoices/index.gohtml:21
+#: web/template/invoices/index.gohtml:17
+msgctxt "action"
+msgid "Download invoices"
+msgstr "Descargar facturas"
+
+#: web/template/invoices/index.gohtml:28
msgctxt "invoice"
msgid "All"
msgstr "Todas"
-#: web/template/invoices/index.gohtml:22 web/template/invoices/view.gohtml:26
+#: web/template/invoices/index.gohtml:29 web/template/invoices/view.gohtml:26
msgctxt "title"
msgid "Date"
msgstr "Fecha"
-#: web/template/invoices/index.gohtml:23
+#: web/template/invoices/index.gohtml:30
msgctxt "title"
msgid "Invoice Num."
msgstr "Nº factura"
-#: web/template/invoices/index.gohtml:24 web/template/contacts/index.gohtml:22
+#: web/template/invoices/index.gohtml:31 web/template/contacts/index.gohtml:22
msgctxt "title"
msgid "Customer"
msgstr "Cliente"
-#: web/template/invoices/index.gohtml:25
+#: web/template/invoices/index.gohtml:32
msgctxt "title"
msgid "Status"
msgstr "Estado"
-#: web/template/invoices/index.gohtml:26
+#: web/template/invoices/index.gohtml:33
msgctxt "title"
msgid "Label"
msgstr "Etiqueta"
-#: web/template/invoices/index.gohtml:27
+#: web/template/invoices/index.gohtml:34
msgctxt "title"
msgid "Amount"
msgstr "Importe"
-#: web/template/invoices/index.gohtml:28
+#: web/template/invoices/index.gohtml:35
msgctxt "title"
msgid "Download"
msgstr "Descargar"
-#: web/template/invoices/index.gohtml:29
+#: web/template/invoices/index.gohtml:36
msgctxt "title"
msgid "Actions"
msgstr "Acciones"
-#: web/template/invoices/index.gohtml:75 web/template/invoices/view.gohtml:14
+#: web/template/invoices/index.gohtml:43
+msgctxt "action"
+msgid "Select invoice %v"
+msgstr "Seleccionar factura %v"
+
+#: web/template/invoices/index.gohtml:86 web/template/invoices/view.gohtml:14
msgctxt "action"
msgid "Duplicate"
msgstr "Duplicar"
-#: web/template/invoices/index.gohtml:85
+#: web/template/invoices/index.gohtml:96
msgid "No invoices added yet."
msgstr "No hay facturas."
@@ -418,44 +428,44 @@ 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:517
+#: pkg/products.go:165 pkg/invoices.go:578
msgctxt "input"
msgid "Name"
msgstr "Nombre"
-#: pkg/products.go:171 pkg/invoices.go:522
+#: pkg/products.go:171 pkg/invoices.go:583
msgctxt "input"
msgid "Description"
msgstr "Descripción"
-#: pkg/products.go:176 pkg/invoices.go:526
+#: pkg/products.go:176 pkg/invoices.go:587
msgctxt "input"
msgid "Price"
msgstr "Precio"
-#: pkg/products.go:186 pkg/invoices.go:552
+#: pkg/products.go:186 pkg/invoices.go:613
msgctxt "input"
msgid "Taxes"
msgstr "Impuestos"
-#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:431
-#: pkg/invoices.go:588
+#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:492
+#: pkg/invoices.go:649
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
-#: pkg/products.go:207 pkg/invoices.go:589
+#: pkg/products.go:207 pkg/invoices.go:650
msgid "Price can not be empty."
msgstr "No podéis dejar el precio en blanco."
-#: pkg/products.go:208 pkg/invoices.go:590
+#: pkg/products.go:208 pkg/invoices.go:651
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:598
+#: pkg/products.go:210 pkg/invoices.go:659
msgid "Selected tax is not valid."
msgstr "Habéis escogido un impuesto que no es válido."
-#: pkg/products.go:211 pkg/invoices.go:599
+#: pkg/products.go:211 pkg/invoices.go:660
msgid "You can only select a tax of each class."
msgstr "Solo podéis escoger un impuesto de cada clase."
@@ -563,79 +573,83 @@ 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:258
+#: pkg/invoices.go:270
msgid "Select a customer to bill."
msgstr "Escoged un cliente a facturar."
-#: pkg/invoices.go:351
+#: pkg/invoices.go:363 pkg/invoices.go:392
msgid "Invalid action"
msgstr "Acción inválida."
-#: pkg/invoices.go:372
+#: pkg/invoices.go:386
+msgid "invoices.zip"
+msgstr "facturas.zip"
+
+#: pkg/invoices.go:433
msgctxt "input"
msgid "Customer"
msgstr "Cliente"
-#: pkg/invoices.go:378
+#: pkg/invoices.go:439
msgctxt "input"
msgid "Number"
msgstr "Número"
-#: pkg/invoices.go:384
+#: pkg/invoices.go:445
msgctxt "input"
msgid "Invoice Date"
msgstr "Fecha de factura"
-#: pkg/invoices.go:390
+#: pkg/invoices.go:451
msgctxt "input"
msgid "Notes"
msgstr "Notas"
-#: pkg/invoices.go:396
+#: pkg/invoices.go:457
msgctxt "input"
msgid "Payment Method"
msgstr "Método de pago"
-#: pkg/invoices.go:432
+#: pkg/invoices.go:493
msgid "Invoice date can not be empty."
msgstr "No podéis dejar la fecha de la factura en blanco."
-#: pkg/invoices.go:433
+#: pkg/invoices.go:494
msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida."
-#: pkg/invoices.go:435
+#: pkg/invoices.go:496
msgid "Selected payment method is not valid."
msgstr "Habéis escogido un método de pago que no es válido."
-#: pkg/invoices.go:512
+#: pkg/invoices.go:573
msgctxt "input"
msgid "Id"
msgstr "Identificador"
-#: pkg/invoices.go:535
+#: pkg/invoices.go:596
msgctxt "input"
msgid "Quantity"
msgstr "Cantidad"
-#: pkg/invoices.go:543
+#: pkg/invoices.go:604
msgctxt "input"
msgid "Discount (%)"
msgstr "Descuento (%)"
-#: pkg/invoices.go:592
+#: pkg/invoices.go:653
msgid "Quantity can not be empty."
msgstr "No podéis dejar la cantidad en blanco."
-#: pkg/invoices.go:593
+#: pkg/invoices.go:654
msgid "Quantity must be a number greater than zero."
msgstr "La cantidad tiene que ser un número mayor a cero."
-#: pkg/invoices.go:595
+#: pkg/invoices.go:656
msgid "Discount can not be empty."
msgstr "No podéis dejar el descuento en blanco."
-#: pkg/invoices.go:596
+#: pkg/invoices.go:657
msgid "Discount must be a percentage between 0 and 100."
msgstr "El descuento tiene que ser un porcentaje entre 0 y 100."
diff --git a/web/template/invoices/index.gohtml b/web/template/invoices/index.gohtml
index 4072f65..1312cd8 100644
--- a/web/template/invoices/index.gohtml
+++ b/web/template/invoices/index.gohtml
@@ -8,10 +8,17 @@
{{( pgettext "Home" "title" )}} /
{{( pgettext "Invoices" "title" )}}
-
- {{( pgettext "New invoice" "action" )}}
-
+
+
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InvoicesIndexPage*/ -}}
@@ -33,7 +40,11 @@
{{ with .Invoices }}
{{- range $invoice := . }}
- |
+ {{ $title := .Number | printf (pgettext "Select invoice %v" "action") }}
+ |
{{ .Date|formatDate }} |
{{ .Number }} |
{{ .CustomerName }} |