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" )}} -

+ +
+ {{ csrfToken }} +

+ + {{( 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 }}