From 8efae0485e9e72c844bff6594d8160250e419621 Mon Sep 17 00:00:00 2001
From: jordi fita mas
Date: Mon, 13 Mar 2023 15:00:35 +0100
Subject: [PATCH] Add the edit form for invoices
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
I had to change the way /invoices/new and /invoices/batch are handled,
because httprouter was not happy with the new POST /invoices/:slug/edit
route, claiming that /invoices/:slug conflicts with the previously
existing routes.
I also could not make it work with the PATCH method, even though i
correctly added the patchMethod override function, therefore editing
invoices is also weird because i have to take into account the “quick”
invoice status change.
I use the same form for both new and edit invoices, because the only
changes are that we can not edit the invoice date and number, by
Oriol’s design, but must be able to change the status; very similar
forms.
---
pkg/invoices.go | 209 +++++++++++++++++++-------
pkg/pgtypes.go | 64 ++++++++
pkg/router.go | 5 +-
po/ca.po | 154 +++++++++++--------
po/es.po | 154 +++++++++++--------
web/template/invoices/edit.gohtml | 74 +++++++++
web/template/invoices/index.gohtml | 9 +-
web/template/invoices/new.gohtml | 3 +-
web/template/invoices/products.gohtml | 9 +-
web/template/invoices/view.gohtml | 4 +-
10 files changed, 504 insertions(+), 181 deletions(-)
create mode 100644 web/template/invoices/edit.gohtml
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 }}
+
+
+{{- 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 @@
- {{( pgettext "Duplicate" "action" )}}
+ {{( pgettext "Duplicate" "action" )}}
+ {{( pgettext "Edit" "action" )}}
{{( pgettext "Download invoice" "action" )}}