Compare commits

...

5 Commits

Author SHA1 Message Date
jordi fita mas a30e015639 Add inline tags form to payments 2024-08-15 04:18:35 +02:00
jordi fita mas fa57c4b191 Refactor inline tag edit form into its own file
I was repeating myself a lot for this use case, because each one needed
a different URL and SQL query, however they were kind of structurally
similar and could be refactored into common functions.
2024-08-15 04:18:18 +02:00
jordi fita mas dca8b3a719 Add the document (expense) column to payment index page 2024-08-15 03:59:30 +02:00
jordi fita mas 9ab08deaa1 Include the taxes when updating an expense to paid or partial 2024-08-15 03:51:30 +02:00
jordi fita mas 7f21a2131e Add the where company_id filter to accounts and payments queries
I actually did not forget them, and i did not add them on purpose,
mistakenly believing that PostgreSQL’s row-level policies would project
only rows from the current company.  That is actually how Camper works,
but that’s because we use the request’s domain name to select the
company; here we use the path, and the row-level policy would return
rows from all companies the user belongs to.
2024-08-15 02:59:46 +02:00
16 changed files with 316 additions and 294 deletions

View File

@ -4,6 +4,7 @@
-- requires: expense -- requires: expense
-- requires: payment -- requires: payment
-- requires: expense_payment -- requires: expense_payment
-- requires: expense_tax_amount
-- requires: available_expense_status -- requires: available_expense_status
-- requires: available_payment_status -- requires: available_payment_status
@ -14,15 +15,16 @@ set search_path to numerus, public;
create or replace function update_expense_payment_status(pid integer, eid integer, amount_cents integer) returns void as create or replace function update_expense_payment_status(pid integer, eid integer, amount_cents integer) returns void as
$$ $$
update payment update payment
set payment_status = case when expense.amount > amount_cents or exists (select 1 from expense_payment as ep where ep.expense_id = expense.expense_id and payment_id <> pid) then 'partial' else 'complete' end set payment_status = case when expense.amount + coalesce(tax.amount, 0) > amount_cents or exists (select 1 from expense_payment as ep where ep.expense_id = expense.expense_id and payment_id <> pid) then 'partial' else 'complete' end
from expense from expense
left join ( select expense_id, sum(amount) as amount from expense_tax_amount group by expense_id) as tax using (expense_id)
where expense.expense_id = eid where expense.expense_id = eid
and payment_id = pid and payment_id = pid
; ;
update expense update expense
set expense_status = case set expense_status = case
when paid_amount >= expense.amount then 'paid' when paid_amount >= expense.amount + tax_amount then 'paid'
when paid_amount = 0 then 'pending' when paid_amount = 0 then 'pending'
else 'partial' end else 'partial' end
from ( from (
@ -30,7 +32,12 @@ $$
from expense_payment from expense_payment
join payment using (payment_id) join payment using (payment_id)
where expense_payment.expense_id = eid where expense_payment.expense_id = eid
) as payment ) as payment,
(
select coalesce (sum(amount), 0) as tax_amount
from expense_tax_amount
where expense_id = eid
) as tax
where expense.expense_id = eid where expense.expense_id = eid
; ;
$$ $$

View File

