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" )}}