Add filters form for invoices

Instead of using links in the invoice tags, that we will replace with a
“click-to-edit field”, with Oriol agreed to add a form with filters that
includes not only the tags but also dates, customer, status, and the
invoice number.

This means i now need dynamic SQL, and i do not think this belongs to
the database (i.e., no PL/pgSQL function for that).  I have looked at
query builder libraries for Golang, and did not find anything that
suited me: either they wanted to manage not only the SQL query but also
all structs, or they managed to confuse Goland’s SQL analyzer.

For now, at least, i am using a very simple approach with arrays, that
still confuses Goland’s analyzer, but just in a very specific part,
which i find tolerable—not that their analyzer is that great to begin
with, but that’s a story for another day.
This commit is contained in:
jordi fita mas 2023-03-29 16:16:31 +02:00
parent 3f092cd0d0
commit b7881c505f
5 changed files with 300 additions and 137 deletions

View File

@ -116,10 +116,14 @@ func (field *SelectField) Scan(value interface{}) error {
} }
func (field *SelectField) Value() (driver.Value, error) { func (field *SelectField) Value() (driver.Value, error) {
return field.String(), nil
}
func (field *SelectField) String() string {
if field.Selected == nil { if field.Selected == nil {
return "", nil return ""
} }
return field.Selected[0], nil return field.Selected[0]
} }
func (field *SelectField) FillValue(r *http.Request) { func (field *SelectField) FillValue(r *http.Request) {

View File

@ -33,22 +33,55 @@ type InvoiceEntry struct {
type InvoicesIndexPage struct { type InvoicesIndexPage struct {
Invoices []*InvoiceEntry Invoices []*InvoiceEntry
Filters *invoiceFilterForm
InvoiceStatuses map[string]string InvoiceStatuses map[string]string
} }
func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r) conn := getConn(r)
locale := getLocale(r) locale := getLocale(r)
tag := r.URL.Query().Get("tag") company := mustGetCompany(r)
filters := newInvoiceFilterForm(r.Context(), conn, locale, company)
if err := filters.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
page := &InvoicesIndexPage{ page := &InvoicesIndexPage{
Invoices: mustCollectInvoiceEntries(r.Context(), conn, mustGetCompany(r), locale, tag), Invoices: mustCollectInvoiceEntries(r.Context(), conn, company, locale, filters),
Filters: filters,
InvoiceStatuses: mustCollectInvoiceStatuses(r.Context(), conn, locale), InvoiceStatuses: mustCollectInvoiceStatuses(r.Context(), conn, locale),
} }
mustRenderMainTemplate(w, r, "invoices/index.gohtml", page) mustRenderMainTemplate(w, r, "invoices/index.gohtml", page)
} }
func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale, tag string) []*InvoiceEntry { func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale, filters *invoiceFilterForm) []*InvoiceEntry {
rows := conn.MustQuery(ctx, ` args := []interface{}{locale.Language.String(), company.Id}
where := []string{"invoice.company_id = $2"}
appendWhere := func(expression string, value interface{}) {
args = append(args, value)
where = append(where, fmt.Sprintf(expression, len(args)))
}
maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) {
if value != "" {
if conv == nil {
appendWhere(expression, value)
} else {
appendWhere(expression, conv(value))
}
}
}
maybeAppendWhere("contact_id = $%d", filters.Customer.String(), func(v string) interface{} {
customerId, _ := strconv.Atoi(filters.Customer.Selected[0])
return customerId
})
maybeAppendWhere("invoice.invoice_status = $%d", filters.InvoiceStatus.String(), nil)
maybeAppendWhere("invoice_number = $%d", filters.InvoiceNumber.String(), nil)
maybeAppendWhere("invoice_date >= $%d", filters.FromDate.String(), nil)
maybeAppendWhere("invoice_date <= $%d", filters.ToDate.String(), nil)
if len(filters.Tags.Tags) > 0 {
appendWhere("exists (select 1 from invoice_tag join tag using (tag_id) where invoice_tag.invoice_id = invoice.invoice_id and tag.name = any($%d))", filters.Tags)
}
rows := conn.MustQuery(ctx, fmt.Sprintf(`
select invoice.slug select invoice.slug
, invoice_date , invoice_date
, invoice_number , invoice_number
@ -61,10 +94,10 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company
left join invoice_tag using (invoice_id) left join invoice_tag using (invoice_id)
left join tag using(tag_id) left join tag using(tag_id)
join contact using (contact_id) join contact using (contact_id)
join invoice_status_i18n isi18n on invoice.invoice_status = isi18n.invoice_status and isi18n.lang_tag = $2 join invoice_status_i18n isi18n on invoice.invoice_status = isi18n.invoice_status and isi18n.lang_tag = $1
join invoice_amount using (invoice_id) join invoice_amount using (invoice_id)
join currency using (currency_code) join currency using (currency_code)
where invoice.company_id = $1 and (($3 = '') or (tag.name = $3)) where (%s)
group by invoice.slug group by invoice.slug
, invoice_date , invoice_date
, invoice_number , invoice_number
@ -75,7 +108,7 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company
, decimal_digits , decimal_digits
order by invoice_date desc order by invoice_date desc
, invoice_number desc , invoice_number desc
`, company.Id, locale.Language.String(), tag) `, strings.Join(where, ") AND (")), args...)
defer rows.Close() defer rows.Close()
var entries []*InvoiceEntry var entries []*InvoiceEntry
@ -112,6 +145,68 @@ func mustCollectInvoiceStatuses(ctx context.Context, conn *Conn, locale *Locale)
return statuses return statuses
} }
type invoiceFilterForm struct {
locale *Locale
company *Company
Customer *SelectField
InvoiceStatus *SelectField
InvoiceNumber *InputField
FromDate *InputField
ToDate *InputField
Tags *TagsField
}
func newInvoiceFilterForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *invoiceFilterForm {
return &invoiceFilterForm{
locale: locale,
company: company,
Customer: &SelectField{
Name: "customer",
Label: pgettext("input", "Customer", locale),
EmptyLabel: gettext("All customers", locale),
Options: MustGetOptions(ctx, conn, "select contact_id::text, business_name from contact where company_id = $1 order by business_name", company.Id),
},
InvoiceStatus: &SelectField{
Name: "invoice_status",
Label: pgettext("input", "Invoice Status", locale),
EmptyLabel: gettext("All status", locale),
Options: MustGetOptions(ctx, conn, "select invoice_status.invoice_status, isi18n.name from invoice_status join invoice_status_i18n isi18n using(invoice_status) where isi18n.lang_tag = $1 order by invoice_status", locale.Language.String()),
},
InvoiceNumber: &InputField{
Name: "number",
Label: pgettext("input", "Invoice Number", locale),
Type: "text",
},
FromDate: &InputField{
Name: "from_date",
Label: pgettext("input", "From Date", locale),
Type: "date",
},
ToDate: &InputField{
Name: "to_date",
Label: pgettext("input", "To Date", locale),
Type: "date",
},
Tags: &TagsField{
Name: "tags",
Label: pgettext("input", "Tags", locale),
},
}
}
func (form *invoiceFilterForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
form.Customer.FillValue(r)
form.InvoiceStatus.FillValue(r)
form.InvoiceNumber.FillValue(r)
form.FromDate.FillValue(r)
form.ToDate.FillValue(r)
form.Tags.FillValue(r)
return nil
}
func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r) conn := getConn(r)
company := mustGetCompany(r) company := mustGetCompany(r)

