From 4903c8a3b90efaff4ee1d074c2ac27cf33a4833e Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Sun, 12 Feb 2023 21:06:48 +0100 Subject: [PATCH] Add the form to add products to an invoice and create invoices too Still missing: the invoice number, that requires more tables and possibly a PL/pgSQL function to do it properly. --- pkg/db.go | 8 + pkg/form.go | 15 ++ pkg/invoices.go | 290 ++++++++++++++++++++++++-- pkg/products.go | 2 +- pkg/router.go | 1 + pkg/template.go | 4 + po/ca.po | 230 ++++++++++++++++---- po/es.po | 230 ++++++++++++++++---- web/template/form.gohtml | 16 +- web/template/invoices/index.gohtml | 20 +- web/template/invoices/new.gohtml | 5 +- web/template/invoices/products.gohtml | 68 ++++++ 12 files changed, 775 insertions(+), 114 deletions(-) create mode 100644 web/template/invoices/products.gohtml diff --git a/pkg/db.go b/pkg/db.go index 6c59468..ff65f72 100644 --- a/pkg/db.go +++ b/pkg/db.go @@ -96,6 +96,14 @@ func (c *Conn) MustExec(ctx context.Context, sql string, args ...interface{}) { } } +func (c *Conn) MustQuery(ctx context.Context, sql string, args ...interface{}) pgx.Rows { + rows, err := c.Conn.Query(ctx, sql, args...) + if err != nil { + panic(err) + } + return rows +} + type Tx struct { pgx.Tx } diff --git a/pkg/form.go b/pkg/form.go index 44d05d5..b8e84bd 100644 --- a/pkg/form.go +++ b/pkg/form.go @@ -5,6 +5,7 @@ import ( "database/sql/driver" "errors" "fmt" + "github.com/jackc/pgtype" "html/template" "net/http" "net/mail" @@ -12,6 +13,7 @@ import ( "regexp" "strconv" "strings" + "time" ) type Attribute struct { @@ -72,6 +74,14 @@ func (field *SelectField) Scan(value interface{}) error { field.Selected = append(field.Selected, "") return nil } + if str, ok := value.(string); ok { + if array, err := pgtype.ParseUntypedTextArray(str); err == nil { + for _, element := range array.Elements { + field.Selected = append(field.Selected, element) + } + return nil + } + } field.Selected = append(field.Selected, fmt.Sprintf("%v", value)) return nil } @@ -185,6 +195,11 @@ func (v *FormValidator) CheckValidURL(field *InputField, message string) bool { return v.checkInput(field, err == nil, message) } +func (v *FormValidator) CheckValidDate(field *InputField, message string) bool { + _, err := time.Parse("2006-02-01", field.Val) + return v.checkInput(field, err == nil, message) +} + func (v *FormValidator) CheckValidPostalCode(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool { pattern := "^" + conn.MustGetText(ctx, ".{1,255}", "select postal_code_regex from country where country_code = $1", country) + "$" match, err := regexp.MatchString(pattern, field.Val) diff --git a/pkg/invoices.go b/pkg/invoices.go index 6dabede..4010d54 100644 --- a/pkg/invoices.go +++ b/pkg/invoices.go @@ -3,20 +3,59 @@ package pkg import ( "context" "fmt" + "github.com/jackc/pgx/v4" "github.com/julienschmidt/httprouter" "html/template" + "math" "net/http" + "strconv" "time" ) +type InvoiceEntry struct { + Slug string + Date time.Time + Number string + CustomerName string + CustomerSlug string + Status string + StatusLabel string +} + type InvoicesIndexPage struct { + Invoices []*InvoiceEntry } func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - page := &InvoicesIndexPage{} + page := &InvoicesIndexPage{ + Invoices: mustGetInvoiceEntries(r.Context(), getConn(r), mustGetCompany(r), getLocale(r)), + } mustRenderAppTemplate(w, r, "invoices/index.gohtml", page) } +func mustGetInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale) []*InvoiceEntry { + rows, err := conn.Query(ctx, "select invoice.slug, invoice_date, invoice_number, contact.business_name, contact.slug, invoice.invoice_status, isi18n.name from invoice join contact using (contact_id) join invoice_status_i18n isi18n on invoice.invoice_status = isi18n.invoice_status and isi18n.lang_tag = $2 where invoice.company_id = $1 order by invoice_date, invoice_number", company.Id, locale.Language.String()) + if err != nil { + panic(err) + } + defer rows.Close() + + var entries []*InvoiceEntry + for rows.Next() { + entry := &InvoiceEntry{} + err = rows.Scan(&entry.Slug, &entry.Date, &entry.Number, &entry.CustomerName, &entry.CustomerSlug, &entry.Status, &entry.StatusLabel) + if err != nil { + panic(err) + } + entries = append(entries, entry) + } + if rows.Err() != nil { + panic(rows.Err()) + } + + return entries +} + func GetInvoiceForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) { locale := getLocale(r) conn := getConn(r) @@ -36,19 +75,123 @@ func mustRenderNewInvoiceForm(w http.ResponseWriter, r *http.Request, form *invo mustRenderAppTemplate(w, r, "invoices/new.gohtml", form) } -func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - r.ParseForm() - w.Write([]byte("OK")) +func mustRenderNewInvoiceProductsForm(w http.ResponseWriter, r *http.Request, form *invoiceForm) { + conn := getConn(r) + company := mustGetCompany(r) + page := newInvoiceProductsPage{ + Form: form, + Products: mustGetProductChoices(r.Context(), conn, company), + } + mustRenderAppTemplate(w, r, "invoices/products.gohtml", page) } -type invoiceProductForm struct { - ProductId *InputField - Name *InputField - Description *InputField - Price *InputField - Quantity *InputField - Discount *InputField - Tax *SelectField +func mustGetProductChoices(ctx context.Context, conn *Conn, company *Company) []*productChoice { + rows := conn.MustQuery(ctx, "select product.product_id, product.name, to_price(price, decimal_digits) from product join company using (company_id) join currency using (currency_code) where company_id = $1 order by name", company.Id) + defer rows.Close() + + var choices []*productChoice + for rows.Next() { + entry := &productChoice{} + if err := rows.Scan(&entry.Id, &entry.Name, &entry.Price); err != nil { + panic(err) + } + choices = append(choices, entry) + } + if rows.Err() != nil { + panic(rows.Err()) + } + + return choices +} + +type newInvoiceProductsPage struct { + Form *invoiceForm + Products []*productChoice +} + +type productChoice struct { + Id int + Name string + Price string +} + +func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + locale := getLocale(r) + conn := getConn(r) + company := mustGetCompany(r) + form := newInvoiceForm(r.Context(), conn, locale, company) + 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 + } + switch r.Form.Get("action") { + case "update": + form.Update() + w.WriteHeader(http.StatusOK) + mustRenderNewInvoiceForm(w, r, form) + case "products": + w.WriteHeader(http.StatusOK) + mustRenderNewInvoiceProductsForm(w, r, form) + case "add": + if !form.Validate() { + w.WriteHeader(http.StatusUnprocessableEntity) + mustRenderNewInvoiceForm(w, r, form) + return + } + tx := conn.MustBegin(r.Context()) + invoiceId := tx.MustGetInteger(r.Context(), "insert into invoice (company_id, invoice_number, invoice_date, contact_id, notes, currency_code) select company_id, $2, $3, $4, $5, currency_code from company join currency using (currency_code) where company_id = $1 returning invoice_id", company.Id, form.Number, form.Date, form.Customer, form.Notes) + batch := &pgx.Batch{} + for _, product := range form.Products { + batch.Queue("insert into invoice_product(invoice_id, product_id, name, description, price, quantity, discount_rate) select $2, $3, $4, $5, parse_price($6, decimal_digits), $7, $8 / 100::decimal from company join currency using (currency_code) where company_id = $1", company.Id, invoiceId, product.ProductId, product.Name, product.Description, product.Price, product.Quantity.Integer(), product.Discount.Integer()) + } + br := tx.SendBatch(r.Context(), batch) + for range form.Products { + if _, err := br.Exec(); err != nil { + panic(err) + } + } + if err := br.Close(); err != nil { + panic(err) + } + tx.MustCommit(r.Context()) + http.Redirect(w, r, companyURI(company, "/invoices"), http.StatusSeeOther) + default: + http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest) + } +} + +func HandleAddProductsToInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + locale := getLocale(r) + conn := getConn(r) + company := mustGetCompany(r) + form := newInvoiceForm(r.Context(), conn, locale, company) + if err := form.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + index := len(form.Products) + productsId := r.Form["id"] + rows := conn.MustQuery(r.Context(), "select product_id, name, description, to_price(price, decimal_digits), 1 as quantity, 0 as discount, array_agg(tax_id) from product join company using (company_id) join currency using (currency_code) left join product_tax using (product_id) where product_id = any ($1) group by product_id, name, description, price, decimal_digits", productsId) + defer rows.Close() + for rows.Next() { + product := newInvoiceProductForm(index, company, locale, form.Tax.Options) + if err := rows.Scan(product.ProductId, product.Name, product.Description, product.Price, product.Quantity, product.Discount, product.Tax); err != nil { + panic(err) + } + form.Products = append(form.Products, product) + index++ + } + if rows.Err() != nil { + panic(rows.Err()) + } + + w.WriteHeader(http.StatusOK) + mustRenderNewInvoiceForm(w, r, form) } type invoiceForm struct { @@ -58,6 +201,7 @@ type invoiceForm struct { Number *InputField Date *InputField Notes *InputField + Tax *SelectField Products []*invoiceProductForm } @@ -88,33 +232,103 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co Label: pgettext("input", "Notes", locale), Type: "textarea", }, - Products: []*invoiceProductForm{ - newInvoiceProductForm(ctx, conn, company, locale), + Tax: &SelectField{ + Name: "text", + Label: pgettext("input", "Taxes", locale), + Multiple: true, + Options: mustGetTaxOptions(ctx, conn, company), }, } } -func newInvoiceProductForm(ctx context.Context, conn *Conn, company *Company, locale *Locale) *invoiceProductForm { +func (form *invoiceForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + form.Customer.FillValue(r) + form.Number.FillValue(r) + form.Date.FillValue(r) + form.Notes.FillValue(r) + if r.Form.Has("product.id.0") { + for index := 0; r.Form.Has("product.id." + strconv.Itoa(index)); index++ { + productForm := newInvoiceProductForm(index, form.company, form.locale, form.Tax.Options) + if err := productForm.Parse(r); err != nil { + return err + } + form.Products = append(form.Products, productForm) + } + } + return nil +} + +func (form *invoiceForm) Validate() bool { + validator := newFormValidator() + + validator.CheckValidSelectOption(form.Customer, gettext("Name can not be empty.", form.locale)) + if validator.CheckRequiredInput(form.Date, gettext("Invoice date can not be empty.", form.locale)) { + validator.CheckValidDate(form.Date, gettext("Invoice date must be a valid date.", form.locale)) + } + validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale)) + + allOK := validator.AllOK() + for _, product := range form.Products { + allOK = product.Validate() && allOK + } + return allOK +} + +func (form *invoiceForm) Update() { + products := form.Products + form.Products = nil + index := 0 + for _, product := range products { + if product.Quantity.Val != "0" { + form.Products = append(form.Products, product) + index++ + } + } +} + +func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption { + return MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id) +} + +type invoiceProductForm struct { + locale *Locale + company *Company + ProductId *InputField + Name *InputField + Description *InputField + Price *InputField + Quantity *InputField + Discount *InputField + Tax *SelectField +} + +func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptions []*SelectOption) *invoiceProductForm { + suffix := "." + strconv.Itoa(index) return &invoiceProductForm{ + locale: locale, + company: company, ProductId: &InputField{ - Name: "product[][id]", + Name: "product.id" + suffix, Label: pgettext("input", "Id", locale), Type: "hidden", Required: true, }, Name: &InputField{ - Name: "product[][name]", + Name: "product.name" + suffix, Label: pgettext("input", "Name", locale), Type: "text", Required: true, }, Description: &InputField{ - Name: "product[][description]", + Name: "product.description" + suffix, Label: pgettext("input", "Description", locale), Type: "textarea", }, Price: &InputField{ - Name: "product[][price]", + Name: "product.price" + suffix, Label: pgettext("input", "Price", locale), Type: "number", Required: true, @@ -124,7 +338,7 @@ func newInvoiceProductForm(ctx context.Context, conn *Conn, company *Company, lo }, }, Quantity: &InputField{ - Name: "product[][quantity]", + Name: "product.quantity" + suffix, Label: pgettext("input", "Quantity", locale), Type: "number", Required: true, @@ -133,7 +347,7 @@ func newInvoiceProductForm(ctx context.Context, conn *Conn, company *Company, lo }, }, Discount: &InputField{ - Name: "product[][discount]", + Name: "product.discount" + suffix, Label: pgettext("input", "Discount (%)", locale), Type: "number", Required: true, @@ -143,10 +357,40 @@ func newInvoiceProductForm(ctx context.Context, conn *Conn, company *Company, lo }, }, Tax: &SelectField{ - Name: "product[][tax]", + Name: "product.tax" + suffix, Label: pgettext("input", "Taxes", locale), Multiple: true, - Options: MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id), + Options: taxOptions, }, } } + +func (form *invoiceProductForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + form.ProductId.FillValue(r) + form.Name.FillValue(r) + form.Description.FillValue(r) + form.Price.FillValue(r) + form.Quantity.FillValue(r) + form.Discount.FillValue(r) + form.Tax.FillValue(r) + return nil +} + +func (form *invoiceProductForm) Validate() bool { + validator := newFormValidator() + validator.CheckRequiredInput(form.Name, gettext("Name can not be empty.", form.locale)) + if validator.CheckRequiredInput(form.Price, gettext("Price can not be empty.", form.locale)) { + validator.CheckValidDecimal(form.Price, form.company.MinCents(), math.MaxFloat64, gettext("Price must be a number greater than zero.", form.locale)) + } + if validator.CheckRequiredInput(form.Quantity, gettext("Quantity can not be empty.", form.locale)) { + validator.CheckValidInteger(form.Quantity, 1, math.MaxInt, gettext("Quantity must be a number greater than zero.", form.locale)) + } + if validator.CheckRequiredInput(form.Discount, gettext("Discount can not be empty.", form.locale)) { + validator.CheckValidInteger(form.Discount, 0, 100, gettext("Discount must be a percentage between 0 and 100.", form.locale)) + } + validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale)) + return validator.AllOK() +} diff --git a/pkg/products.go b/pkg/products.go index 50128c3..53a3c50 100644 --- a/pkg/products.go +++ b/pkg/products.go @@ -214,7 +214,7 @@ func newProductForm(ctx context.Context, conn *Conn, locale *Locale, company *Co Name: "tax", Label: pgettext("input", "Taxes", locale), Multiple: true, - Options: MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id), + Options: mustGetTaxOptions(ctx, conn, company), }, } } diff --git a/pkg/router.go b/pkg/router.go index 8f2fa6a..3c56ede 100644 --- a/pkg/router.go +++ b/pkg/router.go @@ -25,6 +25,7 @@ func NewRouter(db *Db) http.Handler { companyRouter.GET("/invoices", IndexInvoices) companyRouter.POST("/invoices", HandleAddInvoice) companyRouter.GET("/invoices/:slug", GetInvoiceForm) + companyRouter.POST("/invoices/new/products", HandleAddProductsToInvoice) companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { mustRenderAppTemplate(w, r, "dashboard.gohtml", nil) }) diff --git a/pkg/template.go b/pkg/template.go index aa9033e..a4e3b13 100644 --- a/pkg/template.go +++ b/pkg/template.go @@ -9,6 +9,7 @@ import ( "math" "net/http" "strconv" + "time" ) const overrideMethodName = "_method" @@ -39,6 +40,9 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s } return p.Sprintf("%.*f", company.DecimalDigits, number.Decimal(f)) }, + "formatDate": func(time time.Time) string { + return time.Format("02/01/2006") + }, "csrfToken": func() template.HTML { return template.HTML(fmt.Sprintf(``, csrfTokenField, user.CsrfToken)) }, diff --git a/po/ca.po b/po/ca.po index fb5b368..b086249 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: numerus\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-02-08 13:43+0100\n" +"POT-Creation-Date: 2023-02-12 20:51+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -17,6 +17,111 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +#: web/template/invoices/products.gohtml:2 +#: web/template/invoices/products.gohtml:15 +msgctxt "title" +msgid "Add Products to Invoice" +msgstr "Afegeix productes a la factura" + +#: web/template/invoices/products.gohtml:9 web/template/invoices/new.gohtml:9 +#: web/template/invoices/index.gohtml:8 web/template/contacts/new.gohtml:9 +#: web/template/contacts/index.gohtml:8 web/template/contacts/edit.gohtml:9 +#: web/template/profile.gohtml:9 web/template/tax-details.gohtml:8 +#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:8 +#: web/template/products/edit.gohtml:9 +msgctxt "title" +msgid "Home" +msgstr "Inici" + +#: web/template/invoices/products.gohtml:10 web/template/invoices/new.gohtml:10 +#: web/template/invoices/index.gohtml:2 web/template/invoices/index.gohtml:9 +msgctxt "title" +msgid "Invoices" +msgstr "Factures" + +#: web/template/invoices/products.gohtml:11 web/template/invoices/new.gohtml:2 +#: web/template/invoices/new.gohtml:11 web/template/invoices/new.gohtml:15 +msgctxt "title" +msgid "New Invoice" +msgstr "Nova factura" + +#: web/template/invoices/products.gohtml:41 +#: web/template/products/index.gohtml:21 +msgctxt "product" +msgid "All" +msgstr "Tots" + +#: web/template/invoices/products.gohtml:42 +#: web/template/products/index.gohtml:22 +msgctxt "title" +msgid "Name" +msgstr "Nom" + +#: web/template/invoices/products.gohtml:43 +#: web/template/products/index.gohtml:23 +msgctxt "title" +msgid "Price" +msgstr "Preu" + +#: web/template/invoices/products.gohtml:57 +#: web/template/products/index.gohtml:37 +msgid "No products added yet." +msgstr "No hi ha cap producte." + +#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:37 +msgctxt "action" +msgid "Add products" +msgstr "Afegeix productes" + +#: web/template/invoices/new.gohtml:38 +msgctxt "action" +msgid "Update" +msgstr "Actualitza" + +#: web/template/invoices/new.gohtml:40 web/template/invoices/index.gohtml:13 +msgctxt "action" +msgid "New invoice" +msgstr "Nova factura" + +#: web/template/invoices/index.gohtml:21 +msgctxt "invoice" +msgid "All" +msgstr "Totes" + +#: web/template/invoices/index.gohtml:22 +msgctxt "title" +msgid "Date" +msgstr "Data" + +#: web/template/invoices/index.gohtml:23 +msgctxt "title" +msgid "Invoice Num." +msgstr "Núm. factura" + +#: web/template/invoices/index.gohtml:24 web/template/contacts/index.gohtml:22 +msgctxt "title" +msgid "Customer" +msgstr "Client" + +#: web/template/invoices/index.gohtml:25 +msgctxt "title" +msgid "Status" +msgstr "Estat" + +#: web/template/invoices/index.gohtml:26 +msgctxt "title" +msgid "Label" +msgstr "Etiqueta" + +#: web/template/invoices/index.gohtml:27 +msgctxt "title" +msgid "Download" +msgstr "Descàrrega" + +#: web/template/invoices/index.gohtml:45 +msgid "No invoices added yet." +msgstr "No hi ha cap factura." + #: web/template/dashboard.gohtml:2 msgctxt "title" msgid "Dashboard" @@ -44,10 +149,15 @@ msgstr "Tauler" #: web/template/app.gohtml:44 msgctxt "nav" +msgid "Invoices" +msgstr "Factures" + +#: web/template/app.gohtml:45 +msgctxt "nav" msgid "Products" msgstr "Productes" -#: web/template/app.gohtml:45 +#: web/template/app.gohtml:46 msgctxt "nav" msgid "Contacts" msgstr "Contactes" @@ -58,14 +168,6 @@ msgctxt "title" msgid "New Contact" msgstr "Nou contacte" -#: web/template/contacts/new.gohtml:9 web/template/contacts/index.gohtml:8 -#: web/template/contacts/edit.gohtml:9 web/template/profile.gohtml:9 -#: web/template/tax-details.gohtml:8 web/template/products/new.gohtml:9 -#: web/template/products/index.gohtml:8 web/template/products/edit.gohtml:9 -msgctxt "title" -msgid "Home" -msgstr "Inici" - #: web/template/contacts/new.gohtml:10 web/template/contacts/index.gohtml:2 #: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10 msgctxt "title" @@ -82,11 +184,6 @@ msgctxt "contact" msgid "All" msgstr "Tots" -#: web/template/contacts/index.gohtml:22 -msgctxt "title" -msgid "Customer" -msgstr "Client" - #: web/template/contacts/index.gohtml:23 msgctxt "title" msgid "Email" @@ -199,25 +296,6 @@ msgctxt "action" msgid "New product" msgstr "Nou producte" -#: web/template/products/index.gohtml:21 -msgctxt "product" -msgid "All" -msgstr "Tots" - -#: web/template/products/index.gohtml:22 -msgctxt "title" -msgid "Name" -msgstr "Nom" - -#: web/template/products/index.gohtml:23 -msgctxt "title" -msgid "Price" -msgstr "Preu" - -#: web/template/products/index.gohtml:37 -msgid "No products added yet." -msgstr "No hi ha cap producte." - #: web/template/products/edit.gohtml:2 web/template/products/edit.gohtml:15 msgctxt "title" msgid "Edit Product “%s”" @@ -254,39 +332,40 @@ msgstr "No podeu deixar la contrasenya en blanc." msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." -#: pkg/products.go:193 +#: pkg/products.go:194 pkg/invoices.go:321 msgctxt "input" msgid "Name" msgstr "Nom" -#: pkg/products.go:199 +#: pkg/products.go:200 pkg/invoices.go:327 msgctxt "input" msgid "Description" msgstr "Descripció" -#: pkg/products.go:204 +#: pkg/products.go:205 pkg/invoices.go:332 msgctxt "input" msgid "Price" msgstr "Preu" -#: pkg/products.go:214 +#: pkg/products.go:215 pkg/invoices.go:237 pkg/invoices.go:361 msgctxt "input" msgid "Taxes" msgstr "Imposts" -#: pkg/products.go:234 pkg/profile.go:92 +#: pkg/products.go:235 pkg/profile.go:92 pkg/invoices.go:267 +#: pkg/invoices.go:384 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." -#: pkg/products.go:235 +#: pkg/products.go:236 pkg/invoices.go:385 msgid "Price can not be empty." msgstr "No podeu deixar el preu en blanc." -#: pkg/products.go:236 +#: pkg/products.go:237 pkg/invoices.go:386 msgid "Price must be a number greater than zero." msgstr "El preu ha de ser un número major a zero." -#: pkg/products.go:238 +#: pkg/products.go:239 pkg/invoices.go:271 pkg/invoices.go:394 msgid "Selected tax is not valid." msgstr "Heu seleccionat un impost que no és vàlid." @@ -349,6 +428,73 @@ 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:66 +msgid "Select a customer to bill." +msgstr "Escolliu un client a facturar." + +#: pkg/invoices.go:163 +msgid "Invalid action" +msgstr "Acció invàlida." + +#: pkg/invoices.go:214 +msgctxt "input" +msgid "Customer" +msgstr "Client" + +#: pkg/invoices.go:220 +msgctxt "input" +msgid "Number" +msgstr "Número" + +#: pkg/invoices.go:226 +msgctxt "input" +msgid "Invoice Date" +msgstr "Data de factura" + +#: pkg/invoices.go:232 +msgctxt "input" +msgid "Notes" +msgstr "Notes" + +#: pkg/invoices.go:268 +msgid "Invoice date can not be empty." +msgstr "No podeu deixar la data de la factura en blanc." + +#: pkg/invoices.go:269 +msgid "Invoice date must be a valid date." +msgstr "La data de facturació ha de ser vàlida." + +#: pkg/invoices.go:315 +msgctxt "input" +msgid "Id" +msgstr "Identificador" + +#: pkg/invoices.go:342 +msgctxt "input" +msgid "Quantity" +msgstr "Quantitat" + +#: pkg/invoices.go:351 +msgctxt "input" +msgid "Discount (%)" +msgstr "Descompte (%)" + +#: pkg/invoices.go:388 +msgid "Quantity can not be empty." +msgstr "No podeu deixar la quantitat en blanc." + +#: pkg/invoices.go:389 +msgid "Quantity must be a number greater than zero." +msgstr "La quantitat ha de ser un número major a zero." + +#: pkg/invoices.go:391 +msgid "Discount can not be empty." +msgstr "No podeu deixar el descompte en blanc." + +#: pkg/invoices.go:392 +msgid "Discount must be a percentage between 0 and 100." +msgstr "El descompte ha de ser un percentatge entre 0 i 100." + #: pkg/contacts.go:149 msgctxt "input" msgid "Business name" diff --git a/po/es.po b/po/es.po index cbd9f11..3479ad3 100644 --- a/po/es.po +++ b/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: numerus\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-02-08 13:43+0100\n" +"POT-Creation-Date: 2023-02-12 20:51+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -17,6 +17,111 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: web/template/invoices/products.gohtml:2 +#: web/template/invoices/products.gohtml:15 +msgctxt "title" +msgid "Add Products to Invoice" +msgstr "Añadir productos a la factura" + +#: web/template/invoices/products.gohtml:9 web/template/invoices/new.gohtml:9 +#: web/template/invoices/index.gohtml:8 web/template/contacts/new.gohtml:9 +#: web/template/contacts/index.gohtml:8 web/template/contacts/edit.gohtml:9 +#: web/template/profile.gohtml:9 web/template/tax-details.gohtml:8 +#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:8 +#: web/template/products/edit.gohtml:9 +msgctxt "title" +msgid "Home" +msgstr "Inicio" + +#: web/template/invoices/products.gohtml:10 web/template/invoices/new.gohtml:10 +#: web/template/invoices/index.gohtml:2 web/template/invoices/index.gohtml:9 +msgctxt "title" +msgid "Invoices" +msgstr "Facturas" + +#: web/template/invoices/products.gohtml:11 web/template/invoices/new.gohtml:2 +#: web/template/invoices/new.gohtml:11 web/template/invoices/new.gohtml:15 +msgctxt "title" +msgid "New Invoice" +msgstr "Nueva factura" + +#: web/template/invoices/products.gohtml:41 +#: web/template/products/index.gohtml:21 +msgctxt "product" +msgid "All" +msgstr "Todos" + +#: web/template/invoices/products.gohtml:42 +#: web/template/products/index.gohtml:22 +msgctxt "title" +msgid "Name" +msgstr "Nombre" + +#: web/template/invoices/products.gohtml:43 +#: web/template/products/index.gohtml:23 +msgctxt "title" +msgid "Price" +msgstr "Precio" + +#: web/template/invoices/products.gohtml:57 +#: web/template/products/index.gohtml:37 +msgid "No products added yet." +msgstr "No hay productos." + +#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:37 +msgctxt "action" +msgid "Add products" +msgstr "Añadir productos" + +#: web/template/invoices/new.gohtml:38 +msgctxt "action" +msgid "Update" +msgstr "Actualizar" + +#: web/template/invoices/new.gohtml:40 web/template/invoices/index.gohtml:13 +msgctxt "action" +msgid "New invoice" +msgstr "Nueva factura" + +#: web/template/invoices/index.gohtml:21 +msgctxt "invoice" +msgid "All" +msgstr "Todas" + +#: web/template/invoices/index.gohtml:22 +msgctxt "title" +msgid "Date" +msgstr "Fecha" + +#: web/template/invoices/index.gohtml:23 +msgctxt "title" +msgid "Invoice Num." +msgstr "Nº factura" + +#: web/template/invoices/index.gohtml:24 web/template/contacts/index.gohtml:22 +msgctxt "title" +msgid "Customer" +msgstr "Cliente" + +#: web/template/invoices/index.gohtml:25 +msgctxt "title" +msgid "Status" +msgstr "Estado" + +#: web/template/invoices/index.gohtml:26 +msgctxt "title" +msgid "Label" +msgstr "Etiqueta" + +#: web/template/invoices/index.gohtml:27 +msgctxt "title" +msgid "Download" +msgstr "Descargar" + +#: web/template/invoices/index.gohtml:45 +msgid "No invoices added yet." +msgstr "No hay facturas." + #: web/template/dashboard.gohtml:2 msgctxt "title" msgid "Dashboard" @@ -44,10 +149,15 @@ msgstr "Panel" #: web/template/app.gohtml:44 msgctxt "nav" +msgid "Invoices" +msgstr "Facturas" + +#: web/template/app.gohtml:45 +msgctxt "nav" msgid "Products" msgstr "Productos" -#: web/template/app.gohtml:45 +#: web/template/app.gohtml:46 msgctxt "nav" msgid "Contacts" msgstr "Contactos" @@ -58,14 +168,6 @@ msgctxt "title" msgid "New Contact" msgstr "Nuevo contacto" -#: web/template/contacts/new.gohtml:9 web/template/contacts/index.gohtml:8 -#: web/template/contacts/edit.gohtml:9 web/template/profile.gohtml:9 -#: web/template/tax-details.gohtml:8 web/template/products/new.gohtml:9 -#: web/template/products/index.gohtml:8 web/template/products/edit.gohtml:9 -msgctxt "title" -msgid "Home" -msgstr "Inicio" - #: web/template/contacts/new.gohtml:10 web/template/contacts/index.gohtml:2 #: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10 msgctxt "title" @@ -82,11 +184,6 @@ msgctxt "contact" msgid "All" msgstr "Todos" -#: web/template/contacts/index.gohtml:22 -msgctxt "title" -msgid "Customer" -msgstr "Cliente" - #: web/template/contacts/index.gohtml:23 msgctxt "title" msgid "Email" @@ -199,25 +296,6 @@ msgctxt "action" msgid "New product" msgstr "Nuevo producto" -#: web/template/products/index.gohtml:21 -msgctxt "product" -msgid "All" -msgstr "Todos" - -#: web/template/products/index.gohtml:22 -msgctxt "title" -msgid "Name" -msgstr "Nombre" - -#: web/template/products/index.gohtml:23 -msgctxt "title" -msgid "Price" -msgstr "Precio" - -#: web/template/products/index.gohtml:37 -msgid "No products added yet." -msgstr "No hay productos." - #: web/template/products/edit.gohtml:2 web/template/products/edit.gohtml:15 msgctxt "title" msgid "Edit Product “%s”" @@ -254,39 +332,40 @@ 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:193 +#: pkg/products.go:194 pkg/invoices.go:321 msgctxt "input" msgid "Name" msgstr "Nombre" -#: pkg/products.go:199 +#: pkg/products.go:200 pkg/invoices.go:327 msgctxt "input" msgid "Description" msgstr "Descripción" -#: pkg/products.go:204 +#: pkg/products.go:205 pkg/invoices.go:332 msgctxt "input" msgid "Price" msgstr "Precio" -#: pkg/products.go:214 +#: pkg/products.go:215 pkg/invoices.go:237 pkg/invoices.go:361 msgctxt "input" msgid "Taxes" msgstr "Impuestos" -#: pkg/products.go:234 pkg/profile.go:92 +#: pkg/products.go:235 pkg/profile.go:92 pkg/invoices.go:267 +#: pkg/invoices.go:384 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." -#: pkg/products.go:235 +#: pkg/products.go:236 pkg/invoices.go:385 msgid "Price can not be empty." msgstr "No podéis dejar el precio en blanco." -#: pkg/products.go:236 +#: pkg/products.go:237 pkg/invoices.go:386 msgid "Price must be a number greater than zero." msgstr "El precio tiene que ser un número mayor a cero." -#: pkg/products.go:238 +#: pkg/products.go:239 pkg/invoices.go:271 pkg/invoices.go:394 msgid "Selected tax is not valid." msgstr "Habéis escogido un impuesto que no es válido." @@ -349,6 +428,73 @@ 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:66 +msgid "Select a customer to bill." +msgstr "Escoged un cliente a facturar." + +#: pkg/invoices.go:163 +msgid "Invalid action" +msgstr "Acción inválida." + +#: pkg/invoices.go:214 +msgctxt "input" +msgid "Customer" +msgstr "Cliente" + +#: pkg/invoices.go:220 +msgctxt "input" +msgid "Number" +msgstr "Número" + +#: pkg/invoices.go:226 +msgctxt "input" +msgid "Invoice Date" +msgstr "Fecha de factura" + +#: pkg/invoices.go:232 +msgctxt "input" +msgid "Notes" +msgstr "Notas" + +#: pkg/invoices.go:268 +msgid "Invoice date can not be empty." +msgstr "No podéis dejar la fecha de la factura en blanco." + +#: pkg/invoices.go:269 +msgid "Invoice date must be a valid date." +msgstr "La fecha de factura debe ser válida." + +#: pkg/invoices.go:315 +msgctxt "input" +msgid "Id" +msgstr "Identificador" + +#: pkg/invoices.go:342 +msgctxt "input" +msgid "Quantity" +msgstr "Cantidad" + +#: pkg/invoices.go:351 +msgctxt "input" +msgid "Discount (%)" +msgstr "Descuento (%)" + +#: pkg/invoices.go:388 +msgid "Quantity can not be empty." +msgstr "No podéis dejar la cantidad en blanco." + +#: pkg/invoices.go:389 +msgid "Quantity must be a number greater than zero." +msgstr "La cantidad tiene que ser un número mayor a cero." + +#: pkg/invoices.go:391 +msgid "Discount can not be empty." +msgstr "No podéis dejar el descuento en blanco." + +#: pkg/invoices.go:392 +msgid "Discount must be a percentage between 0 and 100." +msgstr "El descuento tiene que ser un percentage entre 0 y 100." + #: pkg/contacts.go:149 msgctxt "input" msgid "Business name" diff --git a/web/template/form.gohtml b/web/template/form.gohtml index db874b6..486b012 100644 --- a/web/template/form.gohtml +++ b/web/template/form.gohtml @@ -1,10 +1,12 @@ {{ define "hidden-field" -}} - + value="{{ .Val }}"> {{- end }} {{ define "input-field" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InputField*/ -}}
{{ if eq .Type "textarea" }}