Add attachments to payments

This commit is contained in:
jordi fita mas 2024-08-12 00:08:18 +02:00
parent 58cef8c00b
commit c95f172499
18 changed files with 440 additions and 71 deletions

View File

@ -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;

View File

@ -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;

View File

@ -3,6 +3,7 @@
-- requires: schema_numerus -- requires: schema_numerus
-- requires: expense_payment -- requires: expense_payment
-- requires: payment -- requires: payment
-- requires: payment_attachment
-- requires: update_expense_payment_status -- requires: update_expense_payment_status
begin; begin;
@ -25,6 +26,7 @@ begin
perform update_expense_payment_status(null, eid, 0); perform update_expense_payment_status(null, eid, 0);
end if; end if;
delete from payment_attachment where payment_id = pid;
delete from payment where payment_id = pid; delete from payment where payment_id = pid;
end end
$$ $$

View File

@ -33,14 +33,15 @@ func (page *PaymentIndexPage) MustRender(w http.ResponseWriter, r *http.Request)
} }
type PaymentEntry struct { type PaymentEntry struct {
ID int ID int
Slug string Slug string
PaymentDate time.Time PaymentDate time.Time
Description string Description string
Total string Total string
Tags []string OriginalFileName string
Status string Tags []string
StatusLabel string Status string
StatusLabel string
} }
func mustCollectPaymentEntries(ctx context.Context, conn *Conn, locale *Locale) []*PaymentEntry { 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.tags
, payment.payment_status , payment.payment_status
, psi18n.name , psi18n.name
, coalesce(attachment.original_filename, '')
from payment from payment
join payment_status_i18n psi18n on payment.payment_status = psi18n.payment_status and psi18n.lang_tag = $1 join payment_status_i18n psi18n on payment.payment_status = psi18n.payment_status and psi18n.lang_tag = $1
join currency using (currency_code) join currency using (currency_code)
left join payment_attachment as attachment using (payment_id)
order by payment_date desc, total desc order by payment_date desc, total desc
`, locale.Language) `, locale.Language)
defer rows.Close() defer rows.Close()
@ -63,7 +66,7 @@ func mustCollectPaymentEntries(ctx context.Context, conn *Conn, locale *Locale)
var entries []*PaymentEntry var entries []*PaymentEntry
for rows.Next() { for rows.Next() {
entry := &PaymentEntry{} 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) panic(err)
} }
entries = append(entries, entry) entries = append(entries, entry)
@ -105,6 +108,7 @@ type PaymentForm struct {
PaymentDate *InputField PaymentDate *InputField
PaymentAccount *SelectField PaymentAccount *SelectField
Amount *InputField Amount *InputField
File *FileField
Tags *TagsField 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())), template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
}, },
}, },
File: &FileField{
Name: "file",
Label: pgettext("input", "File", locale),
MaxSize: 1 << 20,
},
Tags: &TagsField{ Tags: &TagsField{
Name: "tags", Name: "tags",
Label: pgettext("input", "Tags", locale), 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 { 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 return err
} }
f.Description.FillValue(r) f.Description.FillValue(r)
f.PaymentDate.FillValue(r) f.PaymentDate.FillValue(r)
f.PaymentAccount.FillValue(r) f.PaymentAccount.FillValue(r)
f.Amount.FillValue(r) f.Amount.FillValue(r)
if err := f.File.FillValue(r); err != nil {
return err
}
f.Tags.FillValue(r) f.Tags.FillValue(r)
return nil return nil
} }
@ -224,7 +236,10 @@ func handleAddPayment(w http.ResponseWriter, r *http.Request, _ httprouter.Param
form.MustRender(w, r) form.MustRender(w, r)
return 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")) htmxRedirect(w, r, companyURI(company, "/payments"))
} }
@ -257,6 +272,9 @@ func handleEditPayment(w http.ResponseWriter, r *http.Request, params httprouter
http.NotFound(w, r) http.NotFound(w, r)
return 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")) htmxRedirect(w, r, companyURI(company, "/payments"))
} }
@ -278,3 +296,13 @@ func handleRemovePayment(w http.ResponseWriter, r *http.Request, params httprout
company := mustGetCompany(r) company := mustGetCompany(r)
htmxRedirect(w, r, companyURI(company, "/payments")) 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
`)
}

View File

@ -63,6 +63,7 @@ func NewRouter(db *Db, demo bool) http.Handler {
companyRouter.GET("/payments/:slug", servePaymentForm) companyRouter.GET("/payments/:slug", servePaymentForm)
companyRouter.PUT("/payments/:slug", handleEditPayment) companyRouter.PUT("/payments/:slug", handleEditPayment)
companyRouter.DELETE("/payments/:slug", handleRemovePayment) companyRouter.DELETE("/payments/:slug", handleRemovePayment)
companyRouter.GET("/payments/:slug/download/:filename", servePaymentAttachment)
companyRouter.GET("/payment-accounts", servePaymentAccountIndex) companyRouter.GET("/payment-accounts", servePaymentAccountIndex)
companyRouter.POST("/payment-accounts", handleAddPaymentAccount) companyRouter.POST("/payment-accounts", handleAddPaymentAccount)
companyRouter.GET("/payment-accounts/:slug", servePaymentAccountForm) companyRouter.GET("/payment-accounts/:slug", servePaymentAccountForm)

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\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" "PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -121,7 +121,7 @@ msgstr "Total"
#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: 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/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93
#: web/template/expenses/new.gohtml:57 web/template/expenses/edit.gohtml:59 #: 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 #: web/template/payments/accounts/edit.gohtml:38
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
@ -132,7 +132,7 @@ msgstr "Actualitza"
#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: 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/expenses/new.gohtml:60 web/template/expenses/edit.gohtml:62
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 #: 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 #: web/template/payments/accounts/new.gohtml:41
msgctxt "action" msgctxt "action"
msgid "Save" msgid "Save"
@ -209,7 +209,7 @@ msgid "Amount"
msgstr "Import" msgstr "Import"
#: web/template/invoices/index.gohtml:73 web/template/quotes/index.gohtml:73 #: 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" msgctxt "title"
msgid "Download" msgid "Download"
msgstr "Descàrrega" msgstr "Descàrrega"
@ -217,7 +217,7 @@ msgstr "Descàrrega"
#: web/template/invoices/index.gohtml:74 web/template/switch-company.gohtml:23 #: 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/quotes/index.gohtml:74 web/template/contacts/index.gohtml:51
#: web/template/expenses/index.gohtml:76 web/template/products/index.gohtml:48 #: 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" msgctxt "title"
msgid "Actions" msgid "Actions"
msgstr "Accions" 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/invoices/index.gohtml:139 web/template/invoices/view.gohtml:19
#: web/template/quotes/index.gohtml:137 web/template/quotes/view.gohtml:22 #: 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/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" msgctxt "action"
msgid "Edit" msgid "Edit"
msgstr "Edita" msgstr "Edita"
@ -773,20 +773,20 @@ msgctxt "title"
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: web/template/payments/index.gohtml:35 #: web/template/payments/index.gohtml:36
msgid "Are you sure you wish to delete this payment?" msgid "Are you sure you wish to delete this payment?"
msgstr "Esteu segur de voler esborrar aquest pagament?" 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" msgid "Actions for payment %s"
msgstr "Accions pel pagament %s" msgstr "Accions pel pagament %s"
#: web/template/payments/index.gohtml:73 #: web/template/payments/index.gohtml:82
msgctxt "action" msgctxt "action"
msgid "Remove" msgid "Remove"
msgstr "Esborra" msgstr "Esborra"
#: web/template/payments/index.gohtml:83 #: web/template/payments/index.gohtml:92
msgid "No payments added yet." msgid "No payments added yet."
msgstr "No hi ha cap pagament." msgstr "No hi ha cap pagament."
@ -876,7 +876,7 @@ msgid "Name"
msgstr "Nom" msgstr "Nom"
#: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708 #: 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/invoices.go:177 pkg/invoices.go:877 pkg/invoices.go:1462
#: pkg/contacts.go:154 pkg/contacts.go:362 #: pkg/contacts.go:154 pkg/contacts.go:362
msgctxt "input" msgctxt "input"
@ -911,7 +911,7 @@ msgstr "Qualsevol"
msgid "Invoices must have at least one of the specified labels." 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." 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 #: pkg/invoices.go:1161
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
@ -1205,8 +1205,8 @@ msgstr "pressuposts.zip"
msgid "quotations.ods" msgid "quotations.ods"
msgstr "pressuposts.ods" msgstr "pressuposts.ods"
#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:719 #: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:704
#: pkg/expenses.go:749 pkg/invoices.go:684 pkg/invoices.go:1437 #: pkg/expenses.go:734 pkg/invoices.go:684 pkg/invoices.go:1437
#: pkg/invoices.go:1445 #: pkg/invoices.go:1445
msgid "Invalid action" msgid "Invalid action"
msgstr "Acció invàlida." msgstr "Acció invàlida."
@ -1326,42 +1326,47 @@ msgstr "La confirmació no és igual a la contrasenya."
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid." 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" msgctxt "input"
msgid "Invoice Date" msgid "Invoice Date"
msgstr "Data de factura" msgstr "Data de factura"
#: pkg/payments.go:129 #: pkg/payments.go:133
msgctxt "input" msgctxt "input"
msgid "Account" msgid "Account"
msgstr "Compte" msgstr "Compte"
#: pkg/payments.go:135 pkg/expenses.go:320 #: pkg/payments.go:139 pkg/expenses.go:320
msgctxt "input" msgctxt "input"
msgid "Amount" msgid "Amount"
msgstr "Import" 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." msgid "Select an account."
msgstr "Escolliu un compte." msgstr "Escolliu un compte."
#: pkg/payments.go:198 #: pkg/payments.go:210
msgid "Description can not be empty." msgid "Description can not be empty."
msgstr "No podeu deixar la descripció en blanc." msgstr "No podeu deixar la descripció en blanc."
#: pkg/payments.go:199 #: pkg/payments.go:211
msgid "Selected payment account is not valid." msgid "Selected payment account is not valid."
msgstr "Heu seleccionat un compte de pagament que no és vàlid." 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." msgid "Payment date must be a valid date."
msgstr "La data de pagament ha de ser vàlida." 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." msgid "Amount can not be empty."
msgstr "No podeu deixar limport en blanc." msgstr "No podeu deixar limport 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." msgid "Amount must be a number greater than zero."
msgstr "Limport ha de ser un número major a zero." msgstr "Limport ha de ser un número major a zero."
@ -1470,11 +1475,6 @@ msgctxt "input"
msgid "Invoice number" msgid "Invoice number"
msgstr "Número de factura" 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 #: pkg/expenses.go:337 pkg/expenses.go:514
msgctxt "input" msgctxt "input"
msgid "Expense Status" msgid "Expense Status"
@ -1501,7 +1501,7 @@ msgctxt "input"
msgid "Invoice Number" msgid "Invoice Number"
msgstr "Número de factura" msgstr "Número de factura"
#: pkg/expenses.go:747 #: pkg/expenses.go:732
msgid "expenses.ods" msgid "expenses.ods"
msgstr "despeses.ods" msgstr "despeses.ods"

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\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" "PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -121,7 +121,7 @@ msgstr "Total"
#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: 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/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93
#: web/template/expenses/new.gohtml:57 web/template/expenses/edit.gohtml:59 #: 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 #: web/template/payments/accounts/edit.gohtml:38
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
@ -132,7 +132,7 @@ msgstr "Actualizar"
#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: 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/expenses/new.gohtml:60 web/template/expenses/edit.gohtml:62
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 #: 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 #: web/template/payments/accounts/new.gohtml:41
msgctxt "action" msgctxt "action"
msgid "Save" msgid "Save"
@ -209,7 +209,7 @@ msgid "Amount"
msgstr "Importe" msgstr "Importe"
#: web/template/invoices/index.gohtml:73 web/template/quotes/index.gohtml:73 #: 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" msgctxt "title"
msgid "Download" msgid "Download"
msgstr "Descargar" msgstr "Descargar"
@ -217,7 +217,7 @@ msgstr "Descargar"
#: web/template/invoices/index.gohtml:74 web/template/switch-company.gohtml:23 #: 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/quotes/index.gohtml:74 web/template/contacts/index.gohtml:51
#: web/template/expenses/index.gohtml:76 web/template/products/index.gohtml:48 #: 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" msgctxt "title"
msgid "Actions" msgid "Actions"
msgstr "Acciones" 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/invoices/index.gohtml:139 web/template/invoices/view.gohtml:19
#: web/template/quotes/index.gohtml:137 web/template/quotes/view.gohtml:22 #: 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/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" msgctxt "action"
msgid "Edit" msgid "Edit"
msgstr "Editar" msgstr "Editar"
@ -773,20 +773,20 @@ msgctxt "title"
msgid "Description" msgid "Description"
msgstr "Descripción" 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?" msgid "Are you sure you wish to delete this payment?"
msgstr "¿Estáis seguro de querer borrar este pago?" 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" msgid "Actions for payment %s"
msgstr "Acciones para el pago %s" msgstr "Acciones para el pago %s"
#: web/template/payments/index.gohtml:73 #: web/template/payments/index.gohtml:82
msgctxt "action" msgctxt "action"
msgid "Remove" msgid "Remove"
msgstr "Borrar" msgstr "Borrar"
#: web/template/payments/index.gohtml:83 #: web/template/payments/index.gohtml:92
msgid "No payments added yet." msgid "No payments added yet."
msgstr "No hay pagos." msgstr "No hay pagos."
@ -876,7 +876,7 @@ msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708 #: 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/invoices.go:177 pkg/invoices.go:877 pkg/invoices.go:1462
#: pkg/contacts.go:154 pkg/contacts.go:362 #: pkg/contacts.go:154 pkg/contacts.go:362
msgctxt "input" msgctxt "input"
@ -911,7 +911,7 @@ msgstr "Cualquiera"
msgid "Invoices must have at least one of the specified labels." msgid "Invoices must have at least one of the specified labels."
msgstr "Las facturas deben tener como mínimo una de las etiquetas." 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 #: pkg/invoices.go:1161
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
@ -1205,8 +1205,8 @@ msgstr "presupuestos.zip"
msgid "quotations.ods" msgid "quotations.ods"
msgstr "presupuestos.ods" msgstr "presupuestos.ods"
#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:719 #: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:704
#: pkg/expenses.go:749 pkg/invoices.go:684 pkg/invoices.go:1437 #: pkg/expenses.go:734 pkg/invoices.go:684 pkg/invoices.go:1437
#: pkg/invoices.go:1445 #: pkg/invoices.go:1445
msgid "Invalid action" msgid "Invalid action"
msgstr "Acción inválida." 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." msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido." 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" msgctxt "input"
msgid "Invoice Date" msgid "Invoice Date"
msgstr "Fecha de factura" msgstr "Fecha de factura"
#: pkg/payments.go:129 #: pkg/payments.go:133
msgctxt "input" msgctxt "input"
msgid "Account" msgid "Account"
msgstr "Cuenta" msgstr "Cuenta"
#: pkg/payments.go:135 pkg/expenses.go:320 #: pkg/payments.go:139 pkg/expenses.go:320
msgctxt "input" msgctxt "input"
msgid "Amount" msgid "Amount"
msgstr "Importe" 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." msgid "Select an account."
msgstr "Escoged una cuenta." msgstr "Escoged una cuenta."
#: pkg/payments.go:198 #: pkg/payments.go:210
msgid "Description can not be empty." msgid "Description can not be empty."
msgstr "No podéis dejar la descripción en blanco." 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." msgid "Selected payment account is not valid."
msgstr "Habéis escogido una cuenta de pago que no es válida." 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." msgid "Payment date must be a valid date."
msgstr "La fecha de pago debe ser válida." 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." msgid "Amount can not be empty."
msgstr "No podéis dejar el importe en blanco." 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." msgid "Amount must be a number greater than zero."
msgstr "El importe tiene que ser un número mayor a cero." msgstr "El importe tiene que ser un número mayor a cero."
@ -1470,11 +1475,6 @@ msgctxt "input"
msgid "Invoice number" msgid "Invoice number"
msgstr "Número de factura" 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 #: pkg/expenses.go:337 pkg/expenses.go:514
msgctxt "input" msgctxt "input"
msgid "Expense Status" msgid "Expense Status"
@ -1501,7 +1501,7 @@ msgctxt "input"
msgid "Invoice Number" msgid "Invoice Number"
msgstr "Número de factura" msgstr "Número de factura"
#: pkg/expenses.go:747 #: pkg/expenses.go:732
msgid "expenses.ods" msgid "expenses.ods"
msgstr "gastos.ods" msgstr "gastos.ods"

View File

@ -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;

View File

@ -0,0 +1,7 @@
-- Revert numerus:payment_attachment from pg
begin;
drop table if exists numerus.payment_attachment;
commit;

View File

@ -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 <jordi@tandem.blog> # Add function to update payment and expense status 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to insert new payments 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to update 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 <jordi@tandem.blog> # Add function to update payments
payment_attachment [roles schema_numerus payment] 2024-08-11T21:01:50Z jordi fita mas <jordi@tandem.blog> # Add relation of payment attachments
attach_to_payment [roles schema_numerus payment payment_attachment] 2024-08-11T21:33:36Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to remove payments remove_payment [roles schema_numerus expense_payment payment payment_attachment update_expense_payment_status] 2024-08-11T00:30:58Z jordi fita mas <jordi@tandem.blog> # Add function to remove payments

View File

@ -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('<html><p>To pay 42 €</p></html>', 'UTF-8')) $$,
'Should be able to replate the second payments 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', '<html><p>To pay 42 €</p></html>')
$$,
'Should have attached all documents'
);
select *
from finish();
rollback;

131
test/payment_attachment.sql Normal file
View File

@ -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('<html>Payment <em>42</em></html>', '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;

View File

@ -5,7 +5,7 @@ reset client_min_messages;
begin; begin;
select plan(15); select plan(16);
set search_path to numerus, public; 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; set client_min_messages to warning;
truncate expense_payment cascade; truncate expense_payment;
truncate payment_attachment;
truncate payment cascade; truncate payment cascade;
truncate expense cascade; truncate expense cascade;
truncate contact cascade; truncate contact cascade;
@ -74,6 +75,12 @@ values (13, 16)
, (15, 19) , (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('<html> PAY <em>42</em></html>', 'UTF-8'))
;
select lives_ok( select lives_ok(
$$ select remove_payment('7ac3ae0e-b0c1-4206-a19b-0be20835edd4') $$, $$ select remove_payment('7ac3ae0e-b0c1-4206-a19b-0be20835edd4') $$,
'Should be able to remove a complete payment' 'Should be able to remove a complete payment'
@ -103,6 +110,12 @@ select bag_eq(
'Should have deleted all related expenses payments' '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 bag_eq(
$$ select expense_id, expense_status from expense $$, $$ select expense_id, expense_status from expense $$,
$$ values (13, 'pending') $$ values (13, 'pending')

View File

@ -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;

View File

@ -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;

View File

@ -19,6 +19,7 @@
<section data-hx-target="main"> <section data-hx-target="main">
<h2>{{ template "title" . }}</h2> <h2>{{ template "title" . }}</h2>
<form method="POST" action="{{ companyURI "/payments/" }}{{ .Slug }}" <form method="POST" action="{{ companyURI "/payments/" }}{{ .Slug }}"
enctype="multipart/form-data"
data-hx-swap="innerHTML show:false" data-hx-swap="innerHTML show:false"
data-hx-boost="true" data-hx-boost="true"
> >
@ -30,6 +31,7 @@
{{ template "input-field" .PaymentDate }} {{ template "input-field" .PaymentDate }}
{{ template "input-field" .Amount }} {{ template "input-field" .Amount }}
{{ template "tags-field" .Tags }} {{ template "tags-field" .Tags }}
{{ template "file-field" .File }}
<footer> <footer>
<button class="primary" type="submit">{{( pgettext "Update" "action" )}}</button> <button class="primary" type="submit">{{( pgettext "Update" "action" )}}</button>

View File

@ -27,6 +27,7 @@
<th>{{( pgettext "Status" "title" )}}</th> <th>{{( pgettext "Status" "title" )}}</th>
<th>{{( pgettext "Tags" "title" )}}</th> <th>{{( pgettext "Tags" "title" )}}</th>
<th class="numeric">{{( pgettext "Total" "title" )}}</th> <th class="numeric">{{( pgettext "Total" "title" )}}</th>
<th>{{( pgettext "Download" "title" )}}</th>
<th>{{( pgettext "Actions" "title" )}}</th> <th>{{( pgettext "Actions" "title" )}}</th>
</tr> </tr>
</thead> </thead>
@ -49,6 +50,14 @@
{{- end }} {{- end }}
</td> </td>
<td class="numeric">{{ .Total | formatPrice }}</td> <td class="numeric">{{ .Total | formatPrice }}</td>
<td class="invoice-download">
{{ if .OriginalFileName }}
<a href="{{ companyURI "/payments/"}}{{ .Slug }}/download/{{.OriginalFileName}}"
title="{{( pgettext "Download payment attachment" "action" )}}"
aria-label="{{( pgettext "Download payment attachment" "action" )}}"><i
class="ri-download-line"></i></a>
{{ end }}
</td>
<td class="actions"> <td class="actions">
<details class="menu"> <details class="menu">
{{- $label := .Description | printf (gettext "Actions for payment %s") -}} {{- $label := .Description | printf (gettext "Actions for payment %s") -}}

View File

@ -19,6 +19,7 @@
<section data-hx-target="main"> <section data-hx-target="main">
<h2>{{ template "title" . }}</h2> <h2>{{ template "title" . }}</h2>
<form method="POST" action="{{ companyURI "/payments" }}" <form method="POST" action="{{ companyURI "/payments" }}"
enctype="multipart/form-data"
data-hx-swap="innerHTML show:false" data-hx-swap="innerHTML show:false"
data-hx-boost="true"> data-hx-boost="true">
{{ csrfToken }} {{ csrfToken }}
@ -28,6 +29,7 @@
{{ template "input-field" .PaymentDate }} {{ template "input-field" .PaymentDate }}
{{ template "input-field" .Amount }} {{ template "input-field" .Amount }}
{{ template "tags-field" .Tags }} {{ template "tags-field" .Tags }}
{{ template "file-field" .File }}
<footer> <footer>
<button class="primary" type="submit">{{( pgettext "Save" "action" )}}</button> <button class="primary" type="submit">{{( pgettext "Save" "action" )}}</button>