From c95f1724994836e8aa14f50d3b9640185c60de8d Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Mon, 12 Aug 2024 00:08:18 +0200 Subject: [PATCH] Add attachments to payments --- deploy/attach_to_payment.sql | 30 +++++++ deploy/payment_attachment.sql | 33 ++++++++ deploy/remove_payment.sql | 2 + pkg/payments.go | 50 ++++++++--- pkg/router.go | 1 + po/ca.po | 58 ++++++------- po/es.po | 58 ++++++------- revert/attach_to_payment.sql | 7 ++ revert/payment_attachment.sql | 7 ++ sqitch.plan | 2 + test/attach_to_payment.sql | 80 ++++++++++++++++++ test/payment_attachment.sql | 131 +++++++++++++++++++++++++++++ test/remove_payment.sql | 17 +++- verify/attach_to_payment.sql | 7 ++ verify/payment_attachment.sql | 15 ++++ web/template/payments/edit.gohtml | 2 + web/template/payments/index.gohtml | 9 ++ web/template/payments/new.gohtml | 2 + 18 files changed, 440 insertions(+), 71 deletions(-) create mode 100644 deploy/attach_to_payment.sql create mode 100644 deploy/payment_attachment.sql create mode 100644 revert/attach_to_payment.sql create mode 100644 revert/payment_attachment.sql create mode 100644 test/attach_to_payment.sql create mode 100644 test/payment_attachment.sql create mode 100644 verify/attach_to_payment.sql create mode 100644 verify/payment_attachment.sql diff --git a/deploy/attach_to_payment.sql b/deploy/attach_to_payment.sql new file mode 100644 index 0000000..e20121b --- /dev/null +++ b/deploy/attach_to_payment.sql @@ -0,0 +1,30 @@ +-- Deploy numerus:attach_to_payment to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment +-- requires: payment_attachment + +begin; + +set search_path to numerus, public; + +create or replace function attach_to_payment(payment_slug uuid, original_filename text, mime_type text, content bytea) returns void as +$$ + insert into payment_attachment (payment_id, original_filename, mime_type, content) + select payment_id, original_filename, mime_type, content + from payment + where slug = payment_slug + on conflict (payment_id) do update + set original_filename = excluded.original_filename + , mime_type = excluded.mime_type + , content = excluded.content + ; +$$ + language sql +; + +revoke execute on function attach_to_payment(uuid, text, text, bytea) from public; +grant execute on function attach_to_payment(uuid, text, text, bytea) to invoicer; +grant execute on function attach_to_payment(uuid, text, text, bytea) to admin; + +commit; diff --git a/deploy/payment_attachment.sql b/deploy/payment_attachment.sql new file mode 100644 index 0000000..4d58f0d --- /dev/null +++ b/deploy/payment_attachment.sql @@ -0,0 +1,33 @@ +-- Deploy numerus:payment_attachment to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment + +begin; + +set search_path to numerus, public; + +create table payment_attachment ( + payment_id integer primary key references payment, + original_filename text not null, + mime_type text not null, + content bytea not null +); + +grant select, insert, update, delete on table payment_attachment to invoicer; +grant select, insert, update, delete on table payment_attachment to admin; + +alter table payment_attachment enable row level security; + +create policy company_policy +on payment_attachment +using ( + exists( + select 1 + from payment + where payment.payment_id = payment_attachment.payment_id + ) +); + + +commit; diff --git a/deploy/remove_payment.sql b/deploy/remove_payment.sql index 9844159..31189ae 100644 --- a/deploy/remove_payment.sql +++ b/deploy/remove_payment.sql @@ -3,6 +3,7 @@ -- requires: schema_numerus -- requires: expense_payment -- requires: payment +-- requires: payment_attachment -- requires: update_expense_payment_status begin; @@ -25,6 +26,7 @@ begin perform update_expense_payment_status(null, eid, 0); end if; + delete from payment_attachment where payment_id = pid; delete from payment where payment_id = pid; end $$ diff --git a/pkg/payments.go b/pkg/payments.go index 162e9a4..8c2627a 100644 --- a/pkg/payments.go +++ b/pkg/payments.go @@ -33,14 +33,15 @@ func (page *PaymentIndexPage) MustRender(w http.ResponseWriter, r *http.Request) } type PaymentEntry struct { - ID int - Slug string - PaymentDate time.Time - Description string - Total string - Tags []string - Status string - StatusLabel string + ID int + Slug string + PaymentDate time.Time + Description string + Total string + OriginalFileName string + Tags []string + Status string + StatusLabel string } func mustCollectPaymentEntries(ctx context.Context, conn *Conn, locale *Locale) []*PaymentEntry { @@ -53,9 +54,11 @@ func mustCollectPaymentEntries(ctx context.Context, conn *Conn, locale *Locale) , payment.tags , payment.payment_status , psi18n.name + , coalesce(attachment.original_filename, '') from payment join payment_status_i18n psi18n on payment.payment_status = psi18n.payment_status and psi18n.lang_tag = $1 join currency using (currency_code) + left join payment_attachment as attachment using (payment_id) order by payment_date desc, total desc `, locale.Language) defer rows.Close() @@ -63,7 +66,7 @@ func mustCollectPaymentEntries(ctx context.Context, conn *Conn, locale *Locale) var entries []*PaymentEntry for rows.Next() { entry := &PaymentEntry{} - if err := rows.Scan(&entry.ID, &entry.Slug, &entry.PaymentDate, &entry.Description, &entry.Total, &entry.Tags, &entry.Status, &entry.StatusLabel); err != nil { + if err := rows.Scan(&entry.ID, &entry.Slug, &entry.PaymentDate, &entry.Description, &entry.Total, &entry.Tags, &entry.Status, &entry.StatusLabel, &entry.OriginalFileName); err != nil { panic(err) } entries = append(entries, entry) @@ -105,6 +108,7 @@ type PaymentForm struct { PaymentDate *InputField PaymentAccount *SelectField Amount *InputField + File *FileField Tags *TagsField } @@ -140,6 +144,11 @@ func newPaymentForm(ctx context.Context, conn *Conn, locale *Locale, company *Co template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())), }, }, + File: &FileField{ + Name: "file", + Label: pgettext("input", "File", locale), + MaxSize: 1 << 20, + }, Tags: &TagsField{ Name: "tags", Label: pgettext("input", "Tags", locale), @@ -182,13 +191,16 @@ func (f *PaymentForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug } func (f *PaymentForm) Parse(r *http.Request) error { - if err := r.ParseForm(); err != nil { + if err := r.ParseMultipartForm(f.File.MaxSize); err != nil { return err } f.Description.FillValue(r) f.PaymentDate.FillValue(r) f.PaymentAccount.FillValue(r) f.Amount.FillValue(r) + if err := f.File.FillValue(r); err != nil { + return err + } f.Tags.FillValue(r) return nil } @@ -224,7 +236,10 @@ func handleAddPayment(w http.ResponseWriter, r *http.Request, _ httprouter.Param form.MustRender(w, r) return } - conn.MustExec(r.Context(), "select add_payment($1, $2, $3, $4, $5, $6, $7)", company.Id, nil, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags) + slug := conn.MustGetText(r.Context(), "", "select add_payment($1, $2, $3, $4, $5, $6, $7)", company.Id, nil, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags) + if len(form.File.Content) > 0 { + conn.MustQuery(r.Context(), "select attach_to_payment($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content) + } htmxRedirect(w, r, companyURI(company, "/payments")) } @@ -257,6 +272,9 @@ func handleEditPayment(w http.ResponseWriter, r *http.Request, params httprouter http.NotFound(w, r) return } + if len(form.File.Content) > 0 { + conn.MustQuery(r.Context(), "select attach_to_payment($1, $2, $3, $4)", form.Slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content) + } htmxRedirect(w, r, companyURI(company, "/payments")) } @@ -278,3 +296,13 @@ func handleRemovePayment(w http.ResponseWriter, r *http.Request, params httprout company := mustGetCompany(r) htmxRedirect(w, r, companyURI(company, "/payments")) } + +func servePaymentAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + serveAttachment(w, r, params, ` + select mime_type + , content + from payment + join payment_attachment using (payment_id) + where slug = $1 +`) +} diff --git a/pkg/router.go b/pkg/router.go index 5cf644b..bc88181 100644 --- a/pkg/router.go +++ b/pkg/router.go @@ -63,6 +63,7 @@ func NewRouter(db *Db, demo bool) http.Handler { companyRouter.GET("/payments/:slug", servePaymentForm) companyRouter.PUT("/payments/:slug", handleEditPayment) companyRouter.DELETE("/payments/:slug", handleRemovePayment) + companyRouter.GET("/payments/:slug/download/:filename", servePaymentAttachment) companyRouter.GET("/payment-accounts", servePaymentAccountIndex) companyRouter.POST("/payment-accounts", handleAddPaymentAccount) companyRouter.GET("/payment-accounts/:slug", servePaymentAccountForm) diff --git a/po/ca.po b/po/ca.po index 88132d7..16c4aac 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: 2024-08-11 03:17+0200\n" +"POT-Creation-Date: 2024-08-12 00:06+0200\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -121,7 +121,7 @@ msgstr "Total" #: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 #: web/template/expenses/new.gohtml:57 web/template/expenses/edit.gohtml:59 -#: web/template/payments/edit.gohtml:35 +#: web/template/payments/edit.gohtml:37 #: web/template/payments/accounts/edit.gohtml:38 msgctxt "action" msgid "Update" @@ -132,7 +132,7 @@ msgstr "Actualitza" #: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: web/template/expenses/new.gohtml:60 web/template/expenses/edit.gohtml:62 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 -#: web/template/payments/new.gohtml:33 +#: web/template/payments/new.gohtml:35 #: web/template/payments/accounts/new.gohtml:41 msgctxt "action" msgid "Save" @@ -209,7 +209,7 @@ msgid "Amount" msgstr "Import" #: web/template/invoices/index.gohtml:73 web/template/quotes/index.gohtml:73 -#: web/template/expenses/index.gohtml:75 +#: web/template/expenses/index.gohtml:75 web/template/payments/index.gohtml:30 msgctxt "title" msgid "Download" msgstr "Descàrrega" @@ -217,7 +217,7 @@ msgstr "Descàrrega" #: web/template/invoices/index.gohtml:74 web/template/switch-company.gohtml:23 #: web/template/quotes/index.gohtml:74 web/template/contacts/index.gohtml:51 #: web/template/expenses/index.gohtml:76 web/template/products/index.gohtml:48 -#: web/template/payments/index.gohtml:30 +#: web/template/payments/index.gohtml:31 msgctxt "title" msgid "Actions" msgstr "Accions" @@ -239,7 +239,7 @@ msgstr "Accions per la factura %s" #: web/template/invoices/index.gohtml:139 web/template/invoices/view.gohtml:19 #: web/template/quotes/index.gohtml:137 web/template/quotes/view.gohtml:22 #: web/template/contacts/index.gohtml:82 web/template/expenses/index.gohtml:143 -#: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:62 +#: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:71 msgctxt "action" msgid "Edit" msgstr "Edita" @@ -773,20 +773,20 @@ msgctxt "title" msgid "Description" msgstr "Descripció" -#: web/template/payments/index.gohtml:35 +#: web/template/payments/index.gohtml:36 msgid "Are you sure you wish to delete this payment?" msgstr "Esteu segur de voler esborrar aquest pagament?" -#: web/template/payments/index.gohtml:54 +#: web/template/payments/index.gohtml:63 msgid "Actions for payment %s" msgstr "Accions pel pagament %s" -#: web/template/payments/index.gohtml:73 +#: web/template/payments/index.gohtml:82 msgctxt "action" msgid "Remove" msgstr "Esborra" -#: web/template/payments/index.gohtml:83 +#: web/template/payments/index.gohtml:92 msgid "No payments added yet." msgstr "No hi ha cap pagament." @@ -876,7 +876,7 @@ msgid "Name" msgstr "Nom" #: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708 -#: pkg/payments.go:145 pkg/expenses.go:343 pkg/expenses.go:510 +#: pkg/payments.go:154 pkg/expenses.go:343 pkg/expenses.go:510 #: pkg/invoices.go:177 pkg/invoices.go:877 pkg/invoices.go:1462 #: pkg/contacts.go:154 pkg/contacts.go:362 msgctxt "input" @@ -911,7 +911,7 @@ msgstr "Qualsevol" msgid "Invoices must have at least one of the specified labels." msgstr "Les factures han de tenir com a mínim una de les etiquetes." -#: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:117 +#: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:121 #: pkg/invoices.go:1161 msgctxt "input" msgid "Description" @@ -1205,8 +1205,8 @@ msgstr "pressuposts.zip" msgid "quotations.ods" msgstr "pressuposts.ods" -#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:719 -#: pkg/expenses.go:749 pkg/invoices.go:684 pkg/invoices.go:1437 +#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:704 +#: pkg/expenses.go:734 pkg/invoices.go:684 pkg/invoices.go:1437 #: pkg/invoices.go:1445 msgid "Invalid action" msgstr "Acció invàlida." @@ -1326,42 +1326,47 @@ 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/payments.go:123 pkg/expenses.go:305 pkg/invoices.go:866 +#: pkg/payments.go:127 pkg/expenses.go:305 pkg/invoices.go:866 msgctxt "input" msgid "Invoice Date" msgstr "Data de factura" -#: pkg/payments.go:129 +#: pkg/payments.go:133 msgctxt "input" msgid "Account" msgstr "Compte" -#: pkg/payments.go:135 pkg/expenses.go:320 +#: pkg/payments.go:139 pkg/expenses.go:320 msgctxt "input" msgid "Amount" msgstr "Import" -#: pkg/payments.go:152 +#: pkg/payments.go:149 pkg/expenses.go:331 pkg/invoices.go:888 +msgctxt "input" +msgid "File" +msgstr "Fitxer" + +#: pkg/payments.go:161 msgid "Select an account." msgstr "Escolliu un compte." -#: pkg/payments.go:198 +#: pkg/payments.go:210 msgid "Description can not be empty." msgstr "No podeu deixar la descripció en blanc." -#: pkg/payments.go:199 +#: pkg/payments.go:211 msgid "Selected payment account is not valid." msgstr "Heu seleccionat un compte de pagament que no és vàlid." -#: pkg/payments.go:200 +#: pkg/payments.go:212 msgid "Payment date must be a valid date." msgstr "La data de pagament ha de ser vàlida." -#: pkg/payments.go:201 pkg/expenses.go:381 +#: pkg/payments.go:213 pkg/expenses.go:381 msgid "Amount can not be empty." msgstr "No podeu deixar l’import en blanc." -#: pkg/payments.go:202 pkg/expenses.go:382 +#: pkg/payments.go:214 pkg/expenses.go:382 msgid "Amount must be a number greater than zero." msgstr "L’import ha de ser un número major a zero." @@ -1470,11 +1475,6 @@ msgctxt "input" msgid "Invoice number" msgstr "Número de factura" -#: pkg/expenses.go:331 pkg/invoices.go:888 -msgctxt "input" -msgid "File" -msgstr "Fitxer" - #: pkg/expenses.go:337 pkg/expenses.go:514 msgctxt "input" msgid "Expense Status" @@ -1501,7 +1501,7 @@ msgctxt "input" msgid "Invoice Number" msgstr "Número de factura" -#: pkg/expenses.go:747 +#: pkg/expenses.go:732 msgid "expenses.ods" msgstr "despeses.ods" diff --git a/po/es.po b/po/es.po index 6a43ec1..ca10a6d 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: 2024-08-11 03:17+0200\n" +"POT-Creation-Date: 2024-08-12 00:06+0200\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -121,7 +121,7 @@ msgstr "Total" #: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 #: web/template/expenses/new.gohtml:57 web/template/expenses/edit.gohtml:59 -#: web/template/payments/edit.gohtml:35 +#: web/template/payments/edit.gohtml:37 #: web/template/payments/accounts/edit.gohtml:38 msgctxt "action" msgid "Update" @@ -132,7 +132,7 @@ msgstr "Actualizar" #: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: web/template/expenses/new.gohtml:60 web/template/expenses/edit.gohtml:62 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 -#: web/template/payments/new.gohtml:33 +#: web/template/payments/new.gohtml:35 #: web/template/payments/accounts/new.gohtml:41 msgctxt "action" msgid "Save" @@ -209,7 +209,7 @@ msgid "Amount" msgstr "Importe" #: web/template/invoices/index.gohtml:73 web/template/quotes/index.gohtml:73 -#: web/template/expenses/index.gohtml:75 +#: web/template/expenses/index.gohtml:75 web/template/payments/index.gohtml:30 msgctxt "title" msgid "Download" msgstr "Descargar" @@ -217,7 +217,7 @@ msgstr "Descargar" #: web/template/invoices/index.gohtml:74 web/template/switch-company.gohtml:23 #: web/template/quotes/index.gohtml:74 web/template/contacts/index.gohtml:51 #: web/template/expenses/index.gohtml:76 web/template/products/index.gohtml:48 -#: web/template/payments/index.gohtml:30 +#: web/template/payments/index.gohtml:31 msgctxt "title" msgid "Actions" msgstr "Acciones" @@ -239,7 +239,7 @@ msgstr "Acciones para la factura %s" #: web/template/invoices/index.gohtml:139 web/template/invoices/view.gohtml:19 #: web/template/quotes/index.gohtml:137 web/template/quotes/view.gohtml:22 #: web/template/contacts/index.gohtml:82 web/template/expenses/index.gohtml:143 -#: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:62 +#: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:71 msgctxt "action" msgid "Edit" msgstr "Editar" @@ -773,20 +773,20 @@ msgctxt "title" msgid "Description" msgstr "Descripción" -#: web/template/payments/index.gohtml:35 +#: web/template/payments/index.gohtml:36 msgid "Are you sure you wish to delete this payment?" msgstr "¿Estáis seguro de querer borrar este pago?" -#: web/template/payments/index.gohtml:54 +#: web/template/payments/index.gohtml:63 msgid "Actions for payment %s" msgstr "Acciones para el pago %s" -#: web/template/payments/index.gohtml:73 +#: web/template/payments/index.gohtml:82 msgctxt "action" msgid "Remove" msgstr "Borrar" -#: web/template/payments/index.gohtml:83 +#: web/template/payments/index.gohtml:92 msgid "No payments added yet." msgstr "No hay pagos." @@ -876,7 +876,7 @@ msgid "Name" msgstr "Nombre" #: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708 -#: pkg/payments.go:145 pkg/expenses.go:343 pkg/expenses.go:510 +#: pkg/payments.go:154 pkg/expenses.go:343 pkg/expenses.go:510 #: pkg/invoices.go:177 pkg/invoices.go:877 pkg/invoices.go:1462 #: pkg/contacts.go:154 pkg/contacts.go:362 msgctxt "input" @@ -911,7 +911,7 @@ msgstr "Cualquiera" msgid "Invoices must have at least one of the specified labels." msgstr "Las facturas deben tener como mínimo una de las etiquetas." -#: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:117 +#: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:121 #: pkg/invoices.go:1161 msgctxt "input" msgid "Description" @@ -1205,8 +1205,8 @@ msgstr "presupuestos.zip" msgid "quotations.ods" msgstr "presupuestos.ods" -#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:719 -#: pkg/expenses.go:749 pkg/invoices.go:684 pkg/invoices.go:1437 +#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:704 +#: pkg/expenses.go:734 pkg/invoices.go:684 pkg/invoices.go:1437 #: pkg/invoices.go:1445 msgid "Invalid action" msgstr "Acción inválida." @@ -1326,42 +1326,47 @@ 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/payments.go:123 pkg/expenses.go:305 pkg/invoices.go:866 +#: pkg/payments.go:127 pkg/expenses.go:305 pkg/invoices.go:866 msgctxt "input" msgid "Invoice Date" msgstr "Fecha de factura" -#: pkg/payments.go:129 +#: pkg/payments.go:133 msgctxt "input" msgid "Account" msgstr "Cuenta" -#: pkg/payments.go:135 pkg/expenses.go:320 +#: pkg/payments.go:139 pkg/expenses.go:320 msgctxt "input" msgid "Amount" msgstr "Importe" -#: pkg/payments.go:152 +#: pkg/payments.go:149 pkg/expenses.go:331 pkg/invoices.go:888 +msgctxt "input" +msgid "File" +msgstr "Archivo" + +#: pkg/payments.go:161 msgid "Select an account." msgstr "Escoged una cuenta." -#: pkg/payments.go:198 +#: pkg/payments.go:210 msgid "Description can not be empty." msgstr "No podéis dejar la descripción en blanco." -#: pkg/payments.go:199 +#: pkg/payments.go:211 msgid "Selected payment account is not valid." msgstr "Habéis escogido una cuenta de pago que no es válida." -#: pkg/payments.go:200 +#: pkg/payments.go:212 msgid "Payment date must be a valid date." msgstr "La fecha de pago debe ser válida." -#: pkg/payments.go:201 pkg/expenses.go:381 +#: pkg/payments.go:213 pkg/expenses.go:381 msgid "Amount can not be empty." msgstr "No podéis dejar el importe en blanco." -#: pkg/payments.go:202 pkg/expenses.go:382 +#: pkg/payments.go:214 pkg/expenses.go:382 msgid "Amount must be a number greater than zero." msgstr "El importe tiene que ser un número mayor a cero." @@ -1470,11 +1475,6 @@ msgctxt "input" msgid "Invoice number" msgstr "Número de factura" -#: pkg/expenses.go:331 pkg/invoices.go:888 -msgctxt "input" -msgid "File" -msgstr "Archivo" - #: pkg/expenses.go:337 pkg/expenses.go:514 msgctxt "input" msgid "Expense Status" @@ -1501,7 +1501,7 @@ msgctxt "input" msgid "Invoice Number" msgstr "Número de factura" -#: pkg/expenses.go:747 +#: pkg/expenses.go:732 msgid "expenses.ods" msgstr "gastos.ods" diff --git a/revert/attach_to_payment.sql b/revert/attach_to_payment.sql new file mode 100644 index 0000000..1254a4e --- /dev/null +++ b/revert/attach_to_payment.sql @@ -0,0 +1,7 @@ +-- Revert numerus:attach_to_payment from pg + +begin; + +drop function if exists numerus.attach_to_payment(uuid, text, text, bytea); + +commit; diff --git a/revert/payment_attachment.sql b/revert/payment_attachment.sql new file mode 100644 index 0000000..3861d6a --- /dev/null +++ b/revert/payment_attachment.sql @@ -0,0 +1,7 @@ +-- Revert numerus:payment_attachment from pg + +begin; + +drop table if exists numerus.payment_attachment; + +commit; diff --git a/sqitch.plan b/sqitch.plan index 3cdc854..3e06971 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -152,4 +152,6 @@ available_expense_status [available_expense_status@v2] 2024-08-04T05:24:08Z jord update_expense_payment_status [roles schema_numerus expense payment expense_payment available_expense_status available_payment_status] 2024-08-04T06:36:00Z jordi fita mas # Add function to update payment and expense status add_payment [roles schema_numerus payment expense_payment company currency parse_price tag_name update_expense_payment_status] 2024-08-04T03:16:55Z jordi fita mas # Add function to insert new payments edit_payment [roles schema_numerus payment expense_payment currency parse_price tag_name update_expense_payment_status] 2024-08-04T03:31:45Z jordi fita mas # Add function to update payments +payment_attachment [roles schema_numerus payment] 2024-08-11T21:01:50Z jordi fita mas # Add relation of payment attachments +attach_to_payment [roles schema_numerus payment payment_attachment] 2024-08-11T21:33:36Z jordi fita mas # Add function to attach files to payments remove_payment [roles schema_numerus expense_payment payment payment_attachment update_expense_payment_status] 2024-08-11T00:30:58Z jordi fita mas # Add function to remove payments diff --git a/test/attach_to_payment.sql b/test/attach_to_payment.sql new file mode 100644 index 0000000..8179097 --- /dev/null +++ b/test/attach_to_payment.sql @@ -0,0 +1,80 @@ +-- Test attach_to_payment +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(12); + +set search_path to numerus, public; + +select has_function('numerus', 'attach_to_payment', array['uuid', 'text', 'text', 'bytea']); +select function_lang_is('numerus', 'attach_to_payment', array['uuid', 'text', 'text', 'bytea'], 'sql'); +select function_returns('numerus', 'attach_to_payment', array['uuid', 'text', 'text', 'bytea'], 'void'); +select isnt_definer('numerus', 'attach_to_payment', array['uuid', 'text', 'text', 'bytea']); +select volatility_is('numerus', 'attach_to_payment', array['uuid', 'text', 'text', 'bytea'], 'volatile'); +select function_privs_are('numerus', 'attach_to_payment', array ['uuid', 'text', 'text', 'bytea'], 'guest', array []::text[]); +select function_privs_are('numerus', 'attach_to_payment', array ['uuid', 'text', 'text', 'bytea'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'attach_to_payment', array ['uuid', 'text', 'text', 'bytea'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'attach_to_payment', array ['uuid', 'text', 'text', 'bytea'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate payment_attachment cascade; +truncate payment cascade; +truncate payment_account cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + + +set constraints "company_default_payment_method_id_fkey" deferred; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into payment_account (payment_account_id, company_id, payment_account_type, name) +values (11, 1, 'cash', 'Cash 1') + , (12, 1, 'cash', 'Cash 2') + , (13, 1, 'other', 'Other') +; + +insert into payment (payment_id, company_id, slug, description, payment_date, payment_account_id, amount, currency_code, payment_status) +values (16, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Payment 1', '2023-05-04', 12, 111, 'EUR', 'complete') + , (17, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Payment 2', '2023-05-05', 13, 100, 'EUR', 'partial') +; + +insert into payment_attachment (payment_id, original_filename, mime_type, content) +values (17, 'something.txt', 'text/plain', convert_to('Once upon a time…', 'UTF-8')) +; + +select lives_ok( + $$ select attach_to_payment('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'payment.txt', 'text/plain', convert_to('To pay 42 €', 'UTF-8')) $$, + 'Should be able to attach a document to the first payment' +); + +select lives_ok( + $$ select attach_to_payment('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'payment.html', 'text/html', convert_to('

To pay 42 €

', 'UTF-8')) $$, + 'Should be able to replate the second payment’s attachment with a new document' +); + +select bag_eq( + $$ select payment_id, original_filename, mime_type, convert_from(content, 'UTF-8') from payment_attachment $$, + $$ values (16, 'payment.txt', 'text/plain', 'To pay 42 €') + , (17, 'payment.html', 'text/html', '

To pay 42 €

') + $$, + 'Should have attached all documents' +); + +select * +from finish(); + +rollback; diff --git a/test/payment_attachment.sql b/test/payment_attachment.sql new file mode 100644 index 0000000..6b7ca9d --- /dev/null +++ b/test/payment_attachment.sql @@ -0,0 +1,131 @@ +-- Test payment_attachment +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(29); + +set search_path to numerus, auth, public; + +select has_table('payment_attachment'); +select has_pk('payment_attachment'); +select table_privs_are('payment_attachment', 'guest', array []::text[]); +select table_privs_are('payment_attachment', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment_attachment', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment_attachment', 'authenticator', array []::text[]); + +select has_column('payment_attachment', 'payment_id'); +select col_is_pk('payment_attachment', 'payment_id'); +select col_is_fk('payment_attachment', 'payment_id'); +select fk_ok('payment_attachment', 'payment_id', 'payment', 'payment_id'); +select col_type_is('payment_attachment', 'payment_id', 'integer'); +select col_not_null('payment_attachment', 'payment_id'); +select col_hasnt_default('payment_attachment', 'payment_id'); + +select has_column('payment_attachment', 'original_filename'); +select col_type_is('payment_attachment', 'original_filename', 'text'); +select col_not_null('payment_attachment', 'original_filename'); +select col_hasnt_default('payment_attachment', 'original_filename'); + +select has_column('payment_attachment', 'mime_type'); +select col_type_is('payment_attachment', 'mime_type', 'text'); +select col_not_null('payment_attachment', 'mime_type'); +select col_hasnt_default('payment_attachment', 'mime_type'); + +select has_column('payment_attachment', 'content'); +select col_type_is('payment_attachment', 'content', 'bytea'); +select col_not_null('payment_attachment', 'content'); +select col_hasnt_default('payment_attachment', 'content'); + + +set client_min_messages to warning; +truncate payment_attachment cascade; +truncate payment cascade; +truncate company_user cascade; +truncate payment_method cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +set constraints "company_default_payment_method_id_fkey" deferred; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222) + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (444, 4, 'cash', 'cash') + , (222, 2, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into company_user (company_id, user_id) +values (2, 1) + , (4, 5) +; + +insert into payment_account(payment_account_id, company_id, payment_account_type, name) +values (8, 2, 'other', 'Other 2') + , (9, 4, 'other', 'Other 4') +; + +insert into payment (payment_id, company_id, description, payment_account_id, payment_date, amount, currency_code) +values (13, 2, 'Payment 2', 8, '2011-01-11', 111, 'EUR') + , (14, 4, 'Payment 4', 9, '2022-02-22', 222, 'EUR') +; + +insert into payment_attachment (payment_id, original_filename, mime_type, content) +values (13, 'payment.txt', 'text/plain', convert_to('Payment 42', 'UTF8')) + , (14, 'payment.html', 'text/html', convert_to('Payment 42', 'UTF8')) +; + +prepare payment_attachment_data as +select payment_id, original_filename +from payment_attachment +order by payment_id, original_filename; + +set role invoicer; +select is_empty('payment_attachment_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'payment_attachment_data', + $$ values (13, 'payment.txt') + $$, + 'Should only list payment attachmements of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'payment_attachment_data', + $$ values (14, 'payment.html') + $$, + 'Should only list payment attachmements of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'payment_attachment_data', + '42501', 'permission denied for table payment_attachment', + 'Should not allow select to guest users' +); +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/remove_payment.sql b/test/remove_payment.sql index 86a17f3..f0982e8 100644 --- a/test/remove_payment.sql +++ b/test/remove_payment.sql @@ -5,7 +5,7 @@ reset client_min_messages; begin; -select plan(15); +select plan(16); set search_path to numerus, public; @@ -22,7 +22,8 @@ select function_privs_are('numerus', 'remove_payment', array ['uuid'], 'authenti set client_min_messages to warning; -truncate expense_payment cascade; +truncate expense_payment; +truncate payment_attachment; truncate payment cascade; truncate expense cascade; truncate contact cascade; @@ -74,6 +75,12 @@ values (13, 16) , (15, 19) ; +insert into payment_attachment (payment_id, original_fileName, mime_type, content) +values (16, 'payment.txt', 'text/plain', convert_to('Pay 42', 'UTF-8')) + , (18, 'empty.html', 'text/html', convert_to('empty', 'UTF-8')) + , (19, 'payment.html', 'text/html', convert_to(' PAY 42', 'UTF-8')) +; + select lives_ok( $$ select remove_payment('7ac3ae0e-b0c1-4206-a19b-0be20835edd4') $$, 'Should be able to remove a complete payment' @@ -103,6 +110,12 @@ select bag_eq( 'Should have deleted all related expenses’ payments' ); +select bag_eq( + $$ select payment_id, original_filename from payment_attachment $$, + $$ values (18, 'empty.html') $$, + 'Should have deleted all related attachments' +); + select bag_eq( $$ select expense_id, expense_status from expense $$, $$ values (13, 'pending') diff --git a/verify/attach_to_payment.sql b/verify/attach_to_payment.sql new file mode 100644 index 0000000..9a4adee --- /dev/null +++ b/verify/attach_to_payment.sql @@ -0,0 +1,7 @@ +-- Verify numerus:attach_to_payment on pg + +begin; + +select has_function_privilege('numerus.attach_to_payment(uuid, text, text, bytea)', 'execute'); + +rollback; diff --git a/verify/payment_attachment.sql b/verify/payment_attachment.sql new file mode 100644 index 0000000..11b9869 --- /dev/null +++ b/verify/payment_attachment.sql @@ -0,0 +1,15 @@ +-- Verify numerus:payment_attachment on pg + +begin; + +select payment_id + , original_filename + , mime_type + , content +from numerus.payment_attachment +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.payment_attachment'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.payment_attachment'::regclass; + +rollback; diff --git a/web/template/payments/edit.gohtml b/web/template/payments/edit.gohtml index c3ea09d..9e72aef 100644 --- a/web/template/payments/edit.gohtml +++ b/web/template/payments/edit.gohtml @@ -19,6 +19,7 @@

{{ template "title" . }}

@@ -30,6 +31,7 @@ {{ template "input-field" .PaymentDate }} {{ template "input-field" .Amount }} {{ template "tags-field" .Tags }} + {{ template "file-field" .File }}