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 {
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()
var entries []*InvoiceEntry
@ -92,12 +92,12 @@ func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Para
if err != nil {
panic(err)
}
defer stdout.Close()
defer mustClose(stdout)
if err = cmd.Start(); err != nil {
panic(err)
}
go func() {
defer stdin.Close()
defer mustClose(stdin)
mustRenderAppTemplate(stdin, r, "invoices/view.gohtml", invoice)
}()
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 {
Number string
Slug string
@ -256,28 +262,16 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param
http.Error(w, err.Error(), http.StatusForbidden)
return
}
switch r.Form.Get("action") {
case "update":
form.Update()
w.WriteHeader(http.StatusOK)
mustRenderNewInvoiceForm(w, r, form)
case "products":
w.WriteHeader(http.StatusOK)
mustRenderNewInvoiceProductsForm(w, r, form)
case "add":
if !form.Validate() {
w.WriteHeader(http.StatusUnprocessableEntity)
mustRenderNewInvoiceForm(w, r, form)
return
}
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))
http.Redirect(w, r, companyURI(company, "/invoices"), http.StatusSeeOther)
default:
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
}
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/"+slug), http.StatusSeeOther)
}
func HandleAddProductsToInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func HandleNewInvoiceAction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
@ -286,25 +280,25 @@ func HandleAddProductsToInvoice(w http.ResponseWriter, r *http.Request, _ httpro
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
index := len(form.Products)
productsId := r.Form["id"]
rows := conn.MustQuery(r.Context(), "select product_id, name, description, to_price(price, decimal_digits), 1 as quantity, 0 as discount, array_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)
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
form.Products = append(form.Products, product)
index++
}
if rows.Err() != nil {
panic(rows.Err())
}
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)
default:
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
}
}
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 {
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.POST("/invoices", HandleAddInvoice)
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) {
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
})

View File

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

View File

@ -13,7 +13,7 @@
</nav>
<section class="dialog-content">
<h2>{{(pgettext "Add Products to Invoice" "title")}}</h2>
<form method="POST" action="{{ companyURI "/invoices/new/products" }}">
<form method="POST" action="{{ companyURI "/invoices/new" }}">
{{ csrfToken }}
{{- with .Form }}
@ -61,7 +61,8 @@
</table>
<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>
</form>
</section>