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) {
return field.String(), nil
}
func (field *SelectField) String() string {
if field.Selected == nil {
return "", nil
return ""
}
return field.Selected[0], nil
return field.Selected[0]
}
func (field *SelectField) FillValue(r *http.Request) {

View File

@ -33,22 +33,55 @@ type InvoiceEntry struct {
type InvoicesIndexPage struct {
Invoices []*InvoiceEntry
Filters *invoiceFilterForm
InvoiceStatuses map[string]string
}
func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(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{
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),
}
mustRenderMainTemplate(w, r, "invoices/index.gohtml", page)
}
func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale, tag string) []*InvoiceEntry {
rows := conn.MustQuery(ctx, `
func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale, filters *invoiceFilterForm) []*InvoiceEntry {
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
, invoice_date
, invoice_number
@ -61,10 +94,10 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company
left join invoice_tag using (invoice_id)
left join tag using(tag_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 currency using (currency_code)
where invoice.company_id = $1 and (($3 = '') or (tag.name = $3))
where (%s)
group by invoice.slug
, invoice_date
, invoice_number
@ -75,7 +108,7 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company
, decimal_digits
order by invoice_date desc
, invoice_number desc
`, company.Id, locale.Language.String(), tag)
`, strings.Join(where, ") AND (")), args...)
defer rows.Close()
var entries []*InvoiceEntry
@ -112,6 +145,68 @@ func mustCollectInvoiceStatuses(ctx context.Context, conn *Conn, locale *Locale)
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) {
conn := getConn(r)
company := mustGetCompany(r)

151
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\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"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n"
@ -103,68 +103,73 @@ msgctxt "action"
msgid "Download invoices"
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"
msgid "All"
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"
msgid "Date"
msgstr "Data"
#: web/template/invoices/index.gohtml:33
#: web/template/invoices/index.gohtml:46
msgctxt "title"
msgid "Invoice Num."
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"
msgid "Customer"
msgstr "Client"
#: web/template/invoices/index.gohtml:35
#: web/template/invoices/index.gohtml:48
msgctxt "title"
msgid "Status"
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
msgctxt "title"
msgid "Tags"
msgstr "Etiquetes"
#: web/template/invoices/index.gohtml:37
#: web/template/invoices/index.gohtml:50
msgctxt "title"
msgid "Amount"
msgstr "Import"
#: web/template/invoices/index.gohtml:38
#: web/template/invoices/index.gohtml:51
msgctxt "title"
msgid "Download"
msgstr "Descàrrega"
#: web/template/invoices/index.gohtml:39
#: web/template/invoices/index.gohtml:52
msgctxt "title"
msgid "Actions"
msgstr "Accions"
#: web/template/invoices/index.gohtml:46
#: web/template/invoices/index.gohtml:59
msgctxt "action"
msgid "Select invoice %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"
msgid "Edit"
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"
msgid "Duplicate"
msgstr "Duplica"
#: web/template/invoices/index.gohtml:111
#: web/template/invoices/index.gohtml:125
msgid "No invoices added yet."
msgstr "No hi ha cap factura."
@ -451,48 +456,49 @@ msgstr "No podeu deixar la contrasenya en blanc."
msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/products.go:209 pkg/invoices.go:636
#: pkg/products.go:209 pkg/invoices.go:728
msgctxt "input"
msgid "Name"
msgstr "Nom"
#: pkg/products.go:215 pkg/invoices.go:641
#: pkg/products.go:215 pkg/invoices.go:733
msgctxt "input"
msgid "Description"
msgstr "Descripció"
#: pkg/products.go:220 pkg/invoices.go:645
#: pkg/products.go:220 pkg/invoices.go:737
msgctxt "input"
msgid "Price"
msgstr "Preu"
#: pkg/products.go:230 pkg/invoices.go:671
#: pkg/products.go:230 pkg/invoices.go:763
msgctxt "input"
msgid "Taxes"
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"
msgid "Tags"
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."
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."
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."
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."
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."
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."
msgstr "Heu seleccionat un idioma que no és vàlid."
#: pkg/invoices.go:302
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
#: pkg/invoices.go:165 pkg/invoices.go:549
msgctxt "input"
msgid "Customer"
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"
msgid "Number"
msgstr "Número"
#: pkg/invoices.go:468
#: pkg/invoices.go:560
msgctxt "input"
msgid "Invoice Date"
msgstr "Data de factura"
#: pkg/invoices.go:474
#: pkg/invoices.go:566
msgctxt "input"
msgid "Notes"
msgstr "Notes"
#: pkg/invoices.go:484
#: pkg/invoices.go:576
msgctxt "input"
msgid "Payment Method"
msgstr "Mètode de pagament"
#: pkg/invoices.go:521
#: pkg/invoices.go:613
msgid "Selected invoice status is not valid."
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."
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."
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."
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."
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"
msgid "Id"
msgstr "Identificador"
#: pkg/invoices.go:654
#: pkg/invoices.go:746
msgctxt "input"
msgid "Quantity"
msgstr "Quantitat"
#: pkg/invoices.go:662
#: pkg/invoices.go:754
msgctxt "input"
msgid "Discount (%)"
msgstr "Descompte (%)"
#: pkg/invoices.go:709
#: pkg/invoices.go:801
msgid "Product ID can not be empty."
msgstr "No podeu deixar lidentificador del producte en blanc."
#: pkg/invoices.go:714
#: pkg/invoices.go:806
msgid "Quantity can not be empty."
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."
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."
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."
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"
#~ msgstr "Impost"
#~ msgctxt "nav"
#~ msgid "Customers"
#~ msgstr "Clients"
#~ msgctxt "title"
#~ msgid "Customers"
#~ msgstr "Clients"

151
po/es.po
View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\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"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
@ -103,68 +103,73 @@ msgctxt "action"
msgid "Download invoices"
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"
msgid "All"
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"
msgid "Date"
msgstr "Fecha"
#: web/template/invoices/index.gohtml:33
#: web/template/invoices/index.gohtml:46
msgctxt "title"
msgid "Invoice Num."
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"
msgid "Customer"
msgstr "Cliente"
#: web/template/invoices/index.gohtml:35
#: web/template/invoices/index.gohtml:48
msgctxt "title"
msgid "Status"
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
msgctxt "title"
msgid "Tags"
msgstr "Etiquetes"
#: web/template/invoices/index.gohtml:37
#: web/template/invoices/index.gohtml:50
msgctxt "title"
msgid "Amount"
msgstr "Importe"
#: web/template/invoices/index.gohtml:38
#: web/template/invoices/index.gohtml:51
msgctxt "title"
msgid "Download"
msgstr "Descargar"
#: web/template/invoices/index.gohtml:39
#: web/template/invoices/index.gohtml:52
msgctxt "title"
msgid "Actions"
msgstr "Acciones"
#: web/template/invoices/index.gohtml:46
#: web/template/invoices/index.gohtml:59
msgctxt "action"
msgid "Select invoice %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"
msgid "Edit"
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"
msgid "Duplicate"
msgstr "Duplicar"
#: web/template/invoices/index.gohtml:111
#: web/template/invoices/index.gohtml:125
msgid "No invoices added yet."
msgstr "No hay facturas."
@ -451,48 +456,49 @@ msgstr "No podéis dejar la contraseña en blanco."
msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido."
#: pkg/products.go:209 pkg/invoices.go:636
#: pkg/products.go:209 pkg/invoices.go:728
msgctxt "input"
msgid "Name"
msgstr "Nombre"
#: pkg/products.go:215 pkg/invoices.go:641
#: pkg/products.go:215 pkg/invoices.go:733
msgctxt "input"
msgid "Description"
msgstr "Descripción"
#: pkg/products.go:220 pkg/invoices.go:645
#: pkg/products.go:220 pkg/invoices.go:737
msgctxt "input"
msgid "Price"
msgstr "Precio"
#: pkg/products.go:230 pkg/invoices.go:671
#: pkg/products.go:230 pkg/invoices.go:763
msgctxt "input"
msgid "Taxes"
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"
msgid "Tags"
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."
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."
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."
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."
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."
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."
msgstr "Habéis escogido un idioma que no es válido."
#: pkg/invoices.go:302
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
#: pkg/invoices.go:165 pkg/invoices.go:549
msgctxt "input"
msgid "Customer"
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"
msgid "Number"
msgstr "Número"
#: pkg/invoices.go:468
#: pkg/invoices.go:560
msgctxt "input"
msgid "Invoice Date"
msgstr "Fecha de factura"
#: pkg/invoices.go:474
#: pkg/invoices.go:566
msgctxt "input"
msgid "Notes"
msgstr "Notas"
#: pkg/invoices.go:484
#: pkg/invoices.go:576
msgctxt "input"
msgid "Payment Method"
msgstr "Método de pago"
#: pkg/invoices.go:521
#: pkg/invoices.go:613
msgid "Selected invoice status is not valid."
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."
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."
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."
msgstr "La fecha de factura debe ser válida."
#: pkg/invoices.go:526
#: pkg/invoices.go:618
msgid "Selected payment method is not valid."
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"
msgid "Id"
msgstr "Identificador"
#: pkg/invoices.go:654
#: pkg/invoices.go:746
msgctxt "input"
msgid "Quantity"
msgstr "Cantidad"
#: pkg/invoices.go:662
#: pkg/invoices.go:754
msgctxt "input"
msgid "Discount (%)"
msgstr "Descuento (%)"
#: pkg/invoices.go:709
#: pkg/invoices.go:801
msgid "Product ID can not be empty."
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."
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."
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."
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."
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"
#~ msgstr "Impuesto"
#~ msgctxt "nav"
#~ msgid "Customers"
#~ msgstr "Clientes"
#~ msgctxt "title"
#~ msgid "Customers"
#~ msgstr "Clientes"

View File

@ -25,6 +25,19 @@
{{ define "content" }}
{{- /*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">
<thead>
<tr>
@ -49,7 +62,8 @@
aria-label="{{ $title }}"
title="{{ $title }}"/></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>
<details class="invoice-status menu">
@ -76,7 +90,7 @@
<td>
{{- range $index, $tag := .Tags }}
{{- if gt $index 0 }}, {{ end -}}
<a href="?tag={{ . }}" data-hx-target="main" data-hx-boost="true">{{ . }}</a>
{{ . }}
{{- end }}
</td>
<td class="numeric">{{ .Total|formatPrice }}</td>