Compare commits

...

4 Commits

Author SHA1 Message Date
jordi fita mas 0d4fb124b4 Keep all “new invoice actions” on the same /new URI 2023-02-27 13:13:28 +01:00
jordi fita mas bc7ed0f06f Tell Goland to go fuck itself with the unhandled errors in close 2023-02-27 12:55:18 +01:00
jordi fita mas 2ef75efda8 Sort invoices so that the first is the most recent 2023-02-27 12:48:56 +01:00
jordi fita mas c7ac82d6ea Redirect to the newly created invoice on add
It makes more sense to see the invoice once created than to return to
the list of invoices and having to look for it.
2023-02-27 12:45:32 +01:00
4 changed files with 57 additions and 43 deletions

View File

@ -39,7 +39,7 @@ func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
} }
func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale) []*InvoiceEntry { func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale) []*InvoiceEntry {
rows := conn.MustQuery(ctx, "select invoice.slug, invoice_date, invoice_number, contact.business_name, contact.slug, invoice.invoice_status, isi18n.name, to_price(total, decimal_digits) from invoice join contact using (contact_id) join invoice_status_i18n isi18n on invoice.invoice_status = isi18n.invoice_status and isi18n.lang_tag = $2 join invoice_amount using (invoice_id) join currency using (currency_code) where invoice.company_id = $1 order by invoice_date, invoice_number", company.Id, locale.Language.String()) rows := conn.MustQuery(ctx, "select invoice.slug, invoice_date, invoice_number, contact.business_name, contact.slug, invoice.invoice_status, isi18n.name, to_price(total, decimal_digits) from invoice join contact using (contact_id) join invoice_status_i18n isi18n on invoice.invoice_status = isi18n.invoice_status and isi18n.lang_tag = $2 join invoice_amount using (invoice_id) join currency using (currency_code) where invoice.company_id = $1 order by invoice_date desc, invoice_number desc", company.Id, locale.Language.String())
defer rows.Close() defer rows.Close()
var entries []*InvoiceEntry var entries []*InvoiceEntry
@ -92,12 +92,12 @@ func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Para
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer stdout.Close() defer mustClose(stdout)
if err = cmd.Start(); err != nil { if err = cmd.Start(); err != nil {
panic(err) panic(err)
} }
go func() { go func() {
defer stdin.Close() defer mustClose(stdin)
mustRenderAppTemplate(stdin, r, "invoices/view.gohtml", invoice) mustRenderAppTemplate(stdin, r, "invoices/view.gohtml", invoice)
}() }()
w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Type", "application/pdf")
@ -113,6 +113,12 @@ func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Para
} }
} }
func mustClose(closer io.Closer) {
if err := closer.Close(); err != nil {
panic(err)
}
}
type invoice struct { type invoice struct {
Number string Number string
Slug string Slug string
@ -256,28 +262,16 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param
http.Error(w, err.Error(), http.StatusForbidden) http.Error(w, err.Error(), http.StatusForbidden)
return return
} }
switch r.Form.Get("action") {
case "update":
form.Update()
w.WriteHeader(http.StatusOK)
mustRenderNewInvoiceForm(w, r, form)
case "products":
w.WriteHeader(http.StatusOK)
mustRenderNewInvoiceProductsForm(w, r, form)
case "add":
if !form.Validate() { if !form.Validate() {
w.WriteHeader(http.StatusUnprocessableEntity) w.WriteHeader(http.StatusUnprocessableEntity)
mustRenderNewInvoiceForm(w, r, form) mustRenderNewInvoiceForm(w, r, form)
return return
} }
conn.MustExec(r.Context(), "select add_invoice($1, $2, $3, $4, $5, $6)", company.Id, form.Number, form.Date, form.Customer, form.Notes, NewInvoiceProductArray(form.Products)) slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6)", company.Id, form.Number, form.Date, form.Customer, form.Notes, NewInvoiceProductArray(form.Products))
http.Redirect(w, r, companyURI(company, "/invoices"), http.StatusSeeOther) http.Redirect(w, r, companyURI(company, "/invoices/"+slug), http.StatusSeeOther)
default:
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
}
} }
func HandleAddProductsToInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func HandleNewInvoiceAction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
locale := getLocale(r) locale := getLocale(r)
conn := getConn(r) conn := getConn(r)
company := mustGetCompany(r) company := mustGetCompany(r)
@ -286,25 +280,25 @@ func HandleAddProductsToInvoice(w http.ResponseWriter, r *http.Request, _ httpro
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
if err := verifyCsrfTokenValid(r); err != nil {
index := len(form.Products) http.Error(w, err.Error(), http.StatusForbidden)
productsId := r.Form["id"] return
rows := conn.MustQuery(r.Context(), "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)
defer rows.Close()
for rows.Next() {
product := newInvoiceProductForm(index, company, locale, form.Tax.Options)
if err := rows.Scan(product.ProductId, product.Name, product.Description, product.Price, product.Quantity, product.Discount, product.Tax); err != nil {
panic(err)
} }
form.Products = append(form.Products, product) switch r.Form.Get("action") {
index++ case "update":
} form.Update()
if rows.Err() != nil {
panic(rows.Err())
}
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
mustRenderNewInvoiceForm(w, r, form) 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)
default:
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
}
} }
type invoiceForm struct { type invoiceForm struct {
@ -406,6 +400,23 @@ func (form *invoiceForm) Update() {
} }
} }
func (form *invoiceForm) AddProducts(ctx context.Context, conn *Conn, productsId []string) {
index := len(form.Products)
rows := conn.MustQuery(ctx, "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)
defer rows.Close()
for rows.Next() {
product := newInvoiceProductForm(index, form.company, form.locale, form.Tax.Options)
if err := rows.Scan(product.ProductId, product.Name, product.Description, product.Price, product.Quantity, product.Discount, product.Tax); err != nil {
panic(err)
}
form.Products = append(form.Products, product)
index++
}
if rows.Err() != nil {
panic(rows.Err())
}
}
func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption { func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption {
return MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id) return MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id)
} }