@ -19,9 +19,10 @@ const (
func servePaymentAccountIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func servePaymentAccountIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r) conn := getConn(r)
company := mustGetCompany(r)
locale := getLocale(r) locale := getLocale(r)
page := NewPaymentAccountIndexPage(r.Context(), conn, locale) page := NewPaymentAccountIndexPage(r.Context(), conn, company, locale)
page.MustRender(w, r) page.MustRender(w, r)
} }
@ -29,9 +30,9 @@ type PaymentAccountIndexPage struct {
Accounts []*PaymentAccountEntry Accounts []*PaymentAccountEntry
} }
func NewPaymentAccountIndexPage(ctx context.Context, conn *Conn, locale *Locale) *PaymentAccountIndexPage { func NewPaymentAccountIndexPage(ctx context.Context, conn *Conn, company *Company, locale *Locale) *PaymentAccountIndexPage {
return &PaymentAccountIndexPage{ return &PaymentAccountIndexPage{
Accounts: mustCollectPaymentAccountEntries(ctx, conn, locale), Accounts: mustCollectPaymentAccountEntries(ctx, conn, company, locale),
} }
} }
@ -50,7 +51,7 @@ type PaymentAccountEntry struct {
ExpirationDate string ExpirationDate string
} }
func mustCollectPaymentAccountEntries(ctx context.Context, conn *Conn, locale *Locale) []*PaymentAccountEntry { func mustCollectPaymentAccountEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale) []*PaymentAccountEntry {
rows := conn.MustQuery(ctx, ` rows := conn.MustQuery(ctx, `
select payment_account_id select payment_account_id
, slug , slug
@ -65,8 +66,9 @@ func mustCollectPaymentAccountEntries(ctx context.Context, conn *Conn, locale *L
left join payment_account_card using (payment_account_id, payment_account_type) left join payment_account_card using (payment_account_id, payment_account_type)
join payment_account_type using (payment_account_type) join payment_account_type using (payment_account_type)
left join payment_account_type_i18n as i18n on payment_account_type.payment_account_type = i18n.payment_account_type and i18n.lang_tag = $1 left join payment_account_type_i18n as i18n on payment_account_type.payment_account_type = i18n.payment_account_type and i18n.lang_tag = $1
where company_id = $2
order by payment_account_id order by payment_account_id
`, locale.Language.String()) `, locale.Language.String(), company.Id)
defer rows.Close() defer rows.Close()
var entries []*PaymentAccountEntry var entries []*PaymentAccountEntry

View File

@ -491,44 +491,11 @@ func (form *contactForm) TaxDetails() *CustomerTaxDetails {
} }
func ServeEditContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeEditContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r) serveTagsEditForm(w, r, params, "/contacts/", "select tags from contact where slug = $1")
locale := getLocale(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, "/contacts/"+slug+"/tags"), slug, locale)
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from contact where slug = $1`, form.Slug).Scan(form.Tags)) {
http.NotFound(w, r)
return
}
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
} }
func HandleUpdateContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func HandleUpdateContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r) handleUpdateTags(w, r, params, "/contacts/", "update contact set tags = $1 where slug = $2 returning slug")
conn := getConn(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, "/contacts/"+slug+"/tags/edit"), slug, locale)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if conn.MustGetText(r.Context(), "", "update contact set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
http.NotFound(w, r)
}
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
} }
func ServeImportPage(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func ServeImportPage(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {

View File

@ -568,45 +568,11 @@ func (form *expenseFilterForm) BuildQuery(args []interface{}) (string, []interfa
} }
func ServeEditExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeEditExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r) serveTagsEditForm(w, r, params, "/expenses/", "select tags from expense where slug = $1")
locale := getLocale(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, "/expenses/"+slug+"/tags"), slug, locale)
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from expense where slug = $1`, form.Slug).Scan(form.Tags)) {
http.NotFound(w, r)
return
}
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
} }
func HandleUpdateExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func HandleUpdateExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r) handleUpdateTags(w, r, params, "/expenses/", "update expense set tags = $1 where slug = $2 returning slug")
conn := getConn(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, "/expenses/"+slug+"/tags/edit"), slug, locale)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if conn.MustGetText(r.Context(), "", "update expense set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
http.NotFound(w, r)
return
}
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
} }
func ServeExpenseAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeExpenseAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) {

View File

@ -1447,70 +1447,12 @@ func handleInvoiceAction(w http.ResponseWriter, r *http.Request, action string,
} }
} }
type tagsForm struct {
Action string
Slug string
Tags *TagsField
}
func newTagsForm(uri string, slug string, locale *Locale) *tagsForm {
return &tagsForm{
Action: uri,
Slug: slug,
Tags: &TagsField{
Name: "tags-" + slug,
Label: pgettext("input", "Tags", locale),
},
}
}
func (form *tagsForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
form.Tags.FillValue(r)
return nil
}
func ServeEditInvoiceTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeEditInvoiceTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r) serveTagsEditForm(w, r, params, "/invoices/", "select tags from invoice where slug = $1")
locale := getLocale(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, "/invoices/"+slug+"/tags"), slug, locale)
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from invoice where slug = $1`, form.Slug).Scan(form.Tags)) {
http.NotFound(w, r)
return
}
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
} }
func HandleUpdateInvoiceTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func HandleUpdateInvoiceTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r) handleUpdateTags(w, r, params, "/invoices/", "update invoice set tags = $1 where slug = $2 returning slug")
conn := getConn(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, "/invoices/"+slug+"/tags/edit"), slug, locale)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if conn.MustGetText(r.Context(), "", "update invoice set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
http.NotFound(w, r)
}
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
} }
func ServeInvoiceAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeInvoiceAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) {

View File

@ -12,9 +12,10 @@ import (
func servePaymentIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func servePaymentIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r) conn := getConn(r)
company := mustGetCompany(r)
locale := getLocale(r) locale := getLocale(r)
page := NewPaymentIndexPage(r.Context(), conn, locale) page := NewPaymentIndexPage(r.Context(), conn, company, locale)
page.MustRender(w, r) page.MustRender(w, r)
} }
@ -22,9 +23,9 @@ type PaymentIndexPage struct {
Payments []*PaymentEntry Payments []*PaymentEntry
} }
func NewPaymentIndexPage(ctx context.Context, conn *Conn, locale *Locale) *PaymentIndexPage { func NewPaymentIndexPage(ctx context.Context, conn *Conn, company *Company, locale *Locale) *PaymentIndexPage {
return &PaymentIndexPage{ return &PaymentIndexPage{
Payments: mustCollectPaymentEntries(ctx, conn, locale), Payments: mustCollectPaymentEntries(ctx, conn, company, locale),
} }
} }
@ -37,6 +38,8 @@ type PaymentEntry struct {
Slug string Slug string
PaymentDate time.Time PaymentDate time.Time
Description string Description string
ExpenseSlug string
InvoiceNumber string
Total string Total string
OriginalFileName string OriginalFileName string
Tags []string Tags []string
@ -44,7 +47,7 @@ type PaymentEntry struct {
StatusLabel string StatusLabel string
} }
func mustCollectPaymentEntries(ctx context.Context, conn *Conn, locale *Locale) []*PaymentEntry { func mustCollectPaymentEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale) []*PaymentEntry {
rows := conn.MustQuery(ctx, ` rows := conn.MustQuery(ctx, `
select payment_id select payment_id
, payment.slug , payment.slug
@ -55,18 +58,23 @@ func mustCollectPaymentEntries(ctx context.Context, conn *Conn, locale *Locale)
, payment.payment_status , payment.payment_status
, psi18n.name , psi18n.name
, coalesce(attachment.original_filename, '') , coalesce(attachment.original_filename, '')
, coalesce(expense.slug::text, '')
, coalesce(expense.invoice_number, '')
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) left join payment_attachment as attachment using (payment_id)
left join expense_payment using (payment_id)
left join expense using (expense_id)
where payment.company_id = $2
order by payment_date desc, total desc order by payment_date desc, total desc
`, locale.Language) `, locale.Language, company.Id)
defer rows.Close() defer rows.Close()
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, &entry.OriginalFileName); err != nil { if err := rows.Scan(&entry.ID, &entry.Slug, &entry.PaymentDate, &entry.Description, &entry.Total, &entry.Tags, &entry.Status, &entry.StatusLabel, &entry.OriginalFileName, &entry.ExpenseSlug, &entry.InvoiceNumber); err != nil {
panic(err) panic(err)
} }
entries = append(entries, entry) entries = append(entries, entry)
@ -132,7 +140,7 @@ func newPaymentForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Name: "payment_account", Name: "payment_account",
Label: pgettext("input", "Account", locale), Label: pgettext("input", "Account", locale),
Required: true, Required: true,
Options: MustGetOptions(ctx, conn, "select payment_account_id::text, name from payment_account order by name"), Options: MustGetOptions(ctx, conn, "select payment_account_id::text, name from payment_account where company_id = $1 order by name", company.Id),
}, },
Amount: &InputField{ Amount: &InputField{
Name: "amount", Name: "amount",
@ -306,3 +314,11 @@ func servePaymentAttachment(w http.ResponseWriter, r *http.Request, params httpr
where slug = $1 where slug = $1
`) `)
} }
func servePaymentTagsEditForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
serveTagsEditForm(w, r, params, "/payments/", "select tags from payment where slug = $1")
}
func handleUpdatePaymentTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
handleUpdateTags(w, r, params, "/payments/", "update payment set tags = $1 where slug = $2 returning slug")
}

View File

@ -367,42 +367,9 @@ func HandleProductSearch(w http.ResponseWriter, r *http.Request, _ httprouter.Pa
} }
func ServeEditProductTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeEditProductTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r) serveTagsEditForm(w, r, params, "/products/", "select tags from product where slug = $1")
locale := getLocale(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, "/products/"+slug+"/tags"), slug, locale)
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from product where slug = $1`, form.Slug).Scan(form.Tags)) {
http.NotFound(w, r)
return
}
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
} }
func HandleUpdateProductTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func HandleUpdateProductTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r) handleUpdateTags(w, r, params, "/products/", "update product set tags = $1 where slug = $2 returning slug")
conn := getConn(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, "/products/"+slug+"/tags/edit"), slug, locale)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if conn.MustGetText(r.Context(), "", "update product set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
http.NotFound(w, r)
}
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
} }