151
po/ca.po
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: 2023-03-27 09:43+0200\n" "POT-Creation-Date: 2023-03-29 16:08+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"
@ -103,68 +103,73 @@ msgctxt "action"
msgid "Download invoices" msgid "Download invoices"
msgstr "Descarrega factures" msgstr "Descarrega factures"
#: web/template/invoices/index.gohtml:31 #: web/template/invoices/index.gohtml:38
msgctxt "action"
msgid "Filter"
msgstr "Filtra"
#: web/template/invoices/index.gohtml:44
msgctxt "invoice" msgctxt "invoice"
msgid "All" msgid "All"
msgstr "Totes" msgstr "Totes"
#: web/template/invoices/index.gohtml:32 web/template/invoices/view.gohtml:31 #: web/template/invoices/index.gohtml:45 web/template/invoices/view.gohtml:31
msgctxt "title" msgctxt "title"
msgid "Date" msgid "Date"
msgstr "Data" msgstr "Data"
#: web/template/invoices/index.gohtml:33 #: web/template/invoices/index.gohtml:46
msgctxt "title" msgctxt "title"
msgid "Invoice Num." msgid "Invoice Num."
msgstr "Núm. factura" msgstr "Núm. factura"
#: web/template/invoices/index.gohtml:34 web/template/contacts/index.gohtml:26 #: web/template/invoices/index.gohtml:47 web/template/contacts/index.gohtml:26
msgctxt "title" msgctxt "title"
msgid "Customer" msgid "Customer"
msgstr "Client" msgstr "Client"
#: web/template/invoices/index.gohtml:35 #: web/template/invoices/index.gohtml:48
msgctxt "title" msgctxt "title"
msgid "Status" msgid "Status"
msgstr "Estat" msgstr "Estat"
#: web/template/invoices/index.gohtml:36 web/template/contacts/index.gohtml:29 #: web/template/invoices/index.gohtml:49 web/template/contacts/index.gohtml:29
#: web/template/products/index.gohtml:27 #: web/template/products/index.gohtml:27
msgctxt "title" msgctxt "title"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
#: web/template/invoices/index.gohtml:37 #: web/template/invoices/index.gohtml:50
msgctxt "title" msgctxt "title"
msgid "Amount" msgid "Amount"
msgstr "Import" msgstr "Import"
#: web/template/invoices/index.gohtml:38 #: web/template/invoices/index.gohtml:51
msgctxt "title" msgctxt "title"
msgid "Download" msgid "Download"
msgstr "Descàrrega" msgstr "Descàrrega"
#: web/template/invoices/index.gohtml:39 #: web/template/invoices/index.gohtml:52
msgctxt "title" msgctxt "title"
msgid "Actions" msgid "Actions"
msgstr "Accions" msgstr "Accions"
#: web/template/invoices/index.gohtml:46 #: web/template/invoices/index.gohtml:59
msgctxt "action" msgctxt "action"
msgid "Select invoice %v" msgid "Select invoice %v"
msgstr "Selecciona factura %v" msgstr "Selecciona factura %v"
#: web/template/invoices/index.gohtml:95 web/template/invoices/view.gohtml:16 #: web/template/invoices/index.gohtml:109 web/template/invoices/view.gohtml:16
msgctxt "action" msgctxt "action"
msgid "Edit" msgid "Edit"
msgstr "Edita" msgstr "Edita"
#: web/template/invoices/index.gohtml:101 web/template/invoices/view.gohtml:15 #: web/template/invoices/index.gohtml:115 web/template/invoices/view.gohtml:15
msgctxt "action" msgctxt "action"
msgid "Duplicate" msgid "Duplicate"
msgstr "Duplica" msgstr "Duplica"
#: web/template/invoices/index.gohtml:111 #: web/template/invoices/index.gohtml:125
msgid "No invoices added yet." msgid "No invoices added yet."
msgstr "No hi ha cap factura." msgstr "No hi ha cap factura."
@ -451,48 +456,49 @@ 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:209 pkg/invoices.go:636 #: pkg/products.go:209 pkg/invoices.go:728
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: pkg/products.go:215 pkg/invoices.go:641 #: pkg/products.go:215 pkg/invoices.go:733
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: pkg/products.go:220 pkg/invoices.go:645 #: pkg/products.go:220 pkg/invoices.go:737
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Preu" msgstr "Preu"
#: pkg/products.go:230 pkg/invoices.go:671 #: pkg/products.go:230 pkg/invoices.go:763
msgctxt "input" msgctxt "input"
msgid "Taxes" msgid "Taxes"
msgstr "Imposts" msgstr "Imposts"
#: pkg/products.go:236 pkg/invoices.go:479 pkg/contacts.go:273 #: pkg/products.go:236 pkg/invoices.go:192 pkg/invoices.go:571
#: pkg/contacts.go:273
msgctxt "input" msgctxt "input"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
#: pkg/products.go:255 pkg/profile.go:92 pkg/invoices.go:710 #: pkg/products.go:255 pkg/profile.go:92 pkg/invoices.go:802
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc." msgstr "No podeu deixar el nom en blanc."
#: pkg/products.go:256 pkg/invoices.go:711 #: pkg/products.go:256 pkg/invoices.go:803
msgid "Price can not be empty." msgid "Price can not be empty."
msgstr "No podeu deixar el preu en blanc." msgstr "No podeu deixar el preu en blanc."
#: pkg/products.go:257 pkg/invoices.go:712 #: pkg/products.go:257 pkg/invoices.go:804
msgid "Price must be a number greater than zero." msgid "Price must be a number greater than zero."
msgstr "El preu ha de ser un número major a zero." msgstr "El preu ha de ser un número major a zero."
#: pkg/products.go:259 pkg/invoices.go:720 #: pkg/products.go:259 pkg/invoices.go:812
msgid "Selected tax is not valid." msgid "Selected tax is not valid."
msgstr "Heu seleccionat un impost que no és vàlid." msgstr "Heu seleccionat un impost que no és vàlid."
#: pkg/products.go:260 pkg/invoices.go:721 #: pkg/products.go:260 pkg/invoices.go:813
msgid "You can only select a tax of each class." msgid "You can only select a tax of each class."
msgstr "Només podeu seleccionar un impost de cada classe." msgstr "Només podeu seleccionar un impost de cada classe."
@ -600,100 +606,123 @@ 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/invoices.go:302 #: pkg/invoices.go:165 pkg/invoices.go:549
msgid "Select a customer to bill."
msgstr "Escolliu un client a facturar."
#: pkg/invoices.go:401
msgid "invoices.zip"
msgstr "factures.zip"
#: pkg/invoices.go:407 pkg/invoices.go:829
msgid "Invalid action"
msgstr "Acció invàlida."
#: pkg/invoices.go:451
msgctxt "input"
msgid "Invoice Status"
msgstr "Estat de la factura"
#: pkg/invoices.go:457
msgctxt "input" msgctxt "input"
msgid "Customer" msgid "Customer"
msgstr "Client" msgstr "Client"
#: pkg/invoices.go:463 #: pkg/invoices.go:166
msgid "All customers"
msgstr "Tots els clients"
#: pkg/invoices.go:171 pkg/invoices.go:543
msgctxt "input"
msgid "Invoice Status"
msgstr "Estat de la factura"
#: pkg/invoices.go:172
msgid "All status"
msgstr "Tots els estats"
#: pkg/invoices.go:177
msgctxt "input"
msgid "Invoice Number"
msgstr "Número de factura"
#: pkg/invoices.go:182
msgctxt "input"
msgid "From Date"
msgstr "De la data"
#: pkg/invoices.go:187
msgctxt "input"
msgid "To Date"
msgstr "A la data"
#: pkg/invoices.go:394
msgid "Select a customer to bill."
msgstr "Escolliu un client a facturar."
#: pkg/invoices.go:493
msgid "invoices.zip"
msgstr "factures.zip"
#: pkg/invoices.go:499 pkg/invoices.go:921
msgid "Invalid action"
msgstr "Acció invàlida."
#: pkg/invoices.go:555
msgctxt "input" msgctxt "input"
msgid "Number" msgid "Number"
msgstr "Número" msgstr "Número"
#: pkg/invoices.go:468 #: pkg/invoices.go:560
msgctxt "input" msgctxt "input"
msgid "Invoice Date" msgid "Invoice Date"
msgstr "Data de factura" msgstr "Data de factura"
#: pkg/invoices.go:474 #: pkg/invoices.go:566
msgctxt "input" msgctxt "input"
msgid "Notes" msgid "Notes"
msgstr "Notes" msgstr "Notes"
#: pkg/invoices.go:484 #: pkg/invoices.go:576
msgctxt "input" msgctxt "input"
msgid "Payment Method" msgid "Payment Method"
msgstr "Mètode de pagament" msgstr "Mètode de pagament"
#: pkg/invoices.go:521 #: pkg/invoices.go:613
msgid "Selected invoice status is not valid." msgid "Selected invoice status is not valid."
msgstr "Heu seleccionat un estat de factura que no és vàlid." msgstr "Heu seleccionat un estat de factura que no és vàlid."
#: pkg/invoices.go:522 #: pkg/invoices.go:614
msgid "Selected customer is not valid." msgid "Selected customer is not valid."
msgstr "Heu seleccionat un client que no és vàlid." msgstr "Heu seleccionat un client que no és vàlid."
#: pkg/invoices.go:523 #: pkg/invoices.go:615
msgid "Invoice date can not be empty." msgid "Invoice date can not be empty."
msgstr "No podeu deixar la data de la factura en blanc." msgstr "No podeu deixar la data de la factura en blanc."
#: pkg/invoices.go:524 #: pkg/invoices.go:616
msgid "Invoice date must be a valid date." msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida." msgstr "La data de facturació ha de ser vàlida."
#: pkg/invoices.go:526 #: pkg/invoices.go:618
msgid "Selected payment method is not valid." msgid "Selected payment method is not valid."
msgstr "Heu seleccionat un mètode de pagament que no és vàlid." msgstr "Heu seleccionat un mètode de pagament que no és vàlid."
#: pkg/invoices.go:626 pkg/invoices.go:631 #: pkg/invoices.go:718 pkg/invoices.go:723
msgctxt "input" msgctxt "input"
msgid "Id" msgid "Id"
msgstr "Identificador" msgstr "Identificador"
#: pkg/invoices.go:654 #: pkg/invoices.go:746
msgctxt "input" msgctxt "input"
msgid "Quantity" msgid "Quantity"
msgstr "Quantitat" msgstr "Quantitat"
#: pkg/invoices.go:662 #: pkg/invoices.go:754
msgctxt "input" msgctxt "input"
msgid "Discount (%)" msgid "Discount (%)"
msgstr "Descompte (%)" msgstr "Descompte (%)"
#: pkg/invoices.go:709 #: pkg/invoices.go:801
msgid "Product ID can not be empty." msgid "Product ID can not be empty."
msgstr "No podeu deixar lidentificador del producte en blanc." msgstr "No podeu deixar lidentificador del producte en blanc."
#: pkg/invoices.go:714 #: pkg/invoices.go:806
msgid "Quantity can not be empty." msgid "Quantity can not be empty."
msgstr "No podeu deixar la quantitat en blanc." msgstr "No podeu deixar la quantitat en blanc."
#: pkg/invoices.go:715 #: pkg/invoices.go:807
msgid "Quantity must be a number greater than zero." msgid "Quantity must be a number greater than zero."
msgstr "La quantitat ha de ser un número major a zero." msgstr "La quantitat ha de ser un número major a zero."
#: pkg/invoices.go:717 #: pkg/invoices.go:809
msgid "Discount can not be empty." msgid "Discount can not be empty."
msgstr "No podeu deixar el descompte en blanc." msgstr "No podeu deixar el descompte en blanc."
#: pkg/invoices.go:718 #: pkg/invoices.go:810
msgid "Discount must be a percentage between 0 and 100." msgid "Discount must be a percentage between 0 and 100."
msgstr "El descompte ha de ser un percentatge entre 0 i 100." msgstr "El descompte ha de ser un percentatge entre 0 i 100."
@ -806,10 +835,6 @@ msgstr "Aquest valor no és un codi postal vàlid."
#~ msgid "Tax" #~ msgid "Tax"
#~ msgstr "Impost" #~ msgstr "Impost"
#~ msgctxt "nav"
#~ msgid "Customers"
#~ msgstr "Clients"
#~ msgctxt "title" #~ msgctxt "title"
#~ msgid "Customers" #~ msgid "Customers"
#~ msgstr "Clients" #~ msgstr "Clients"

