diff --git a/pkg/invoices.go b/pkg/invoices.go index a1e9c6f..ed95288 100644 --- a/pkg/invoices.go +++ b/pkg/invoices.go @@ -117,14 +117,15 @@ func mustCollectInvoiceStatuses(ctx context.Context, conn *Conn, locale *Locale) } func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - locale := getLocale(r) conn := getConn(r) company := mustGetCompany(r) - form := newInvoiceForm(r.Context(), conn, locale, company) slug := params[0].Value if slug == "new" { + locale := getLocale(r) + form := newInvoiceForm(r.Context(), conn, locale, company) if invoiceToDuplicate := r.URL.Query().Get("duplicate"); invoiceToDuplicate != "" { form.MustFillFromDatabase(r.Context(), conn, invoiceToDuplicate) + form.InvoiceStatus.Selected = []string{"created"} form.Number.Val = "" } form.Date.Val = time.Now().Format("2006-01-02") @@ -304,10 +305,11 @@ func mustRenderNewInvoiceForm(w http.ResponseWriter, r *http.Request, form *invo mustRenderAppTemplate(w, r, "invoices/new.gohtml", page) } -func mustRenderNewInvoiceProductsForm(w http.ResponseWriter, r *http.Request, form *invoiceForm) { +func mustRenderNewInvoiceProductsForm(w http.ResponseWriter, r *http.Request, action string, form *invoiceForm) { conn := getConn(r) company := mustGetCompany(r) page := newInvoiceProductsPage{ + Action: companyURI(company, action), Form: form, Products: mustGetProductChoices(r.Context(), conn, company), } @@ -334,6 +336,7 @@ func mustGetProductChoices(ctx context.Context, conn *Conn, company *Company) [] } type newInvoiceProductsPage struct { + Action string Form *invoiceForm Products []*productChoice } @@ -362,39 +365,18 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param mustRenderNewInvoiceForm(w, r, form) return } - reg := regexp.MustCompile("[^a-z0-9-]+") - tags := strings.Split(reg.ReplaceAllString(form.Tags.Val, " "), " ") - slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7, $8)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, tags, NewInvoiceProductArray(form.Products)) + slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7, $8)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, form.SplitTags(), NewInvoiceProductArray(form.Products)) http.Redirect(w, r, companyURI(company, "/invoices/"+slug), http.StatusSeeOther) } -func HandleNewInvoiceAction(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 "select-products": - w.WriteHeader(http.StatusOK) - mustRenderNewInvoiceProductsForm(w, r, form) - case "add-products": - form.AddProducts(r.Context(), conn, r.Form["id"]) - w.WriteHeader(http.StatusOK) - mustRenderNewInvoiceForm(w, r, form) +func HandleNewInvoiceAction(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + switch params[0].Value { + case "new": + handleInvoiceAction(w, r, "/invoices/new", mustRenderNewInvoiceForm) + case "batch": + HandleBatchInvoiceAction(w, r, params) default: - http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest) + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) } } @@ -450,6 +432,7 @@ func mustWriteInvoicesPdf(r *http.Request, slugs []string) []byte { type invoiceForm struct { locale *Locale company *Company + InvoiceStatus *SelectField Customer *SelectField Number *InputField Date *InputField @@ -463,6 +446,13 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co return &invoiceForm{ locale: locale, company: company, + InvoiceStatus: &SelectField{ + Name: "invoice_status", + Required: true, + Label: pgettext("input", "Invoice Status", locale), + Selected: []string{"created"}, + 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()), + }, Customer: &SelectField{ Name: "customer", Label: pgettext("input", "Customer", locale), @@ -504,6 +494,7 @@ func (form *invoiceForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } + form.InvoiceStatus.FillValue(r) form.Customer.FillValue(r) form.Number.FillValue(r) form.Date.FillValue(r) @@ -529,7 +520,8 @@ func (form *invoiceForm) Parse(r *http.Request) error { func (form *invoiceForm) Validate() bool { validator := newFormValidator() - validator.CheckValidSelectOption(form.Customer, gettext("Name can not be empty.", form.locale)) + validator.CheckValidSelectOption(form.InvoiceStatus, gettext("Selected invoice status is not valid.", form.locale)) + validator.CheckValidSelectOption(form.Customer, gettext("Selected customer is not valid.", 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)) } @@ -556,7 +548,7 @@ func (form *invoiceForm) Update() { } func (form *invoiceForm) AddProducts(ctx context.Context, conn *Conn, productsId []string) { - form.mustAddProductsFromQuery(ctx, conn, "select product_id, name, description, to_price(price, decimal_digits), 1 as quantity, 0 as discount, array_remove(array_agg(tax_id), null) 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) + form.mustAddProductsFromQuery(ctx, conn, "select '', product_id, name, description, to_price(price, decimal_digits), 1 as quantity, 0 as discount, array_remove(array_agg(tax_id), null) 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) } func (form *invoiceForm) mustAddProductsFromQuery(ctx context.Context, conn *Conn, sql string, args ...interface{}) { @@ -566,7 +558,7 @@ func (form *invoiceForm) mustAddProductsFromQuery(ctx context.Context, conn *Con defer rows.Close() for rows.Next() { product := newInvoiceProductForm(index, form.company, form.locale, taxOptions) - if err := rows.Scan(product.ProductId, product.Name, product.Description, product.Price, product.Quantity, product.Discount, product.Tax); err != nil { + if err := rows.Scan(product.InvoiceProductId, product.ProductId, product.Name, product.Description, product.Price, product.Quantity, product.Discount, product.Tax); err != nil { panic(err) } form.Products = append(form.Products, product) @@ -577,12 +569,15 @@ func (form *invoiceForm) mustAddProductsFromQuery(ctx context.Context, conn *Con } } -func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) { +func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool { var invoiceId int + selectedInvoiceStatus := form.InvoiceStatus.Selected + form.InvoiceStatus.Clear() selectedPaymentMethod := form.PaymentMethod.Selected form.PaymentMethod.Clear() if notFoundErrorOrPanic(conn.QueryRow(ctx, ` select invoice_id + , invoice_status , contact_id , invoice_number , invoice_date @@ -598,34 +593,47 @@ func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s , invoice_date , notes , payment_method_id - `, slug).Scan(&invoiceId, form.Customer, form.Number, form.Date, form.Notes, form.PaymentMethod, form.Tags)) { + `, slug).Scan(&invoiceId, form.InvoiceStatus, form.Customer, form.Number, form.Date, form.Notes, form.PaymentMethod, form.Tags)) { form.PaymentMethod.Selected = selectedPaymentMethod - return + form.InvoiceStatus.Selected = selectedInvoiceStatus + return false } form.Products = []*invoiceProductForm{} - form.mustAddProductsFromQuery(ctx, conn, "select product_id, name, description, to_price(price, $2), quantity, (discount_rate * 100)::integer, array_remove(array_agg(tax_id), null) from invoice_product left join invoice_product_tax using (invoice_product_id) where invoice_id = $1 group by product_id, name, description, discount_rate, price, quantity", invoiceId, form.company.DecimalDigits) + form.mustAddProductsFromQuery(ctx, conn, "select invoice_product_id::text, product_id, name, description, to_price(price, $2), quantity, (discount_rate * 100)::integer, array_remove(array_agg(tax_id), null) from invoice_product left join invoice_product_tax using (invoice_product_id) where invoice_id = $1 group by invoice_product_id, product_id, name, description, discount_rate, price, quantity", invoiceId, form.company.DecimalDigits) + return true } func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption { return MustGetGroupedOptions(ctx, conn, "select tax_id::text, tax.name, tax_class.name from tax join tax_class using (tax_class_id) where tax.company_id = $1 order by tax_class.name, tax.name", company.Id) } +func (form *invoiceForm) SplitTags() []string { + reg := regexp.MustCompile("[^a-z0-9-]+") + return strings.Split(reg.ReplaceAllString(form.Tags.Val, " "), " ") +} + type invoiceProductForm struct { - locale *Locale - company *Company - ProductId *InputField - Name *InputField - Description *InputField - Price *InputField - Quantity *InputField - Discount *InputField - Tax *SelectField + locale *Locale + company *Company + InvoiceProductId *InputField + 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 { form := &invoiceProductForm{ locale: locale, company: company, + InvoiceProductId: &InputField{ + Label: pgettext("input", "Id", locale), + Type: "hidden", + Required: true, + }, ProductId: &InputField{ Label: pgettext("input", "Id", locale), Type: "hidden", @@ -678,6 +686,7 @@ func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptio func (form *invoiceProductForm) Reindex(index int) { suffix := "." + strconv.Itoa(index) + form.InvoiceProductId.Name = "product.invoice_product_id" + suffix form.ProductId.Name = "product.id" + suffix form.Name.Name = "product.name" + suffix form.Description.Name = "product.description" + suffix @@ -691,6 +700,7 @@ func (form *invoiceProductForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } + form.InvoiceProductId.FillValue(r) form.ProductId.FillValue(r) form.Name.FillValue(r) form.Description.FillValue(r) @@ -703,6 +713,7 @@ func (form *invoiceProductForm) Parse(r *http.Request) error { func (form *invoiceProductForm) Validate() bool { validator := newFormValidator() + validator.CheckRequiredInput(form.ProductId, gettext("Product ID can not be empty.", form.locale)) 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)) @@ -719,8 +730,11 @@ func (form *invoiceProductForm) Validate() bool { } func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + locale := getLocale(r) conn := getConn(r) - if err := r.ParseForm(); err != nil { + 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 } @@ -728,10 +742,97 @@ func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprout http.Error(w, err.Error(), http.StatusForbidden) return } - invoiceStatus := r.FormValue("status") - slug := conn.MustGetText(r.Context(), "", "update invoice set invoice_status = $1 where slug = $2 returning slug", invoiceStatus, params[0].Value) - if slug == "" { - http.NotFound(w, r) + if r.FormValue("quick") == "status" { + slug := conn.MustGetText(r.Context(), "", "update invoice set invoice_status = $1 where slug = $2 returning slug", form.InvoiceStatus, params[0].Value) + if slug == "" { + http.NotFound(w, r) + } + http.Redirect(w, r, companyURI(mustGetCompany(r), "/invoices"), http.StatusSeeOther) + } else { + slug := params[0].Value + if !form.Validate() { + w.WriteHeader(http.StatusUnprocessableEntity) + mustRenderEditInvoiceForm(w, r, slug, form) + return + } + slug = conn.MustGetText(r.Context(), "", "select edit_invoice($1, $2, $3, $4, $5, $6, $7)", slug, form.InvoiceStatus, form.Customer, form.Notes, form.PaymentMethod, form.SplitTags(), EditedInvoiceProductArray(form.Products)) + if slug == "" { + http.NotFound(w, r) + return + } + http.Redirect(w, r, companyURI(company, "/invoices/"+slug), http.StatusSeeOther) + } +} + +func ServeEditInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + conn := getConn(r) + company := mustGetCompany(r) + slug := params[0].Value + locale := getLocale(r) + form := newInvoiceForm(r.Context(), conn, locale, company) + if !form.MustFillFromDatabase(r.Context(), conn, slug) { + http.NotFound(w, r) + return + } + w.WriteHeader(http.StatusOK) + mustRenderEditInvoiceForm(w, r, slug, form) +} + +type editInvoicePage struct { + *newInvoicePage + Slug string + Number string +} + +func newEditInvoicePage(slug string, form *invoiceForm, r *http.Request) *editInvoicePage { + return &editInvoicePage{ + newNewInvoicePage(form, r), + slug, + form.Number.String(), + } +} + +func mustRenderEditInvoiceForm(w http.ResponseWriter, r *http.Request, slug string, form *invoiceForm) { + page := newEditInvoicePage(slug, form, r) + mustRenderAppTemplate(w, r, "invoices/edit.gohtml", page) +} + +func HandleEditInvoiceAction(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + slug := params[0].Value + actionUri := fmt.Sprintf("/invoices/%s/edit", slug) + handleInvoiceAction(w, r, actionUri, func(w http.ResponseWriter, r *http.Request, form *invoiceForm) { + mustRenderEditInvoiceForm(w, r, slug, form) + }) +} + +type renderFormFunc func(w http.ResponseWriter, r *http.Request, form *invoiceForm) + +func handleInvoiceAction(w http.ResponseWriter, r *http.Request, action string, renderForm renderFormFunc) { + 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) + renderForm(w, r, form) + case "select-products": + w.WriteHeader(http.StatusOK) + mustRenderNewInvoiceProductsForm(w, r, action, form) + case "add-products": + form.AddProducts(r.Context(), conn, r.Form["id"]) + w.WriteHeader(http.StatusOK) + renderForm(w, r, form) + default: + http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest) } - http.Redirect(w, r, companyURI(mustGetCompany(r), "/invoices"), http.StatusSeeOther) } diff --git a/pkg/pgtypes.go b/pkg/pgtypes.go index 521e0c7..1d9be1a 100644 --- a/pkg/pgtypes.go +++ b/pkg/pgtypes.go @@ -34,6 +34,40 @@ func (src NewInvoiceProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) return array.EncodeBinary(ci, buf) } +type EditedInvoiceProductArray []*invoiceProductForm + +func (src EditedInvoiceProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) { + typeName := "edited_invoice_product[]" + dt, ok := ci.DataTypeForName(typeName) + if !ok { + return nil, fmt.Errorf("unable to find oid for type name %v", typeName) + } + var values [][]interface{} + for _, form := range src { + var invoiceProductId interface{} = nil + if form.InvoiceProductId.Val != "" { + if id := form.InvoiceProductId.Integer(); id > 0 { + invoiceProductId = id + } + } + values = append(values, []interface{}{ + invoiceProductId, + form.ProductId.Val, + form.Name.Val, + form.Description.Val, + form.Price.Val, + form.Quantity.Val, + form.Discount.Float64() / 100.0, + form.Tax.Selected, + }) + } + array := pgtype.NewValue(dt.Value).(pgtype.ValueTranscoder) + if err := array.Set(values); err != nil { + return nil, err + } + return array.EncodeBinary(ci, buf) +} + func registerPgTypes(ctx context.Context, conn *pgx.Conn) error { if _, err := conn.Exec(ctx, "set role to admin"); err != nil { return err @@ -71,6 +105,36 @@ func registerPgTypes(ctx context.Context, conn *pgx.Conn) error { return err } + editedInvoiceProduct, err := pgtype.NewCompositeType( + "edited_invoice_product", + []pgtype.CompositeTypeField{ + {"invoice_product_id", pgtype.Int4OID}, + {"product_id", pgtype.Int4OID}, + {"name", pgtype.TextOID}, + {"description", pgtype.TextOID}, + {"price", pgtype.TextOID}, + {"quantity", pgtype.Int4OID}, + {"discount_rate", discountRateOID}, + {"tax", pgtype.Int4ArrayOID}, + }, + conn.ConnInfo(), + ) + if err != nil { + return err + } + editedInvoiceProductOID, err := registerPgType(ctx, conn, editedInvoiceProduct, editedInvoiceProduct.TypeName()) + if err != nil { + return err + } + editedInvoiceProductArray := pgtype.NewArrayType("edited_invoice_product[]", editedInvoiceProductOID, func() pgtype.ValueTranscoder { + value := editedInvoiceProduct.NewTypeValue() + return value.(pgtype.ValueTranscoder) + }) + _, err = registerPgType(ctx, conn, editedInvoiceProductArray, editedInvoiceProductArray.TypeName()) + if err != nil { + return err + } + _, err = conn.Exec(ctx, "reset role") return err } diff --git a/pkg/router.go b/pkg/router.go index 1dcad24..f3f3688 100644 --- a/pkg/router.go +++ b/pkg/router.go @@ -28,8 +28,9 @@ func NewRouter(db *Db) http.Handler { companyRouter.POST("/invoices", HandleAddInvoice) companyRouter.GET("/invoices/:slug", ServeInvoice) companyRouter.PUT("/invoices/:slug", HandleUpdateInvoice) - companyRouter.POST("/invoices/new", HandleNewInvoiceAction) - companyRouter.POST("/invoices/batch", HandleBatchInvoiceAction) + companyRouter.POST("/invoices/:slug", HandleNewInvoiceAction) + companyRouter.GET("/invoices/:slug/edit", ServeEditInvoice) + companyRouter.POST("/invoices/:slug/edit", HandleEditInvoiceAction) companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { mustRenderAppTemplate(w, r, "dashboard.gohtml", nil) }) diff --git a/po/ca.po b/po/ca.po index 70057f6..55c1014 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-03-10 13:59+0100\n" +"POT-Creation-Date: 2023-03-13 14:50+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -18,79 +18,82 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #: web/template/invoices/products.gohtml:2 -#: web/template/invoices/products.gohtml:15 +#: web/template/invoices/products.gohtml:19 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/invoices/view.gohtml:9 -#: 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 +#: web/template/invoices/edit.gohtml:9 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 -#: web/template/invoices/view.gohtml:10 +#: web/template/invoices/view.gohtml:10 web/template/invoices/edit.gohtml:10 msgctxt "title" msgid "Invoices" msgstr "Factures" -#: web/template/invoices/products.gohtml:11 web/template/invoices/new.gohtml:2 +#: web/template/invoices/products.gohtml:12 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:42 +#: web/template/invoices/products.gohtml:47 #: web/template/products/index.gohtml:21 msgctxt "product" msgid "All" msgstr "Tots" -#: web/template/invoices/products.gohtml:43 +#: web/template/invoices/products.gohtml:48 #: web/template/products/index.gohtml:22 msgctxt "title" msgid "Name" msgstr "Nom" -#: web/template/invoices/products.gohtml:44 -#: web/template/invoices/view.gohtml:54 web/template/products/index.gohtml:23 +#: web/template/invoices/products.gohtml:49 +#: web/template/invoices/view.gohtml:56 web/template/products/index.gohtml:23 msgctxt "title" msgid "Price" msgstr "Preu" -#: web/template/invoices/products.gohtml:58 +#: web/template/invoices/products.gohtml:63 #: web/template/products/index.gohtml:37 msgid "No products added yet." msgstr "No hi ha cap producte." -#: web/template/invoices/products.gohtml:66 web/template/invoices/new.gohtml:62 +#: web/template/invoices/products.gohtml:71 web/template/invoices/new.gohtml:63 +#: web/template/invoices/edit.gohtml:64 msgctxt "action" msgid "Add products" msgstr "Afegeix productes" -#: web/template/invoices/new.gohtml:43 web/template/invoices/view.gohtml:59 +#: web/template/invoices/new.gohtml:44 web/template/invoices/view.gohtml:61 +#: web/template/invoices/edit.gohtml:45 msgctxt "title" msgid "Subtotal" msgstr "Subtotal" -#: web/template/invoices/new.gohtml:53 web/template/invoices/view.gohtml:63 -#: web/template/invoices/view.gohtml:103 +#: web/template/invoices/new.gohtml:54 web/template/invoices/view.gohtml:65 +#: web/template/invoices/view.gohtml:105 web/template/invoices/edit.gohtml:55 msgctxt "title" msgid "Total" msgstr "Total" -#: web/template/invoices/new.gohtml:65 +#: web/template/invoices/new.gohtml:66 web/template/invoices/edit.gohtml:67 msgctxt "action" msgid "Update" msgstr "Actualitza" -#: web/template/invoices/new.gohtml:67 web/template/invoices/index.gohtml:19 +#: web/template/invoices/new.gohtml:68 web/template/invoices/index.gohtml:19 msgctxt "action" msgid "New invoice" msgstr "Nova factura" @@ -105,7 +108,7 @@ msgctxt "invoice" msgid "All" msgstr "Totes" -#: web/template/invoices/index.gohtml:29 web/template/invoices/view.gohtml:26 +#: web/template/invoices/index.gohtml:29 web/template/invoices/view.gohtml:28 msgctxt "title" msgid "Date" msgstr "Data" @@ -150,45 +153,60 @@ msgctxt "action" msgid "Select invoice %v" msgstr "Selecciona factura %v" -#: web/template/invoices/index.gohtml:91 web/template/invoices/view.gohtml:14 +#: web/template/invoices/index.gohtml:92 web/template/invoices/view.gohtml:16 +msgctxt "action" +msgid "Edit" +msgstr "Edita" + +#: web/template/invoices/index.gohtml:98 web/template/invoices/view.gohtml:15 msgctxt "action" msgid "Duplicate" msgstr "Duplica" -#: web/template/invoices/index.gohtml:101 +#: web/template/invoices/index.gohtml:108 msgid "No invoices added yet." msgstr "No hi ha cap factura." -#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:25 +#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:27 msgctxt "title" msgid "Invoice %s" msgstr "Factura %s" -#: web/template/invoices/view.gohtml:17 +#: web/template/invoices/view.gohtml:19 msgctxt "action" msgid "Download invoice" msgstr "Descarrega factura" -#: web/template/invoices/view.gohtml:53 +#: web/template/invoices/view.gohtml:55 msgctxt "title" msgid "Concept" msgstr "Concepte" -#: web/template/invoices/view.gohtml:56 +#: web/template/invoices/view.gohtml:58 msgctxt "title" msgid "Discount" msgstr "Descompte" -#: web/template/invoices/view.gohtml:58 +#: web/template/invoices/view.gohtml:60 msgctxt "title" msgid "Units" msgstr "Unitats" -#: web/template/invoices/view.gohtml:93 +#: web/template/invoices/view.gohtml:95 msgctxt "title" msgid "Tax Base" msgstr "Base imposable" +#: web/template/invoices/edit.gohtml:2 web/template/invoices/edit.gohtml:15 +msgctxt "title" +msgid "Edit Invoice “%s”" +msgstr "Edició de la factura «%s»" + +#: web/template/invoices/edit.gohtml:69 +msgctxt "action" +msgid "Edit invoice" +msgstr "Edita factura" + #: web/template/dashboard.gohtml:2 msgctxt "title" msgid "Dashboard" @@ -428,44 +446,43 @@ msgstr "No podeu deixar la contrasenya en blanc." msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." -#: pkg/products.go:165 pkg/invoices.go:635 +#: pkg/products.go:165 pkg/invoices.go:643 msgctxt "input" msgid "Name" msgstr "Nom" -#: pkg/products.go:171 pkg/invoices.go:640 +#: pkg/products.go:171 pkg/invoices.go:648 msgctxt "input" msgid "Description" msgstr "Descripció" -#: pkg/products.go:176 pkg/invoices.go:644 +#: pkg/products.go:176 pkg/invoices.go:652 msgctxt "input" msgid "Price" msgstr "Preu" -#: pkg/products.go:186 pkg/invoices.go:670 +#: pkg/products.go:186 pkg/invoices.go:678 msgctxt "input" msgid "Taxes" msgstr "Imposts" -#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:532 -#: pkg/invoices.go:706 +#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:717 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." -#: pkg/products.go:207 pkg/invoices.go:707 +#: pkg/products.go:207 pkg/invoices.go:718 msgid "Price can not be empty." msgstr "No podeu deixar el preu en blanc." -#: pkg/products.go:208 pkg/invoices.go:708 +#: pkg/products.go:208 pkg/invoices.go:719 msgid "Price must be a number greater than zero." msgstr "El preu ha de ser un número major a zero." -#: pkg/products.go:210 pkg/invoices.go:716 +#: pkg/products.go:210 pkg/invoices.go:727 msgid "Selected tax is not valid." msgstr "Heu seleccionat un impost que no és vàlid." -#: pkg/products.go:211 pkg/invoices.go:717 +#: pkg/products.go:211 pkg/invoices.go:728 msgid "You can only select a tax of each class." msgstr "Només podeu seleccionar un impost de cada classe." @@ -573,88 +590,105 @@ 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 +#: pkg/invoices.go:303 msgid "Select a customer to bill." msgstr "Escolliu un client a facturar." -#: pkg/invoices.go:397 pkg/invoices.go:426 -msgid "Invalid action" -msgstr "Acció invàlida." - -#: pkg/invoices.go:420 +#: pkg/invoices.go:402 msgid "invoices.zip" msgstr "factures.zip" -#: pkg/invoices.go:468 +#: pkg/invoices.go:408 pkg/invoices.go:836 +msgid "Invalid action" +msgstr "Acció invàlida." + +#: pkg/invoices.go:452 +msgctxt "input" +msgid "Invoice Status" +msgstr "Estat de la factura" + +#: pkg/invoices.go:458 msgctxt "input" msgid "Customer" msgstr "Client" -#: pkg/invoices.go:474 +#: pkg/invoices.go:464 msgctxt "input" msgid "Number" msgstr "Número" -#: pkg/invoices.go:479 +#: pkg/invoices.go:469 msgctxt "input" msgid "Invoice Date" msgstr "Data de factura" -#: pkg/invoices.go:485 +#: pkg/invoices.go:475 msgctxt "input" msgid "Notes" msgstr "Notes" -#: pkg/invoices.go:490 +#: pkg/invoices.go:480 msgctxt "input" msgid "Tags" msgstr "Etiquetes" -#: pkg/invoices.go:496 +#: pkg/invoices.go:486 msgctxt "input" msgid "Payment Method" msgstr "Mètode de pagament" -#: pkg/invoices.go:533 +#: pkg/invoices.go:523 +msgid "Selected invoice status is not valid." +msgstr "Heu seleccionat un estat de factura que no és vàlid." + +#: pkg/invoices.go:524 +msgid "Selected customer is not valid." +msgstr "Heu seleccionat un client que no és vàlid." + +#: pkg/invoices.go:525 msgid "Invoice date can not be empty." msgstr "No podeu deixar la data de la factura en blanc." -#: pkg/invoices.go:534 +#: pkg/invoices.go:526 msgid "Invoice date must be a valid date." msgstr "La data de facturació ha de ser vàlida." -#: pkg/invoices.go:536 +#: pkg/invoices.go:528 msgid "Selected payment method is not valid." msgstr "Heu seleccionat un mètode de pagament que no és vàlid." -#: pkg/invoices.go:630 +#: pkg/invoices.go:633 pkg/invoices.go:638 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/invoices.go:653 +#: pkg/invoices.go:661 msgctxt "input" msgid "Quantity" msgstr "Quantitat" -#: pkg/invoices.go:661 +#: pkg/invoices.go:669 msgctxt "input" msgid "Discount (%)" msgstr "Descompte (%)" -#: pkg/invoices.go:710 +#: pkg/invoices.go:716 +msgid "Product ID can not be empty." +msgstr "No podeu deixar l’identificador del producte en blanc." + +#: pkg/invoices.go:721 msgid "Quantity can not be empty." msgstr "No podeu deixar la quantitat en blanc." -#: pkg/invoices.go:711 +#: pkg/invoices.go:722 msgid "Quantity must be a number greater than zero." msgstr "La quantitat ha de ser un número major a zero." -#: pkg/invoices.go:713 +#: pkg/invoices.go:724 msgid "Discount can not be empty." msgstr "No podeu deixar el descompte en blanc." -#: pkg/invoices.go:714 +#: pkg/invoices.go:725 msgid "Discount must be a percentage between 0 and 100." msgstr "El descompte ha de ser un percentatge entre 0 i 100." diff --git a/po/es.po b/po/es.po index 4a8374d..4205e66 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-03-10 13:59+0100\n" +"POT-Creation-Date: 2023-03-13 14:50+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -18,79 +18,82 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: web/template/invoices/products.gohtml:2 -#: web/template/invoices/products.gohtml:15 +#: web/template/invoices/products.gohtml:19 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/invoices/view.gohtml:9 -#: 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 +#: web/template/invoices/edit.gohtml:9 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 -#: web/template/invoices/view.gohtml:10 +#: web/template/invoices/view.gohtml:10 web/template/invoices/edit.gohtml:10 msgctxt "title" msgid "Invoices" msgstr "Facturas" -#: web/template/invoices/products.gohtml:11 web/template/invoices/new.gohtml:2 +#: web/template/invoices/products.gohtml:12 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:42 +#: web/template/invoices/products.gohtml:47 #: web/template/products/index.gohtml:21 msgctxt "product" msgid "All" msgstr "Todos" -#: web/template/invoices/products.gohtml:43 +#: web/template/invoices/products.gohtml:48 #: web/template/products/index.gohtml:22 msgctxt "title" msgid "Name" msgstr "Nombre" -#: web/template/invoices/products.gohtml:44 -#: web/template/invoices/view.gohtml:54 web/template/products/index.gohtml:23 +#: web/template/invoices/products.gohtml:49 +#: web/template/invoices/view.gohtml:56 web/template/products/index.gohtml:23 msgctxt "title" msgid "Price" msgstr "Precio" -#: web/template/invoices/products.gohtml:58 +#: web/template/invoices/products.gohtml:63 #: web/template/products/index.gohtml:37 msgid "No products added yet." msgstr "No hay productos." -#: web/template/invoices/products.gohtml:66 web/template/invoices/new.gohtml:62 +#: web/template/invoices/products.gohtml:71 web/template/invoices/new.gohtml:63 +#: web/template/invoices/edit.gohtml:64 msgctxt "action" msgid "Add products" msgstr "Añadir productos" -#: web/template/invoices/new.gohtml:43 web/template/invoices/view.gohtml:59 +#: web/template/invoices/new.gohtml:44 web/template/invoices/view.gohtml:61 +#: web/template/invoices/edit.gohtml:45 msgctxt "title" msgid "Subtotal" msgstr "Subtotal" -#: web/template/invoices/new.gohtml:53 web/template/invoices/view.gohtml:63 -#: web/template/invoices/view.gohtml:103 +#: web/template/invoices/new.gohtml:54 web/template/invoices/view.gohtml:65 +#: web/template/invoices/view.gohtml:105 web/template/invoices/edit.gohtml:55 msgctxt "title" msgid "Total" msgstr "Total" -#: web/template/invoices/new.gohtml:65 +#: web/template/invoices/new.gohtml:66 web/template/invoices/edit.gohtml:67 msgctxt "action" msgid "Update" msgstr "Actualizar" -#: web/template/invoices/new.gohtml:67 web/template/invoices/index.gohtml:19 +#: web/template/invoices/new.gohtml:68 web/template/invoices/index.gohtml:19 msgctxt "action" msgid "New invoice" msgstr "Nueva factura" @@ -105,7 +108,7 @@ msgctxt "invoice" msgid "All" msgstr "Todas" -#: web/template/invoices/index.gohtml:29 web/template/invoices/view.gohtml:26 +#: web/template/invoices/index.gohtml:29 web/template/invoices/view.gohtml:28 msgctxt "title" msgid "Date" msgstr "Fecha" @@ -150,45 +153,60 @@ msgctxt "action" msgid "Select invoice %v" msgstr "Seleccionar factura %v" -#: web/template/invoices/index.gohtml:91 web/template/invoices/view.gohtml:14 +#: web/template/invoices/index.gohtml:92 web/template/invoices/view.gohtml:16 +msgctxt "action" +msgid "Edit" +msgstr "Editar" + +#: web/template/invoices/index.gohtml:98 web/template/invoices/view.gohtml:15 msgctxt "action" msgid "Duplicate" msgstr "Duplicar" -#: web/template/invoices/index.gohtml:101 +#: web/template/invoices/index.gohtml:108 msgid "No invoices added yet." msgstr "No hay facturas." -#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:25 +#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:27 msgctxt "title" msgid "Invoice %s" msgstr "Factura %s" -#: web/template/invoices/view.gohtml:17 +#: web/template/invoices/view.gohtml:19 msgctxt "action" msgid "Download invoice" msgstr "Descargar factura" -#: web/template/invoices/view.gohtml:53 +#: web/template/invoices/view.gohtml:55 msgctxt "title" msgid "Concept" msgstr "Concepto" -#: web/template/invoices/view.gohtml:56 +#: web/template/invoices/view.gohtml:58 msgctxt "title" msgid "Discount" msgstr "Descuento" -#: web/template/invoices/view.gohtml:58 +#: web/template/invoices/view.gohtml:60 msgctxt "title" msgid "Units" msgstr "Unidades" -#: web/template/invoices/view.gohtml:93 +#: web/template/invoices/view.gohtml:95 msgctxt "title" msgid "Tax Base" msgstr "Base imponible" +#: web/template/invoices/edit.gohtml:2 web/template/invoices/edit.gohtml:15 +msgctxt "title" +msgid "Edit Invoice “%s”" +msgstr "Edición del la factura «%s»" + +#: web/template/invoices/edit.gohtml:69 +msgctxt "action" +msgid "Edit invoice" +msgstr "Editar factura" + #: web/template/dashboard.gohtml:2 msgctxt "title" msgid "Dashboard" @@ -428,44 +446,43 @@ 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:165 pkg/invoices.go:635 +#: pkg/products.go:165 pkg/invoices.go:643 msgctxt "input" msgid "Name" msgstr "Nombre" -#: pkg/products.go:171 pkg/invoices.go:640 +#: pkg/products.go:171 pkg/invoices.go:648 msgctxt "input" msgid "Description" msgstr "Descripción" -#: pkg/products.go:176 pkg/invoices.go:644 +#: pkg/products.go:176 pkg/invoices.go:652 msgctxt "input" msgid "Price" msgstr "Precio" -#: pkg/products.go:186 pkg/invoices.go:670 +#: pkg/products.go:186 pkg/invoices.go:678 msgctxt "input" msgid "Taxes" msgstr "Impuestos" -#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:532 -#: pkg/invoices.go:706 +#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:717 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." -#: pkg/products.go:207 pkg/invoices.go:707 +#: pkg/products.go:207 pkg/invoices.go:718 msgid "Price can not be empty." msgstr "No podéis dejar el precio en blanco." -#: pkg/products.go:208 pkg/invoices.go:708 +#: pkg/products.go:208 pkg/invoices.go:719 msgid "Price must be a number greater than zero." msgstr "El precio tiene que ser un número mayor a cero." -#: pkg/products.go:210 pkg/invoices.go:716 +#: pkg/products.go:210 pkg/invoices.go:727 msgid "Selected tax is not valid." msgstr "Habéis escogido un impuesto que no es válido." -#: pkg/products.go:211 pkg/invoices.go:717 +#: pkg/products.go:211 pkg/invoices.go:728 msgid "You can only select a tax of each class." msgstr "Solo podéis escoger un impuesto de cada clase." @@ -573,88 +590,105 @@ 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 +#: pkg/invoices.go:303 msgid "Select a customer to bill." msgstr "Escoged un cliente a facturar." -#: pkg/invoices.go:397 pkg/invoices.go:426 -msgid "Invalid action" -msgstr "Acción inválida." - -#: pkg/invoices.go:420 +#: pkg/invoices.go:402 msgid "invoices.zip" msgstr "facturas.zip" -#: pkg/invoices.go:468 +#: pkg/invoices.go:408 pkg/invoices.go:836 +msgid "Invalid action" +msgstr "Acción inválida." + +#: pkg/invoices.go:452 +msgctxt "input" +msgid "Invoice Status" +msgstr "Estado de la factura" + +#: pkg/invoices.go:458 msgctxt "input" msgid "Customer" msgstr "Cliente" -#: pkg/invoices.go:474 +#: pkg/invoices.go:464 msgctxt "input" msgid "Number" msgstr "Número" -#: pkg/invoices.go:479 +#: pkg/invoices.go:469 msgctxt "input" msgid "Invoice Date" msgstr "Fecha de factura" -#: pkg/invoices.go:485 +#: pkg/invoices.go:475 msgctxt "input" msgid "Notes" msgstr "Notas" -#: pkg/invoices.go:490 +#: pkg/invoices.go:480 msgctxt "input" msgid "Tags" msgstr "Etiquetes" -#: pkg/invoices.go:496 +#: pkg/invoices.go:486 msgctxt "input" msgid "Payment Method" msgstr "Método de pago" -#: pkg/invoices.go:533 +#: pkg/invoices.go:523 +msgid "Selected invoice status is not valid." +msgstr "Habéis escogido un estado de factura que no es válido." + +#: pkg/invoices.go:524 +msgid "Selected customer is not valid." +msgstr "Habéis escogido un cliente que no es válido." + +#: pkg/invoices.go:525 msgid "Invoice date can not be empty." msgstr "No podéis dejar la fecha de la factura en blanco." -#: pkg/invoices.go:534 +#: pkg/invoices.go:526 msgid "Invoice date must be a valid date." msgstr "La fecha de factura debe ser válida." -#: pkg/invoices.go:536 +#: pkg/invoices.go:528 msgid "Selected payment method is not valid." msgstr "Habéis escogido un método de pago que no es válido." -#: pkg/invoices.go:630 +#: pkg/invoices.go:633 pkg/invoices.go:638 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/invoices.go:653 +#: pkg/invoices.go:661 msgctxt "input" msgid "Quantity" msgstr "Cantidad" -#: pkg/invoices.go:661 +#: pkg/invoices.go:669 msgctxt "input" msgid "Discount (%)" msgstr "Descuento (%)" -#: pkg/invoices.go:710 +#: pkg/invoices.go:716 +msgid "Product ID can not be empty." +msgstr "No podéis dejar el identificador de producto en blanco." + +#: pkg/invoices.go:721 msgid "Quantity can not be empty." msgstr "No podéis dejar la cantidad en blanco." -#: pkg/invoices.go:711 +#: pkg/invoices.go:722 msgid "Quantity must be a number greater than zero." msgstr "La cantidad tiene que ser un número mayor a cero." -#: pkg/invoices.go:713 +#: pkg/invoices.go:724 msgid "Discount can not be empty." msgstr "No podéis dejar el descuento en blanco." -#: pkg/invoices.go:714 +#: pkg/invoices.go:725 msgid "Discount must be a percentage between 0 and 100." msgstr "El descuento tiene que ser un porcentaje entre 0 y 100." diff --git a/web/template/invoices/edit.gohtml b/web/template/invoices/edit.gohtml new file mode 100644 index 0000000..e2875c3 --- /dev/null +++ b/web/template/invoices/edit.gohtml @@ -0,0 +1,74 @@ +{{ define "title" -}} + {{ printf ( pgettext "Edit Invoice “%s”" "title" ) .Number }} +{{- end }} + +{{ define "content" }} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editInvoicePage*/ -}} + +
+

{{ printf (pgettext "Edit Invoice “%s”" "title") .Number }}

+
+ {{ csrfToken }} + + {{ with .Form -}} + {{ template "select-field" .Customer }} + {{ template "hidden-field" .Number }} + {{ template "hidden-field" .Date }} + {{ template "input-field" .Tags }} + {{ template "select-field" .PaymentMethod }} + {{ template "select-field" .InvoiceStatus }} + {{ template "input-field" .Notes }} + + {{- range $product := .Products }} +
+ {{ template "hidden-field" .InvoiceProductId }} + {{ template "hidden-field" .ProductId }} + {{ template "input-field" .Name }} + {{ template "input-field" .Price }} + {{ template "input-field" .Quantity }} + {{ template "input-field" .Discount }} + {{ template "input-field" .Description }} + {{ template "select-field" .Tax }} +
+ {{- end }} + {{- end }} + + + + + + + + {{- range $tax := .Taxes }} + + + + + {{- end }} + + + + + +
{{(pgettext "Subtotal" "title")}}{{ .Subtotal | formatPrice }}
{{ index . 0 }}{{ index . 1 | formatPrice }}
{{(pgettext "Total" "title")}}{{ .Total | formatPrice }}
+ +
+ + + +
+ +
+
+{{- end }} diff --git a/web/template/invoices/index.gohtml b/web/template/invoices/index.gohtml index 5dd6f49..18ff608 100644 --- a/web/template/invoices/index.gohtml +++ b/web/template/invoices/index.gohtml @@ -54,12 +54,13 @@
{{ csrfToken }} {{ putMethod }} +