View File

@ -1187,42 +1187,9 @@ func handleQuoteAction(w http.ResponseWriter, r *http.Request, action string, re
} }
func ServeEditQuoteTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeEditQuoteTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r) serveTagsEditForm(w, r, params, "/quotes/", "select tags from quote where slug = $1")
locale := getLocale(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, "/quotes/"+slug+"/tags"), slug, locale)
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from quote where slug = $1`, form.Slug).Scan(form.Tags)) {
http.NotFound(w, r)
return
}
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
} }
func HandleUpdateQuoteTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func HandleUpdateQuoteTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r) handleUpdateTags(w, r, params, "/quotes/", "update quote set tags = $1 where slug = $2 returning slug")
conn := getConn(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, "/quotes/"+slug+"/tags/edit"), slug, locale)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if conn.MustGetText(r.Context(), "", "update quote set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
http.NotFound(w, r)
}
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
} }

View File

@ -67,6 +67,8 @@ 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.PUT("/payments/:slug/tags", handleUpdatePaymentTags)
companyRouter.GET("/payments/:slug/tags/edit", servePaymentTagsEditForm)
companyRouter.GET("/payments/:slug/download/:filename", servePaymentAttachment) 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)

73
pkg/tags.go Normal file
View File

@ -0,0 +1,73 @@
package pkg
import (
"github.com/julienschmidt/httprouter"
"net/http"
)
func serveTagsEditForm(w http.ResponseWriter, r *http.Request, params httprouter.Params, prefix string, sql string) {
conn := getConn(r)
locale := getLocale(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, prefix+slug+"/tags"), slug, locale)
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), sql, form.Slug).Scan(form.Tags)) {
http.NotFound(w, r)
return
}
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
}
type tagsForm struct {
Action string
Slug string
Tags *TagsField
}
func newTagsForm(uri string, slug string, locale *Locale) *tagsForm {
return &tagsForm{
Action: uri,
Slug: slug,
Tags: &TagsField{
Name: "tags-" + slug,
Label: pgettext("input", "Tags", locale),
},
}
}
func (form *tagsForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
form.Tags.FillValue(r)
return nil
}
func handleUpdateTags(w http.ResponseWriter, r *http.Request, params httprouter.Params, prefix string, sql string) {
locale := getLocale(r)
conn := getConn(r)
company := getCompany(r)
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
form := newTagsForm(companyURI(company, prefix+slug+"/tags/edit"), slug, locale)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if conn.MustGetText(r.Context(), "", sql, form.Tags, form.Slug) == "" {
http.NotFound(w, r)
return
}
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
}

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-14 04:02+0200\n" "POT-Creation-Date: 2024-08-15 03:58+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"
@ -116,7 +116,7 @@ msgstr "Subtotal"
#: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82 #: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82
#: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75 #: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75
#: web/template/expenses/new.gohtml:46 web/template/expenses/index.gohtml:74 #: web/template/expenses/new.gohtml:46 web/template/expenses/index.gohtml:74
#: web/template/expenses/edit.gohtml:48 web/template/payments/index.gohtml:29 #: web/template/expenses/edit.gohtml:48 web/template/payments/index.gohtml:30
msgctxt "title" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
@ -193,14 +193,14 @@ msgid "Customer"
msgstr "Client" msgstr "Client"
#: web/template/invoices/index.gohtml:70 web/template/quotes/index.gohtml:70 #: web/template/invoices/index.gohtml:70 web/template/quotes/index.gohtml:70
#: web/template/expenses/index.gohtml:68 web/template/payments/index.gohtml:27 #: web/template/expenses/index.gohtml:68 web/template/payments/index.gohtml:28
msgctxt "title" msgctxt "title"
msgid "Status" msgid "Status"
msgstr "Estat" msgstr "Estat"
#: web/template/invoices/index.gohtml:71 web/template/quotes/index.gohtml:71 #: web/template/invoices/index.gohtml:71 web/template/quotes/index.gohtml:71
#: web/template/contacts/index.gohtml:50 web/template/expenses/index.gohtml:69 #: web/template/contacts/index.gohtml:50 web/template/expenses/index.gohtml:69
#: web/template/products/index.gohtml:46 web/template/payments/index.gohtml:28 #: web/template/products/index.gohtml:46 web/template/payments/index.gohtml:29
msgctxt "title" msgctxt "title"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
@ -212,7 +212,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/payments/index.gohtml:30 #: web/template/expenses/index.gohtml:75 web/template/payments/index.gohtml:31
msgctxt "title" msgctxt "title"
msgid "Download" msgid "Download"
msgstr "Descàrrega" msgstr "Descàrrega"
@ -220,7 +220,7 @@ msgstr "Descàrrega"
#: web/template/invoices/index.gohtml:74 web/template/quotes/index.gohtml:74 #: web/template/invoices/index.gohtml:74 web/template/quotes/index.gohtml:74
#: web/template/contacts/index.gohtml:51 web/template/expenses/index.gohtml:76 #: web/template/contacts/index.gohtml:51 web/template/expenses/index.gohtml:76
#: web/template/company/switch.gohtml:23 web/template/products/index.gohtml:48 #: web/template/company/switch.gohtml:23 web/template/products/index.gohtml:48
#: web/template/payments/index.gohtml:31 #: web/template/payments/index.gohtml:32
msgctxt "title" msgctxt "title"
msgid "Actions" msgid "Actions"
msgstr "Accions" msgstr "Accions"
@ -242,7 +242,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:121 #: web/template/contacts/index.gohtml:82 web/template/expenses/index.gohtml:121
#: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:71 #: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:77
msgctxt "action" msgctxt "action"
msgid "Edit" msgid "Edit"
msgstr "Edita" msgstr "Edita"
@ -794,20 +794,25 @@ msgctxt "title"
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: web/template/payments/index.gohtml:36 #: web/template/payments/index.gohtml:27
msgctxt "title"
msgid "Document"
msgstr "Document"
#: web/template/payments/index.gohtml:37
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:63 #: web/template/payments/index.gohtml:69
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:82 #: web/template/payments/index.gohtml:88
msgctxt "action" msgctxt "action"
msgid "Remove" msgid "Remove"
msgstr "Esborra" msgstr "Esborra"
#: web/template/payments/index.gohtml:92 #: web/template/payments/index.gohtml:98
msgid "No payments added yet." msgid "No payments added yet."
msgstr "No hi ha cap pagament." msgstr "No hi ha cap pagament."
@ -882,14 +887,14 @@ msgstr "No podeu deixar la contrasenya en blanc."
msgid "Invalid user or password." msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes." msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 pkg/accounts.go:138 #: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 pkg/accounts.go:140
#: pkg/invoices.go:1147 pkg/contacts.go:149 pkg/contacts.go:262 #: pkg/invoices.go:1147 pkg/contacts.go:149 pkg/contacts.go:262
msgctxt "input" msgctxt "input"
msgid "Name" 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:154 pkg/expenses.go:335 pkg/expenses.go:485 #: pkg/payments.go:162 pkg/expenses.go:335 pkg/expenses.go:485
#: 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"
@ -924,7 +929,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:121 #: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:129
#: pkg/invoices.go:1161 #: pkg/invoices.go:1161
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
@ -1339,103 +1344,103 @@ 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:127 pkg/expenses.go:304 pkg/invoices.go:866 #: pkg/payments.go:135 pkg/expenses.go:304 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:133 #: pkg/payments.go:141
msgctxt "input" msgctxt "input"
msgid "Account" msgid "Account"
msgstr "Compte" msgstr "Compte"
#: pkg/payments.go:139 pkg/expenses.go:319 #: pkg/payments.go:147 pkg/expenses.go:319
msgctxt "input" msgctxt "input"
msgid "Amount" msgid "Amount"
msgstr "Import" msgstr "Import"
#: pkg/payments.go:149 pkg/expenses.go:330 pkg/invoices.go:888 #: pkg/payments.go:157 pkg/expenses.go:330 pkg/invoices.go:888
msgctxt "input" msgctxt "input"
msgid "File" msgid "File"
msgstr "Fitxer" msgstr "Fitxer"
#: pkg/payments.go:161 #: pkg/payments.go:169
msgid "Select an account." msgid "Select an account."
msgstr "Escolliu un compte." msgstr "Escolliu un compte."
#: pkg/payments.go:210 #: pkg/payments.go:218
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:211 #: pkg/payments.go:219
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:212 #: pkg/payments.go:220
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:213 pkg/expenses.go:372 #: pkg/payments.go:221 pkg/expenses.go:372
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:214 pkg/expenses.go:373 #: pkg/payments.go:222 pkg/expenses.go:373
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."
#: pkg/accounts.go:129 #: pkg/accounts.go:131
msgctxt "input" msgctxt "input"
msgid "Type" msgid "Type"
msgstr "Tipus" msgstr "Tipus"
#: pkg/accounts.go:144 pkg/contacts.go:352 #: pkg/accounts.go:146 pkg/contacts.go:352
msgctxt "input" msgctxt "input"
msgid "IBAN" msgid "IBAN"
msgstr "IBAN" msgstr "IBAN"
#: pkg/accounts.go:150 #: pkg/accounts.go:152
msgctxt "input" msgctxt "input"
msgid "Cards last four digits" msgid "Cards last four digits"
msgstr "Els quatre darrers dígits de la targeta" msgstr "Els quatre darrers dígits de la targeta"
#: pkg/accounts.go:161 #: pkg/accounts.go:163
msgctxt "input" msgctxt "input"
msgid "Expiration date" msgid "Expiration date"
msgstr "Data de caducitat" msgstr "Data de caducitat"
#: pkg/accounts.go:227 #: pkg/accounts.go:229
msgid "Selected payment account type is not valid." msgid "Selected payment account type is not valid."
msgstr "Heu seleccionat un tipus de compte de pagament que no és vàlid." msgstr "Heu seleccionat un tipus de compte de pagament que no és vàlid."
#: pkg/accounts.go:230 #: pkg/accounts.go:232
msgid "IBAN can not be empty." msgid "IBAN can not be empty."
msgstr "No podeu deixar lIBAN en blanc." msgstr "No podeu deixar lIBAN en blanc."
#: pkg/accounts.go:231 #: pkg/accounts.go:233
msgid "This value is not a valid IBAN." msgid "This value is not a valid IBAN."
msgstr "Aquest valor no és un IBAN vàlid." msgstr "Aquest valor no és un IBAN vàlid."
#: pkg/accounts.go:234 #: pkg/accounts.go:236
msgid "Last four digits can not be empty." msgid "Last four digits can not be empty."
msgstr "No podeu deixar el quatre darrers dígits en blanc." msgstr "No podeu deixar el quatre darrers dígits en blanc."
#: pkg/accounts.go:235 #: pkg/accounts.go:237
msgid "You must enter the cards last four digits" msgid "You must enter the cards last four digits"
msgstr "Heu dentrar els quatre darrers dígits de la targeta" msgstr "Heu dentrar els quatre darrers dígits de la targeta"
#: pkg/accounts.go:236 #: pkg/accounts.go:238
msgid "Last four digits must be a number." msgid "Last four digits must be a number."
msgstr "El quatre darrera dígits han de ser un número." msgstr "El quatre darrera dígits han de ser un número."
#: pkg/accounts.go:239 #: pkg/accounts.go:241
msgid "Expiration date can not be empty." msgid "Expiration date can not be empty."
msgstr "No podeu deixar la data de pagament en blanc." msgstr "No podeu deixar la data de pagament en blanc."
#: pkg/accounts.go:241 #: pkg/accounts.go:243
msgid "Expiration date should be a valid date in format MM/YY (e.g., 08/24)." msgid "Expiration date should be a valid date in format MM/YY (e.g., 08/24)."
msgstr "La data de caducitat has de ser vàlida i en format MM/AA (p. ex., 08/24)." msgstr "La data de caducitat has de ser vàlida i en format MM/AA (p. ex., 08/24)."
#: pkg/accounts.go:245 #: pkg/accounts.go:247
msgid "Payment account name can not be empty." msgid "Payment account name can not be empty."
msgstr "No podeu deixar el nom del compte de pagament en blanc." msgstr "No podeu deixar el nom del compte de pagament en blanc."

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-14 04:02+0200\n" "POT-Creation-Date: 2024-08-15 03:58+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"
@ -116,7 +116,7 @@ msgstr "Subtotal"
#: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82 #: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82
#: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75 #: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75
#: web/template/expenses/new.gohtml:46 web/template/expenses/index.gohtml:74 #: web/template/expenses/new.gohtml:46 web/template/expenses/index.gohtml:74
#: web/template/expenses/edit.gohtml:48 web/template/payments/index.gohtml:29 #: web/template/expenses/edit.gohtml:48 web/template/payments/index.gohtml:30
msgctxt "title" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
@ -193,14 +193,14 @@ msgid "Customer"
msgstr "Cliente" msgstr "Cliente"
#: web/template/invoices/index.gohtml:70 web/template/quotes/index.gohtml:70 #: web/template/invoices/index.gohtml:70 web/template/quotes/index.gohtml:70
#: web/template/expenses/index.gohtml:68 web/template/payments/index.gohtml:27 #: web/template/expenses/index.gohtml:68 web/template/payments/index.gohtml:28
msgctxt "title" msgctxt "title"
msgid "Status" msgid "Status"
msgstr "Estado" msgstr "Estado"
#: web/template/invoices/index.gohtml:71 web/template/quotes/index.gohtml:71 #: web/template/invoices/index.gohtml:71 web/template/quotes/index.gohtml:71
#: web/template/contacts/index.gohtml:50 web/template/expenses/index.gohtml:69 #: web/template/contacts/index.gohtml:50 web/template/expenses/index.gohtml:69
#: web/template/products/index.gohtml:46 web/template/payments/index.gohtml:28 #: web/template/products/index.gohtml:46 web/template/payments/index.gohtml:29
msgctxt "title" msgctxt "title"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
@ -212,7 +212,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/payments/index.gohtml:30 #: web/template/expenses/index.gohtml:75 web/template/payments/index.gohtml:31
msgctxt "title" msgctxt "title"
msgid "Download" msgid "Download"
msgstr "Descargar" msgstr "Descargar"
@ -220,7 +220,7 @@ msgstr "Descargar"
#: web/template/invoices/index.gohtml:74 web/template/quotes/index.gohtml:74 #: web/template/invoices/index.gohtml:74 web/template/quotes/index.gohtml:74
#: web/template/contacts/index.gohtml:51 web/template/expenses/index.gohtml:76 #: web/template/contacts/index.gohtml:51 web/template/expenses/index.gohtml:76
#: web/template/company/switch.gohtml:23 web/template/products/index.gohtml:48 #: web/template/company/switch.gohtml:23 web/template/products/index.gohtml:48
#: web/template/payments/index.gohtml:31 #: web/template/payments/index.gohtml:32
msgctxt "title" msgctxt "title"
msgid "Actions" msgid "Actions"
msgstr "Acciones" msgstr "Acciones"
@ -242,7 +242,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:121 #: web/template/contacts/index.gohtml:82 web/template/expenses/index.gohtml:121
#: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:71 #: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:77
msgctxt "action" msgctxt "action"
msgid "Edit" msgid "Edit"
msgstr "Editar" msgstr "Editar"
@ -794,20 +794,25 @@ msgctxt "title"
msgid "Description" msgid "Description"
msgstr "Descripción" msgstr "Descripción"
#: web/template/payments/index.gohtml:36 #: web/template/payments/index.gohtml:27
msgctxt "title"
msgid "Document"
msgstr "Documento"
#: web/template/payments/index.gohtml:37
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:63 #: web/template/payments/index.gohtml:69
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:82 #: web/template/payments/index.gohtml:88
msgctxt "action" msgctxt "action"
msgid "Remove" msgid "Remove"
msgstr "Borrar" msgstr "Borrar"
#: web/template/payments/index.gohtml:92 #: web/template/payments/index.gohtml:98
msgid "No payments added yet." msgid "No payments added yet."
msgstr "No hay pagos." msgstr "No hay pagos."
@ -882,14 +887,14 @@ msgstr "No podéis dejar la contraseña en blanco."
msgid "Invalid user or password." msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido." msgstr "Nombre de usuario o contraseña inválido."
#: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 pkg/accounts.go:138 #: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 pkg/accounts.go:140
#: pkg/invoices.go:1147 pkg/contacts.go:149 pkg/contacts.go:262 #: pkg/invoices.go:1147 pkg/contacts.go:149 pkg/contacts.go:262
msgctxt "input" msgctxt "input"
msgid "Name" 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:154 pkg/expenses.go:335 pkg/expenses.go:485 #: pkg/payments.go:162 pkg/expenses.go:335 pkg/expenses.go:485
#: 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"
@ -924,7 +929,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:121 #: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:129
#: pkg/invoices.go:1161 #: pkg/invoices.go:1161
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
@ -1339,103 +1344,103 @@ 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:127 pkg/expenses.go:304 pkg/invoices.go:866 #: pkg/payments.go:135 pkg/expenses.go:304 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:133 #: pkg/payments.go:141
msgctxt "input" msgctxt "input"
msgid "Account" msgid "Account"
msgstr "Cuenta" msgstr "Cuenta"
#: pkg/payments.go:139 pkg/expenses.go:319 #: pkg/payments.go:147 pkg/expenses.go:319
msgctxt "input" msgctxt "input"
msgid "Amount" msgid "Amount"
msgstr "Importe" msgstr "Importe"
#: pkg/payments.go:149 pkg/expenses.go:330 pkg/invoices.go:888 #: pkg/payments.go:157 pkg/expenses.go:330 pkg/invoices.go:888
msgctxt "input" msgctxt "input"
msgid "File" msgid "File"
msgstr "Archivo" msgstr "Archivo"
#: pkg/payments.go:161 #: pkg/payments.go:169
msgid "Select an account." msgid "Select an account."
msgstr "Escoged una cuenta." msgstr "Escoged una cuenta."
#: pkg/payments.go:210 #: pkg/payments.go:218
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:211 #: pkg/payments.go:219
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:212 #: pkg/payments.go:220
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:213 pkg/expenses.go:372 #: pkg/payments.go:221 pkg/expenses.go:372
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:214 pkg/expenses.go:373 #: pkg/payments.go:222 pkg/expenses.go:373
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."
#: pkg/accounts.go:129 #: pkg/accounts.go:131
msgctxt "input" msgctxt "input"
msgid "Type" msgid "Type"
msgstr "Tipo" msgstr "Tipo"
#: pkg/accounts.go:144 pkg/contacts.go:352 #: pkg/accounts.go:146 pkg/contacts.go:352
msgctxt "input" msgctxt "input"
msgid "IBAN" msgid "IBAN"
msgstr "IBAN" msgstr "IBAN"
#: pkg/accounts.go:150 #: pkg/accounts.go:152
msgctxt "input" msgctxt "input"
msgid "Cards last four digits" msgid "Cards last four digits"
msgstr "Últimos cuatro dígitos de la tarjeta" msgstr "Últimos cuatro dígitos de la tarjeta"
#: pkg/accounts.go:161 #: pkg/accounts.go:163
msgctxt "input" msgctxt "input"
msgid "Expiration date" msgid "Expiration date"
msgstr "Fecha de caducidad" msgstr "Fecha de caducidad"
#: pkg/accounts.go:227 #: pkg/accounts.go:229
msgid "Selected payment account type is not valid." msgid "Selected payment account type is not valid."
msgstr "Habéis escogido un tipo de cuenta de pago que no es válido." msgstr "Habéis escogido un tipo de cuenta de pago que no es válido."
#: pkg/accounts.go:230 #: pkg/accounts.go:232
msgid "IBAN can not be empty." msgid "IBAN can not be empty."
msgstr "No podéis dejar el IBAN en blanco." msgstr "No podéis dejar el IBAN en blanco."
#: pkg/accounts.go:231 #: pkg/accounts.go:233
msgid "This value is not a valid IBAN." msgid "This value is not a valid IBAN."
msgstr "Este valor no es un IBAN válido." msgstr "Este valor no es un IBAN válido."
#: pkg/accounts.go:234 #: pkg/accounts.go:236
msgid "Last four digits can not be empty." msgid "Last four digits can not be empty."
msgstr "No podéis dejar los cuatro últimos dígitos en blanco." msgstr "No podéis dejar los cuatro últimos dígitos en blanco."
#: pkg/accounts.go:235 #: pkg/accounts.go:237
msgid "You must enter the cards last four digits" msgid "You must enter the cards last four digits"
msgstr "Debéis entrar los cuatro últimos dígitos de la tarjeta" msgstr "Debéis entrar los cuatro últimos dígitos de la tarjeta"
#: pkg/accounts.go:236 #: pkg/accounts.go:238
msgid "Last four digits must be a number." msgid "Last four digits must be a number."
msgstr "Los cuatro últimos dígitos tienen que ser un número." msgstr "Los cuatro últimos dígitos tienen que ser un número."
#: pkg/accounts.go:239 #: pkg/accounts.go:241
msgid "Expiration date can not be empty." msgid "Expiration date can not be empty."
msgstr "No podéis dejar la fecha de caducidad en blanco." msgstr "No podéis dejar la fecha de caducidad en blanco."
#: pkg/accounts.go:241 #: pkg/accounts.go:243
msgid "Expiration date should be a valid date in format MM/YY (e.g., 08/24)." msgid "Expiration date should be a valid date in format MM/YY (e.g., 08/24)."
msgstr "La fecha de caducidad tiene que ser válida y en formato MM/AA (p. ej., 08/24)." msgstr "La fecha de caducidad tiene que ser válida y en formato MM/AA (p. ej., 08/24)."
#: pkg/accounts.go:245 #: pkg/accounts.go:247
msgid "Payment account name can not be empty." msgid "Payment account name can not be empty."
msgstr "No podéis dejar el nombre de la cuenta de pago en blanco." msgstr "No podéis dejar el nombre de la cuenta de pago en blanco."

View File

@ -149,7 +149,7 @@ available_payment_status [schema_numerus payment_status payment_status_i18n] 202
payment [roles schema_numerus company payment_account currency tag_name payment_status extension_pgcrypto] 2024-08-01T01:28:59Z jordi fita mas <jordi@tandem.blog> # Add relation for accounts payable payment [roles schema_numerus company payment_account currency tag_name payment_status extension_pgcrypto] 2024-08-01T01:28:59Z jordi fita mas <jordi@tandem.blog> # Add relation for accounts payable
expense_payment [roles schema_numerus expense payment] 2024-08-04T03:44:30Z jordi fita mas <jordi@tandem.blog> # Add relation of expense payments expense_payment [roles schema_numerus expense payment] 2024-08-04T03:44:30Z jordi fita mas <jordi@tandem.blog> # Add relation of expense payments
available_expense_status [available_expense_status@v2] 2024-08-04T05:24:08Z jordi fita mas <jordi@tandem.blog> # Add “partial” expense status available_expense_status [available_expense_status@v2] 2024-08-04T05:24:08Z jordi fita mas <jordi@tandem.blog> # Add “partial” 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 update_expense_payment_status [roles schema_numerus expense payment expense_payment expense_tax_amount 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 payment_attachment [roles schema_numerus payment] 2024-08-11T21:01:50Z jordi fita mas <jordi@tandem.blog> # Add relation of payment attachments

View File

@ -5,7 +5,7 @@ reset client_min_messages;
begin; begin;
select plan(17); select plan(20);
set search_path to numerus, auth, public; set search_path to numerus, auth, public;
@ -25,8 +25,11 @@ set client_min_messages to warning;
truncate expense_payment cascade; truncate expense_payment cascade;
truncate payment cascade; truncate payment cascade;
truncate payment_account cascade; truncate payment_account cascade;
truncate expense_tax cascade;
truncate expense cascade; truncate expense cascade;
truncate contact cascade; truncate contact cascade;
truncate tax cascade;
truncate tax_class cascade;
truncate payment_method cascade; truncate payment_method cascade;
truncate company cascade; truncate company cascade;
reset client_min_messages; reset client_min_messages;
@ -45,6 +48,16 @@ values (111, 1, 'cash', 'cash')
set constraints "company_default_payment_method_id_fkey" immediate; set constraints "company_default_payment_method_id_fkey" immediate;
insert into tax_class (tax_class_id, company_id, name)
values (11, 1, 'tax')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (2, 1, 11, 'IRPF -15 %', -0.15)
, (3, 1, 11, 'IVA 4 %', 0.04)
, (4, 1, 11, 'IVA 10 %', 0.10)
;
insert into contact (contact_id, company_id, name) insert into contact (contact_id, company_id, name)
values ( 9, 1, 'Customer 1') values ( 9, 1, 'Customer 1')
, (10, 2, 'Customer 2') , (10, 2, 'Customer 2')
@ -55,6 +68,16 @@ values (12, 1, 'REF123', 9, '2011-01-11', 111, 'EUR')
, (13, 2, 'INV001', 10, '2011-01-11', 111, 'USD') , (13, 2, 'INV001', 10, '2011-01-11', 111, 'USD')
, (14, 2, 'INV002', 10, '2022-02-22', 222, 'USD') , (14, 2, 'INV002', 10, '2022-02-22', 222, 'USD')
, (15, 2, 'INV003', 10, '2022-02-22', 222, 'USD') , (15, 2, 'INV003', 10, '2022-02-22', 222, 'USD')
, (16, 1, 'REF001', 9, '2023-03-03', 10000, 'EUR')
, (17, 1, 'REF002', 9, '2023-03-03', 10000, 'EUR')
, (18, 1, 'REF003', 9, '2023-03-03', 10000, 'EUR')
;
insert into expense_tax (expense_id, tax_id, tax_rate)
values (16, 3, 0.04)
, (17, 2, -0.15)
, (18, 2, -0.15)
, (18, 4, 0.10)
; ;
insert into payment_account (payment_account_id, company_id, payment_account_type, name) insert into payment_account (payment_account_id, company_id, payment_account_type, name)
@ -87,6 +110,21 @@ select lives_ok(
'Should be able to insert a partial payment for the third expense' 'Should be able to insert a partial payment for the third expense'
); );
select lives_ok(
$$ select add_payment(1, 16, '2023-03-06', 11, 'Re: REF001', '103.99', array[]::tag_name[]) $$,
'Should be able to pay an expense with taxes'
);
select lives_ok(
$$ select add_payment(1, 17, '2023-03-06', 11, 'Re: REF002', '85', array[]::tag_name[]) $$,
'Should be able to pay an expense with negative taxes'
);
select lives_ok(
$$ select add_payment(1, 18, '2023-03-06', 11, 'Re: REF003', '95', array[]::tag_name[]) $$,
'Should be able to pay an expense with multiple taxes'
);
select bag_eq( select bag_eq(
$$ select company_id, description, payment_date::text, payment_account_id, amount, currency_code, payment_status, tags::text, created_at from payment $$, $$ select company_id, description, payment_date::text, payment_account_id, amount, currency_code, payment_status, tags::text, created_at from payment $$,
$$ values (1, '“Protection”', '2023-05-02', 11, 1111, 'EUR', 'complete', '{tag1,tag2}', current_timestamp) $$ values (1, '“Protection”', '2023-05-02', 11, 1111, 'EUR', 'complete', '{tag1,tag2}', current_timestamp)
@ -94,6 +132,9 @@ select bag_eq(
, (2, 'First payment of INV002', '2023-05-04', 22, 100, 'USD', 'partial', '{}', current_timestamp) , (2, 'First payment of INV002', '2023-05-04', 22, 100, 'USD', 'partial', '{}', current_timestamp)
, (2, 'Second payment of INV002', '2023-05-05', 22, 122, 'USD', 'partial', '{}', current_timestamp) , (2, 'Second payment of INV002', '2023-05-05', 22, 122, 'USD', 'partial', '{}', current_timestamp)
, (2, 'Partial payment of INV003', '2023-05-06', 22, 111, 'USD', 'partial', '{}', current_timestamp) , (2, 'Partial payment of INV003', '2023-05-06', 22, 111, 'USD', 'partial', '{}', current_timestamp)
, (1, 'Re: REF001', '2023-03-06', 11, 10399, 'EUR', 'partial', '{}', current_timestamp)
, (1, 'Re: REF002', '2023-03-06', 11, 8500, 'EUR', 'complete', '{}', current_timestamp)
, (1, 'Re: REF003', '2023-03-06', 11, 9500, 'EUR', 'complete', '{}', current_timestamp)
$$, $$,
'Should have created all payments' 'Should have created all payments'
); );
@ -104,6 +145,9 @@ select bag_eq(
, (14, 'First payment of INV002') , (14, 'First payment of INV002')
, (14, 'Second payment of INV002') , (14, 'Second payment of INV002')
, (15, 'Partial payment of INV003') , (15, 'Partial payment of INV003')
, (16, 'Re: REF001')
, (17, 'Re: REF002')
, (18, 'Re: REF003')
$$, $$,
'Should have linked all expenses to payments' 'Should have linked all expenses to payments'
); );
@ -114,6 +158,9 @@ select bag_eq(
, (13, 'paid') , (13, 'paid')
, (14, 'paid') , (14, 'paid')
, (15, 'partial') , (15, 'partial')
, (16, 'partial')
, (17, 'paid')
, (18, 'paid')
$$, $$,
'Should have updated the status of expenses' 'Should have updated the status of expenses'
); );

View File

@ -5,7 +5,7 @@ reset client_min_messages;
begin; begin;
select plan(14); select plan(17);
set search_path to numerus, public; set search_path to numerus, public;
@ -24,8 +24,11 @@ select function_privs_are('numerus', 'edit_payment', array ['uuid', 'date', 'int
set client_min_messages to warning; set client_min_messages to warning;
truncate expense_payment cascade; truncate expense_payment cascade;
truncate payment cascade; truncate payment cascade;
truncate expense_tax cascade;
truncate expense cascade; truncate expense cascade;
truncate contact cascade; truncate contact cascade;
truncate tax cascade;
truncate tax_class cascade;
truncate payment_account cascade; truncate payment_account cascade;
truncate payment_method cascade; truncate payment_method cascade;
truncate company cascade; truncate company cascade;
@ -44,6 +47,16 @@ values (111, 1, 'cash', 'cash')
set constraints "company_default_payment_method_id_fkey" immediate; set constraints "company_default_payment_method_id_fkey" immediate;
insert into tax_class (tax_class_id, company_id, name)
values (11, 1, 'tax')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (2, 1, 11, 'IRPF -15 %', -0.15)
, (3, 1, 11, 'IVA 4 %', 0.04)
, (4, 1, 11, 'IVA 10 %', 0.10)
;
insert into contact (contact_id, company_id, name) insert into contact (contact_id, company_id, name)
values ( 9, 1, 'Customer 1') values ( 9, 1, 'Customer 1')
; ;
@ -52,6 +65,16 @@ insert into expense (expense_id, company_id, invoice_number, contact_id, invoice
values (13, 1, 'INV001', 9, '2011-01-11', 111, 'EUR', 'paid') values (13, 1, 'INV001', 9, '2011-01-11', 111, 'EUR', 'paid')
, (14, 1, 'INV002', 9, '2022-02-22', 222, 'EUR', 'paid') , (14, 1, 'INV002', 9, '2022-02-22', 222, 'EUR', 'paid')
, (15, 1, 'INV003', 9, '2022-02-22', 333, 'EUR', 'partial') , (15, 1, 'INV003', 9, '2022-02-22', 333, 'EUR', 'partial')
, (16, 1, 'REF001', 9, '2023-03-03', 10000, 'EUR', 'paid')
, (17, 1, 'REF002', 9, '2023-03-03', 10000, 'EUR', 'paid')
, (18, 1, 'REF003', 9, '2023-03-03', 10000, 'EUR', 'paid')
;
insert into expense_tax (expense_id, tax_id, tax_rate)
values (16, 3, 0.04)
, (17, 2, -0.15)
, (18, 2, -0.15)
, (18, 4, 0.10)
; ;
insert into payment_account (payment_account_id, company_id, payment_account_type, name) insert into payment_account (payment_account_id, company_id, payment_account_type, name)
@ -65,6 +88,9 @@ values (16, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Payment INV001', '2023-0
, (17, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'First INV002', '2023-05-05', 13, 100, 'EUR', 'partial', '{tag2}') , (17, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'First INV002', '2023-05-05', 13, 100, 'EUR', 'partial', '{tag2}')
, (18, 1, '3bdad7a8-4a1e-4ae0-b5c6-015e51ee0502', 'Second INV002', '2023-05-06', 13, 122, 'EUR', 'partial', '{tag1,tag3}') , (18, 1, '3bdad7a8-4a1e-4ae0-b5c6-015e51ee0502', 'Second INV002', '2023-05-06', 13, 122, 'EUR', 'partial', '{tag1,tag3}')
, (19, 1, '5a524bee-8311-4d13-9adf-ef6310b26990', 'Partial INV003', '2023-05-07', 11, 123, 'EUR', 'partial', '{}') , (19, 1, '5a524bee-8311-4d13-9adf-ef6310b26990', 'Partial INV003', '2023-05-07', 11, 123, 'EUR', 'partial', '{}')
, (20, 1, '65222c3b-4faa-4be4-b39c-5bd170a943cf', 'Re: REF001', '2023-03-07', 11, 10400, 'EUR', 'complete', '{}')
, (21, 1, 'dbb699cf-d1f4-40ff-96cb-8f29e238d51d', 'Re: REF002', '2023-03-07', 11, 8500, 'EUR', 'complete', '{}')
, (22, 1, '0756a50f-2957-4661-abd2-e422a848af4e', 'Re: REF003', '2023-03-07', 11, 9500, 'EUR', 'complete', '{}')
; ;
insert into expense_payment (expense_id, payment_id) insert into expense_payment (expense_id, payment_id)
@ -72,6 +98,9 @@ values (13, 16)
, (14, 17) , (14, 17)
, (14, 18) , (14, 18)
, (15, 19) , (15, 19)
, (16, 20)
, (17, 21)
, (18, 22)
; ;
select lives_ok( select lives_ok(
@ -89,12 +118,30 @@ select lives_ok(
'Should be able to complete a previously partial payment' 'Should be able to complete a previously partial payment'
); );
select lives_ok(
$$ select edit_payment('65222c3b-4faa-4be4-b39c-5bd170a943cf', '2023-03-10', 11, 'Re: REF001', '103.99', array[]::tag_name[]) $$,
'Should be able to make partial a payment with tax.'
);
select lives_ok(
$$ select edit_payment('dbb699cf-d1f4-40ff-96cb-8f29e238d51d', '2023-03-10', 11, 'Re: REF002', '84.99', array[]::tag_name[]) $$,
'Should be able to make partial a payment with negative tax.'
);
select lives_ok(
$$ select edit_payment('0756a50f-2957-4661-abd2-e422a848af4e', '2023-03-10', 11, 'Re: REF003', '94.99', array[]::tag_name[]) $$,
'Should be able to make partial a payment with multiple taxe.'
);
select bag_eq( select bag_eq(
$$ select description, payment_date::text, payment_account_id, amount, payment_status, tags::text from payment $$, $$ select description, payment_date::text, payment_account_id, amount, payment_status, tags::text from payment $$,
$$ values ('Partial INV001', '2023-05-06', 13, 100, 'partial', '{tag1}') $$ values ('Partial INV001', '2023-05-06', 13, 100, 'partial', '{tag1}')
, ('First INV002', '2023-05-07', 12, 50, 'partial', '{tag1,tag3}') , ('First INV002', '2023-05-07', 12, 50, 'partial', '{tag1,tag3}')
, ('Second INV002', '2023-05-06', 13, 122, 'partial', '{tag1,tag3}') , ('Second INV002', '2023-05-06', 13, 122, 'partial', '{tag1,tag3}')
, ('Complete INV003', '2023-05-01', 11, 333, 'complete', '{}') , ('Complete INV003', '2023-05-01', 11, 333, 'complete', '{}')
, ('Re: REF001', '2023-03-10', 11, 10399, 'partial', '{}')
, ('Re: REF002', '2023-03-10', 11, 8499, 'partial', '{}')
, ('Re: REF003', '2023-03-10', 11, 9499, 'partial', '{}')
$$, $$,
'Should have updated all payments' 'Should have updated all payments'
); );
@ -104,6 +151,9 @@ select bag_eq(
$$ values (13, 'partial') $$ values (13, 'partial')
, (14, 'partial') , (14, 'partial')
, (15, 'paid') , (15, 'paid')
, (16, 'partial')
, (17, 'partial')
, (18, 'partial')
$$, $$,
'Should have updated expenses too' 'Should have updated expenses too'
); );

View File

@ -24,6 +24,7 @@
<tr> <tr>
<th>{{( pgettext "Payment Date" "title" )}}</th> <th>{{( pgettext "Payment Date" "title" )}}</th>
<th>{{( pgettext "Description" "title" )}}</th> <th>{{( pgettext "Description" "title" )}}</th>
<th>{{( pgettext "Document" "title" )}}</th>
<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>
@ -38,6 +39,11 @@
<tr> <tr>
<td>{{ .PaymentDate|formatDate }}</td> <td>{{ .PaymentDate|formatDate }}</td>
<td><a href="{{ companyURI "/payments/"}}{{ .Slug }}">{{ .Description }}</a></td> <td><a href="{{ companyURI "/payments/"}}{{ .Slug }}">{{ .Description }}</a></td>
<td>
{{- if .InvoiceNumber -}}
<a href="{{ companyURI "/expenses/"}}{{ .ExpenseSlug }}">{{ .InvoiceNumber }}</a>
{{- end -}}
</td>
<td class="payment-status-{{ .Status }}">{{ .StatusLabel }}</td> <td class="payment-status-{{ .Status }}">{{ .StatusLabel }}</td>
<td <td
data-hx-get="{{companyURI "/payments/"}}{{ .Slug }}/tags/edit" data-hx-get="{{companyURI "/payments/"}}{{ .Slug }}/tags/edit"
@ -89,7 +95,7 @@
{{- end }} {{- end }}
{{ else }} {{ else }}
<tr> <tr>
<td colspan="5">{{( gettext "No payments added yet." )}}</td> <td colspan="8">{{( gettext "No payments added yet." )}}</td>
</tr> </tr>
{{ end }} {{ end }}
</tbody> </tbody>