151
po/es.po
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: 2023-03-27 09:43+0200\n" "POT-Creation-Date: 2023-03-29 16:08+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"
@ -103,68 +103,73 @@ msgctxt "action"
msgid "Download invoices" msgid "Download invoices"
msgstr "Descargar facturas" msgstr "Descargar facturas"
#: web/template/invoices/index.gohtml:31 #: web/template/invoices/index.gohtml:38
msgctxt "action"
msgid "Filter"
msgstr "Filtrar"
#: web/template/invoices/index.gohtml:44
msgctxt "invoice" msgctxt "invoice"
msgid "All" msgid "All"
msgstr "Todas" msgstr "Todas"
#: web/template/invoices/index.gohtml:32 web/template/invoices/view.gohtml:31 #: web/template/invoices/index.gohtml:45 web/template/invoices/view.gohtml:31
msgctxt "title" msgctxt "title"
msgid "Date" msgid "Date"
msgstr "Fecha" msgstr "Fecha"
#: web/template/invoices/index.gohtml:33 #: web/template/invoices/index.gohtml:46
msgctxt "title" msgctxt "title"
msgid "Invoice Num." msgid "Invoice Num."
msgstr "Nº factura" msgstr "Nº factura"
#: web/template/invoices/index.gohtml:34 web/template/contacts/index.gohtml:26 #: web/template/invoices/index.gohtml:47 web/template/contacts/index.gohtml:26
msgctxt "title" msgctxt "title"
msgid "Customer" msgid "Customer"
msgstr "Cliente" msgstr "Cliente"
#: web/template/invoices/index.gohtml:35 #: web/template/invoices/index.gohtml:48
msgctxt "title" msgctxt "title"
msgid "Status" msgid "Status"
msgstr "Estado" msgstr "Estado"
#: web/template/invoices/index.gohtml:36 web/template/contacts/index.gohtml:29 #: web/template/invoices/index.gohtml:49 web/template/contacts/index.gohtml:29
#: web/template/products/index.gohtml:27 #: web/template/products/index.gohtml:27
msgctxt "title" msgctxt "title"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
#: web/template/invoices/index.gohtml:37 #: web/template/invoices/index.gohtml:50
msgctxt "title" msgctxt "title"
msgid "Amount" msgid "Amount"
msgstr "Importe" msgstr "Importe"
#: web/template/invoices/index.gohtml:38 #: web/template/invoices/index.gohtml:51
msgctxt "title" msgctxt "title"
msgid "Download" msgid "Download"
msgstr "Descargar" msgstr "Descargar"
#: web/template/invoices/index.gohtml:39 #: web/template/invoices/index.gohtml:52
msgctxt "title" msgctxt "title"
msgid "Actions" msgid "Actions"
msgstr "Acciones" msgstr "Acciones"
#: web/template/invoices/index.gohtml:46 #: web/template/invoices/index.gohtml:59
msgctxt "action" msgctxt "action"
msgid "Select invoice %v" msgid "Select invoice %v"
msgstr "Seleccionar factura %v" msgstr "Seleccionar factura %v"
#: web/template/invoices/index.gohtml:95 web/template/invoices/view.gohtml:16 #: web/template/invoices/index.gohtml:109 web/template/invoices/view.gohtml:16
msgctxt "action" msgctxt "action"
msgid "Edit" msgid "Edit"
msgstr "Editar" msgstr "Editar"
#: web/template/invoices/index.gohtml:101 web/template/invoices/view.gohtml:15 #: web/template/invoices/index.gohtml:115 web/template/invoices/view.gohtml:15
msgctxt "action" msgctxt "action"
msgid "Duplicate" msgid "Duplicate"
msgstr "Duplicar" msgstr "Duplicar"
#: web/template/invoices/index.gohtml:111 #: web/template/invoices/index.gohtml:125
msgid "No invoices added yet." msgid "No invoices added yet."
msgstr "No hay facturas." msgstr "No hay facturas."
@ -451,48 +456,49 @@ 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:209 pkg/invoices.go:636 #: pkg/products.go:209 pkg/invoices.go:728
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: pkg/products.go:215 pkg/invoices.go:641 #: pkg/products.go:215 pkg/invoices.go:733
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripción" msgstr "Descripción"
#: pkg/products.go:220 pkg/invoices.go:645 #: pkg/products.go:220 pkg/invoices.go:737
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Precio" msgstr "Precio"
#: pkg/products.go:230 pkg/invoices.go:671 #: pkg/products.go:230 pkg/invoices.go:763
msgctxt "input" msgctxt "input"
msgid "Taxes" msgid "Taxes"
msgstr "Impuestos" msgstr "Impuestos"
#: pkg/products.go:236 pkg/invoices.go:479 pkg/contacts.go:273 #: pkg/products.go:236 pkg/invoices.go:192 pkg/invoices.go:571
#: pkg/contacts.go:273
msgctxt "input" msgctxt "input"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
#: pkg/products.go:255 pkg/profile.go:92 pkg/invoices.go:710 #: pkg/products.go:255 pkg/profile.go:92 pkg/invoices.go:802
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco." msgstr "No podéis dejar el nombre en blanco."
#: pkg/products.go:256 pkg/invoices.go:711 #: pkg/products.go:256 pkg/invoices.go:803
msgid "Price can not be empty." msgid "Price can not be empty."
msgstr "No podéis dejar el precio en blanco." msgstr "No podéis dejar el precio en blanco."
#: pkg/products.go:257 pkg/invoices.go:712 #: pkg/products.go:257 pkg/invoices.go:804
msgid "Price must be a number greater than zero." msgid "Price must be a number greater than zero."
msgstr "El precio tiene que ser un número mayor a cero." msgstr "El precio tiene que ser un número mayor a cero."
#: pkg/products.go:259 pkg/invoices.go:720 #: pkg/products.go:259 pkg/invoices.go:812
msgid "Selected tax is not valid." msgid "Selected tax is not valid."
msgstr "Habéis escogido un impuesto que no es válido." msgstr "Habéis escogido un impuesto que no es válido."
#: pkg/products.go:260 pkg/invoices.go:721 #: pkg/products.go:260 pkg/invoices.go:813
msgid "You can only select a tax of each class." msgid "You can only select a tax of each class."
msgstr "Solo podéis escoger un impuesto de cada clase." msgstr "Solo podéis escoger un impuesto de cada clase."
@ -600,100 +606,123 @@ 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/invoices.go:302 #: pkg/invoices.go:165 pkg/invoices.go:549
msgid "Select a customer to bill."
msgstr "Escoged un cliente a facturar."
#: pkg/invoices.go:401
msgid "invoices.zip"
msgstr "facturas.zip"
#: pkg/invoices.go:407 pkg/invoices.go:829
msgid "Invalid action"
msgstr "Acción inválida."
#: pkg/invoices.go:451
msgctxt "input"
msgid "Invoice Status"
msgstr "Estado de la factura"
#: pkg/invoices.go:457
msgctxt "input" msgctxt "input"
msgid "Customer" msgid "Customer"
msgstr "Cliente" msgstr "Cliente"
#: pkg/invoices.go:463 #: pkg/invoices.go:166
msgid "All customers"
msgstr "Todos los clientes"
#: pkg/invoices.go:171 pkg/invoices.go:543
msgctxt "input"
msgid "Invoice Status"
msgstr "Estado de la factura"
#: pkg/invoices.go:172
msgid "All status"
msgstr "Todos los estados"
#: pkg/invoices.go:177
msgctxt "input"
msgid "Invoice Number"
msgstr "Número de factura"
#: pkg/invoices.go:182
msgctxt "input"
msgid "From Date"
msgstr "De la fecha"
#: pkg/invoices.go:187
msgctxt "input"
msgid "To Date"
msgstr "A la fecha"
#: pkg/invoices.go:394
msgid "Select a customer to bill."
msgstr "Escoged un cliente a facturar."
#: pkg/invoices.go:493
msgid "invoices.zip"
msgstr "facturas.zip"
#: pkg/invoices.go:499 pkg/invoices.go:921
msgid "Invalid action"
msgstr "Acción inválida."
#: pkg/invoices.go:555
msgctxt "input" msgctxt "input"
msgid "Number" msgid "Number"
msgstr "Número" msgstr "Número"
#: pkg/invoices.go:468 #: pkg/invoices.go:560
msgctxt "input" msgctxt "input"
msgid "Invoice Date" msgid "Invoice Date"
msgstr "Fecha de factura" msgstr "Fecha de factura"
#: pkg/invoices.go:474 #: pkg/invoices.go:566
msgctxt "input" msgctxt "input"
msgid "Notes" msgid "Notes"
msgstr "Notas" msgstr "Notas"
#: pkg/invoices.go:484 #: pkg/invoices.go:576
msgctxt "input" msgctxt "input"
msgid "Payment Method" msgid "Payment Method"
msgstr "Método de pago" msgstr "Método de pago"
#: pkg/invoices.go:521 #: pkg/invoices.go:613
msgid "Selected invoice status is not valid." msgid "Selected invoice status is not valid."
msgstr "Habéis escogido un estado de factura que no es válido." msgstr "Habéis escogido un estado de factura que no es válido."
#: pkg/invoices.go:522 #: pkg/invoices.go:614
msgid "Selected customer is not valid." msgid "Selected customer is not valid."
msgstr "Habéis escogido un cliente que no es válido." msgstr "Habéis escogido un cliente que no es válido."
#: pkg/invoices.go:523 #: pkg/invoices.go:615
msgid "Invoice date can not be empty." msgid "Invoice date can not be empty."
msgstr "No podéis dejar la fecha de la factura en blanco." msgstr "No podéis dejar la fecha de la factura en blanco."
#: pkg/invoices.go:524 #: pkg/invoices.go:616
msgid "Invoice date must be a valid date." msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida." msgstr "La fecha de factura debe ser válida."
#: pkg/invoices.go:526 #: pkg/invoices.go:618
msgid "Selected payment method is not valid." msgid "Selected payment method is not valid."
msgstr "Habéis escogido un método de pago que no es válido." msgstr "Habéis escogido un método de pago que no es válido."
#: pkg/invoices.go:626 pkg/invoices.go:631 #: pkg/invoices.go:718 pkg/invoices.go:723
msgctxt "input" msgctxt "input"
msgid "Id" msgid "Id"
msgstr "Identificador" msgstr "Identificador"
#: pkg/invoices.go:654 #: pkg/invoices.go:746
msgctxt "input" msgctxt "input"
msgid "Quantity" msgid "Quantity"
msgstr "Cantidad" msgstr "Cantidad"
#: pkg/invoices.go:662 #: pkg/invoices.go:754
msgctxt "input" msgctxt "input"
msgid "Discount (%)" msgid "Discount (%)"
msgstr "Descuento (%)" msgstr "Descuento (%)"
#: pkg/invoices.go:709 #: pkg/invoices.go:801
msgid "Product ID can not be empty." msgid "Product ID can not be empty."
msgstr "No podéis dejar el identificador de producto en blanco." msgstr "No podéis dejar el identificador de producto en blanco."
#: pkg/invoices.go:714 #: pkg/invoices.go:806
msgid "Quantity can not be empty." msgid "Quantity can not be empty."
msgstr "No podéis dejar la cantidad en blanco." msgstr "No podéis dejar la cantidad en blanco."
#: pkg/invoices.go:715 #: pkg/invoices.go:807
msgid "Quantity must be a number greater than zero." msgid "Quantity must be a number greater than zero."
msgstr "La cantidad tiene que ser un número mayor a cero." msgstr "La cantidad tiene que ser un número mayor a cero."
#: pkg/invoices.go:717 #: pkg/invoices.go:809
msgid "Discount can not be empty." msgid "Discount can not be empty."
msgstr "No podéis dejar el descuento en blanco." msgstr "No podéis dejar el descuento en blanco."
#: pkg/invoices.go:718 #: pkg/invoices.go:810
msgid "Discount must be a percentage between 0 and 100." msgid "Discount must be a percentage between 0 and 100."
msgstr "El descuento tiene que ser un porcentaje entre 0 y 100." msgstr "El descuento tiene que ser un porcentaje entre 0 y 100."
@ -806,10 +835,6 @@ msgstr "Este valor no es un código postal válido válido."
#~ msgid "Tax" #~ msgid "Tax"
#~ msgstr "Impuesto" #~ msgstr "Impuesto"
#~ msgctxt "nav"
#~ msgid "Customers"
#~ msgstr "Clientes"
#~ msgctxt "title" #~ msgctxt "title"
#~ msgid "Customers" #~ msgid "Customers"
#~ msgstr "Clientes" #~ msgstr "Clientes"