View File

@ -25,7 +25,7 @@ func NewRouter(db *Db) http.Handler {
companyRouter.GET("/invoices", IndexInvoices) companyRouter.GET("/invoices", IndexInvoices)
companyRouter.POST("/invoices", HandleAddInvoice) companyRouter.POST("/invoices", HandleAddInvoice)
companyRouter.GET("/invoices/:slug", ServeInvoice) companyRouter.GET("/invoices/:slug", ServeInvoice)
companyRouter.POST("/invoices/new/products", HandleAddProductsToInvoice) companyRouter.POST("/invoices/new", HandleNewInvoiceAction)
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil) mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
}) })

View File

@ -55,9 +55,11 @@
</table> </table>
<fieldset> <fieldset>
<button formnovalidate name="action" value="products" <button formnovalidate formaction="{{ companyURI "/invoices/new" }}"
name="action" value="select-products"
type="submit">{{( pgettext "Add products" "action" )}}</button> type="submit">{{( pgettext "Add products" "action" )}}</button>
<button formnovalidate name="action" value="update" <button formnovalidate formaction="{{ companyURI "/invoices/new" }}"
name="action" value="update"
type="submit">{{( pgettext "Update" "action" )}}</button> type="submit">{{( pgettext "Update" "action" )}}</button>
<button class="primary" name="action" value="add" <button class="primary" name="action" value="add"
type="submit">{{( pgettext "New invoice" "action" )}}</button> type="submit">{{( pgettext "New invoice" "action" )}}</button>

View File

@ -13,7 +13,7 @@
</nav> </nav>
<section class="dialog-content"> <section class="dialog-content">
<h2>{{(pgettext "Add Products to Invoice" "title")}}</h2> <h2>{{(pgettext "Add Products to Invoice" "title")}}</h2>
<form method="POST" action="{{ companyURI "/invoices/new/products" }}"> <form method="POST" action="{{ companyURI "/invoices/new" }}">
{{ csrfToken }} {{ csrfToken }}
{{- with .Form }} {{- with .Form }}
@ -61,7 +61,8 @@
</table> </table>
<fieldset> <fieldset>
<button class="primary" type="submit">{{( pgettext "Add products" "action" )}}</button> <button class="primary" type="submit"
name="action" value="add-products">{{( pgettext "Add products" "action" )}}</button>
</fieldset> </fieldset>
</form> </form>
</section> </section>