View File

@ -25,6 +25,19 @@
{{ define "content" }} {{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InvoicesIndexPage*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InvoicesIndexPage*/ -}}
<div aria-label="{{( pgettext "Filters" "title" )}}">
<form method="GET" action="{{ companyURI "/invoices"}}" data-hx-target="main" data-hx-boost="true">
{{ with .Filters }}
{{ template "select-field" .Customer }}
{{ template "select-field" .InvoiceStatus }}
{{ template "input-field" .FromDate }}
{{ template "input-field" .ToDate }}
{{ template "input-field" .InvoiceNumber }}
{{ template "tags-field" .Tags }}
{{ end }}
<button type="submit">{{( pgettext "Filter" "action" )}}</button>
</form>
</div>
<table class="no-padding"> <table class="no-padding">
<thead> <thead>
<tr> <tr>
@ -49,7 +62,8 @@
aria-label="{{ $title }}" aria-label="{{ $title }}"
title="{{ $title }}"/></td> title="{{ $title }}"/></td>
<td>{{ .Date|formatDate }}</td> <td>{{ .Date|formatDate }}</td>
<td><a href="{{ companyURI "/invoices/"}}{{ .Slug }}" data-hx-target="main" data-hx-boost="true">{{ .Number }}</a></td> <td><a href="{{ companyURI "/invoices/"}}{{ .Slug }}" data-hx-target="main"
data-hx-boost="true">{{ .Number }}</a></td>
<td>{{ .CustomerName }}</td> <td>{{ .CustomerName }}</td>
<td> <td>
<details class="invoice-status menu"> <details class="invoice-status menu">
@ -76,7 +90,7 @@
<td> <td>
{{- range $index, $tag := .Tags }} {{- range $index, $tag := .Tags }}
{{- if gt $index 0 }}, {{ end -}} {{- if gt $index 0 }}, {{ end -}}
<a href="?tag={{ . }}" data-hx-target="main" data-hx-boost="true">{{ . }}</a> {{ . }}
{{- end }} {{- end }}
</td> </td>
<td class="numeric">{{ .Total|formatPrice }}</td> <td class="numeric">{{ .Total|formatPrice }}</td>