2023-02-11 21:16:48 +00:00
package pkg
import (
2023-03-09 11:11:53 +00:00
"archive/zip"
Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.
I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.
I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance. This works, but i would need a Chrome running as a
daemon.
I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.
To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option. I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.
Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 16:26:09 +00:00
"bytes"
2023-02-11 21:16:48 +00:00
"context"
2023-03-09 11:11:53 +00:00
"errors"
2023-02-11 21:16:48 +00:00
"fmt"
"github.com/julienschmidt/httprouter"
"html/template"
Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.
I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.
I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance. This works, but i would need a Chrome running as a
daemon.
I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.
To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option. I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.
Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 16:26:09 +00:00
"io"
"log"
2023-02-12 20:06:48 +00:00
"math"
2023-02-11 21:16:48 +00:00
"net/http"
2023-03-09 11:11:53 +00:00
"os"
Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.
I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.
I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance. This works, but i would need a Chrome running as a
daemon.
I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.
To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option. I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.
Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 16:26:09 +00:00
"os/exec"
2023-03-01 13:08:12 +00:00
"sort"
2023-02-12 20:06:48 +00:00
"strconv"
Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.
I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.
I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance. This works, but i would need a Chrome running as a
daemon.
I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.
To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option. I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.
Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 16:26:09 +00:00
"strings"
2023-02-11 21:16:48 +00:00
"time"
)
2023-02-12 20:06:48 +00:00
type InvoiceEntry struct {
Slug string
Date time . Time
Number string
2023-02-22 13:39:38 +00:00
Total string
2023-02-12 20:06:48 +00:00
CustomerName string
2023-03-10 13:02:55 +00:00
Tags [ ] string
2023-02-12 20:06:48 +00:00
Status string
StatusLabel string
}
2023-02-11 21:16:48 +00:00
type InvoicesIndexPage struct {
2023-03-07 10:52:09 +00:00
Invoices [ ] * InvoiceEntry
Add filters form for invoices
Instead of using links in the invoice tags, that we will replace with a
“click-to-edit field”, with Oriol agreed to add a form with filters that
includes not only the tags but also dates, customer, status, and the
invoice number.
This means i now need dynamic SQL, and i do not think this belongs to
the database (i.e., no PL/pgSQL function for that). I have looked at
query builder libraries for Golang, and did not find anything that
suited me: either they wanted to manage not only the SQL query but also
all structs, or they managed to confuse Goland’s SQL analyzer.
For now, at least, i am using a very simple approach with arrays, that
still confuses Goland’s analyzer, but just in a very specific part,
which i find tolerable—not that their analyzer is that great to begin
with, but that’s a story for another day.
2023-03-29 14:16:31 +00:00
Filters * invoiceFilterForm
2023-03-07 10:52:09 +00:00
InvoiceStatuses map [ string ] string
2023-02-11 21:16:48 +00:00
}
func IndexInvoices ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
2023-03-07 10:52:09 +00:00
conn := getConn ( r )
locale := getLocale ( r )
Add filters form for invoices
Instead of using links in the invoice tags, that we will replace with a
“click-to-edit field”, with Oriol agreed to add a form with filters that
includes not only the tags but also dates, customer, status, and the
invoice number.
This means i now need dynamic SQL, and i do not think this belongs to
the database (i.e., no PL/pgSQL function for that). I have looked at
query builder libraries for Golang, and did not find anything that
suited me: either they wanted to manage not only the SQL query but also
all structs, or they managed to confuse Goland’s SQL analyzer.
For now, at least, i am using a very simple approach with arrays, that
still confuses Goland’s analyzer, but just in a very specific part,
which i find tolerable—not that their analyzer is that great to begin
with, but that’s a story for another day.
2023-03-29 14:16:31 +00:00
company := mustGetCompany ( r )
filters := newInvoiceFilterForm ( r . Context ( ) , conn , locale , company )
if err := filters . Parse ( r ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
2023-02-12 20:06:48 +00:00
page := & InvoicesIndexPage {
Add filters form for invoices
Instead of using links in the invoice tags, that we will replace with a
“click-to-edit field”, with Oriol agreed to add a form with filters that
includes not only the tags but also dates, customer, status, and the
invoice number.
This means i now need dynamic SQL, and i do not think this belongs to
the database (i.e., no PL/pgSQL function for that). I have looked at
query builder libraries for Golang, and did not find anything that
suited me: either they wanted to manage not only the SQL query but also
all structs, or they managed to confuse Goland’s SQL analyzer.
For now, at least, i am using a very simple approach with arrays, that
still confuses Goland’s analyzer, but just in a very specific part,
which i find tolerable—not that their analyzer is that great to begin
with, but that’s a story for another day.
2023-03-29 14:16:31 +00:00
Invoices : mustCollectInvoiceEntries ( r . Context ( ) , conn , company , locale , filters ) ,
Filters : filters ,
2023-03-07 10:52:09 +00:00
InvoiceStatuses : mustCollectInvoiceStatuses ( r . Context ( ) , conn , locale ) ,
2023-02-12 20:06:48 +00:00
}
2023-03-23 09:55:02 +00:00
mustRenderMainTemplate ( w , r , "invoices/index.gohtml" , page )
2023-02-11 21:16:48 +00:00
}
Add filters form for invoices
Instead of using links in the invoice tags, that we will replace with a
“click-to-edit field”, with Oriol agreed to add a form with filters that
includes not only the tags but also dates, customer, status, and the
invoice number.
This means i now need dynamic SQL, and i do not think this belongs to
the database (i.e., no PL/pgSQL function for that). I have looked at
query builder libraries for Golang, and did not find anything that
suited me: either they wanted to manage not only the SQL query but also
all structs, or they managed to confuse Goland’s SQL analyzer.
For now, at least, i am using a very simple approach with arrays, that
still confuses Goland’s analyzer, but just in a very specific part,
which i find tolerable—not that their analyzer is that great to begin
with, but that’s a story for another day.
2023-03-29 14:16:31 +00:00
func mustCollectInvoiceEntries ( ctx context . Context , conn * Conn , company * Company , locale * Locale , filters * invoiceFilterForm ) [ ] * InvoiceEntry {
args := [ ] interface { } { locale . Language . String ( ) , company . Id }
where := [ ] string { "invoice.company_id = $2" }
appendWhere := func ( expression string , value interface { } ) {
args = append ( args , value )
where = append ( where , fmt . Sprintf ( expression , len ( args ) ) )
}
maybeAppendWhere := func ( expression string , value string , conv func ( string ) interface { } ) {
if value != "" {
if conv == nil {
appendWhere ( expression , value )
} else {
appendWhere ( expression , conv ( value ) )
}
}
}
maybeAppendWhere ( "contact_id = $%d" , filters . Customer . String ( ) , func ( v string ) interface { } {
customerId , _ := strconv . Atoi ( filters . Customer . Selected [ 0 ] )
return customerId
} )
maybeAppendWhere ( "invoice.invoice_status = $%d" , filters . InvoiceStatus . String ( ) , nil )
maybeAppendWhere ( "invoice_number = $%d" , filters . InvoiceNumber . String ( ) , nil )
maybeAppendWhere ( "invoice_date >= $%d" , filters . FromDate . String ( ) , nil )
maybeAppendWhere ( "invoice_date <= $%d" , filters . ToDate . String ( ) , nil )
if len ( filters . Tags . Tags ) > 0 {
appendWhere ( "exists (select 1 from invoice_tag join tag using (tag_id) where invoice_tag.invoice_id = invoice.invoice_id and tag.name = any($%d))" , filters . Tags )
}
rows := conn . MustQuery ( ctx , fmt . Sprintf ( `
2023-03-10 13:02:55 +00:00
select invoice . slug
, invoice_date
, invoice_number
, contact . business_name
2023-03-19 22:10:01 +00:00
, array_agg ( coalesce ( tag . name : : text , ' ' ) )
2023-03-10 13:02:55 +00:00
, invoice . invoice_status
, isi18n . name
, to_price ( total , decimal_digits )
from invoice
left join invoice_tag using ( invoice_id )
left join tag using ( tag_id )
join contact using ( contact_id )
Add filters form for invoices
Instead of using links in the invoice tags, that we will replace with a
“click-to-edit field”, with Oriol agreed to add a form with filters that
includes not only the tags but also dates, customer, status, and the
invoice number.
This means i now need dynamic SQL, and i do not think this belongs to
the database (i.e., no PL/pgSQL function for that). I have looked at
query builder libraries for Golang, and did not find anything that
suited me: either they wanted to manage not only the SQL query but also
all structs, or they managed to confuse Goland’s SQL analyzer.
For now, at least, i am using a very simple approach with arrays, that
still confuses Goland’s analyzer, but just in a very specific part,
which i find tolerable—not that their analyzer is that great to begin
with, but that’s a story for another day.
2023-03-29 14:16:31 +00:00
join invoice_status_i18n isi18n on invoice . invoice_status = isi18n . invoice_status and isi18n . lang_tag = $ 1
2023-03-10 13:02:55 +00:00
join invoice_amount using ( invoice_id )
join currency using ( currency_code )
Add filters form for invoices
Instead of using links in the invoice tags, that we will replace with a
“click-to-edit field”, with Oriol agreed to add a form with filters that
includes not only the tags but also dates, customer, status, and the
invoice number.
This means i now need dynamic SQL, and i do not think this belongs to
the database (i.e., no PL/pgSQL function for that). I have looked at
query builder libraries for Golang, and did not find anything that
suited me: either they wanted to manage not only the SQL query but also
all structs, or they managed to confuse Goland’s SQL analyzer.
For now, at least, i am using a very simple approach with arrays, that
still confuses Goland’s analyzer, but just in a very specific part,
which i find tolerable—not that their analyzer is that great to begin
with, but that’s a story for another day.
2023-03-29 14:16:31 +00:00
where ( % s )
2023-03-10 13:02:55 +00:00
group by invoice . slug
, invoice_date
, invoice_number
, contact . business_name
, invoice . invoice_status
, isi18n . name
, total
, decimal_digits
order by invoice_date desc
, invoice_number desc
Add filters form for invoices
Instead of using links in the invoice tags, that we will replace with a
“click-to-edit field”, with Oriol agreed to add a form with filters that
includes not only the tags but also dates, customer, status, and the
invoice number.
This means i now need dynamic SQL, and i do not think this belongs to
the database (i.e., no PL/pgSQL function for that). I have looked at
query builder libraries for Golang, and did not find anything that
suited me: either they wanted to manage not only the SQL query but also
all structs, or they managed to confuse Goland’s SQL analyzer.
For now, at least, i am using a very simple approach with arrays, that
still confuses Goland’s analyzer, but just in a very specific part,
which i find tolerable—not that their analyzer is that great to begin
with, but that’s a story for another day.
2023-03-29 14:16:31 +00:00
` , strings . Join ( where , ") AND (" ) ) , args ... )
2023-02-12 20:06:48 +00:00
defer rows . Close ( )
var entries [ ] * InvoiceEntry
for rows . Next ( ) {
entry := & InvoiceEntry { }
2023-03-28 07:50:19 +00:00
if err := rows . Scan ( & entry . Slug , & entry . Date , & entry . Number , & entry . CustomerName , & entry . Tags , & entry . Status , & entry . StatusLabel , & entry . Total ) ; err != nil {
2023-02-12 20:06:48 +00:00
panic ( err )
}
entries = append ( entries , entry )
}
if rows . Err ( ) != nil {
panic ( rows . Err ( ) )
}
return entries
}
2023-03-07 10:52:09 +00:00
func mustCollectInvoiceStatuses ( ctx context . Context , conn * Conn , locale * Locale ) map [ string ] string {
rows := conn . MustQuery ( ctx , "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 ( ) )
defer rows . Close ( )
statuses := map [ string ] string { }
for rows . Next ( ) {
var key , name string
if err := rows . Scan ( & key , & name ) ; err != nil {
panic ( err )
}
statuses [ key ] = name
}
if rows . Err ( ) != nil {
panic ( rows . Err ( ) )
}
return statuses
}
Add filters form for invoices
Instead of using links in the invoice tags, that we will replace with a
“click-to-edit field”, with Oriol agreed to add a form with filters that
includes not only the tags but also dates, customer, status, and the
invoice number.
This means i now need dynamic SQL, and i do not think this belongs to
the database (i.e., no PL/pgSQL function for that). I have looked at
query builder libraries for Golang, and did not find anything that
suited me: either they wanted to manage not only the SQL query but also
all structs, or they managed to confuse Goland’s SQL analyzer.
For now, at least, i am using a very simple approach with arrays, that
still confuses Goland’s analyzer, but just in a very specific part,
which i find tolerable—not that their analyzer is that great to begin
with, but that’s a story for another day.
2023-03-29 14:16:31 +00:00
type invoiceFilterForm struct {
locale * Locale
company * Company
Customer * SelectField
InvoiceStatus * SelectField
InvoiceNumber * InputField
FromDate * InputField
ToDate * InputField
Tags * TagsField
}
func newInvoiceFilterForm ( ctx context . Context , conn * Conn , locale * Locale , company * Company ) * invoiceFilterForm {
return & invoiceFilterForm {
locale : locale ,
company : company ,
Customer : & SelectField {
Name : "customer" ,
Label : pgettext ( "input" , "Customer" , locale ) ,
EmptyLabel : gettext ( "All customers" , locale ) ,
Options : MustGetOptions ( ctx , conn , "select contact_id::text, business_name from contact where company_id = $1 order by business_name" , company . Id ) ,
} ,
InvoiceStatus : & SelectField {
Name : "invoice_status" ,
Label : pgettext ( "input" , "Invoice Status" , locale ) ,
EmptyLabel : gettext ( "All status" , locale ) ,
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 ( ) ) ,
} ,
InvoiceNumber : & InputField {
Name : "number" ,
Label : pgettext ( "input" , "Invoice Number" , locale ) ,
Type : "text" ,
} ,
FromDate : & InputField {
Name : "from_date" ,
Label : pgettext ( "input" , "From Date" , locale ) ,
Type : "date" ,
} ,
ToDate : & InputField {
Name : "to_date" ,
Label : pgettext ( "input" , "To Date" , locale ) ,
Type : "date" ,
} ,
Tags : & TagsField {
Name : "tags" ,
Label : pgettext ( "input" , "Tags" , locale ) ,
} ,
}
}
func ( form * invoiceFilterForm ) Parse ( r * http . Request ) error {
if err := r . ParseForm ( ) ; err != nil {
return err
}
form . Customer . FillValue ( r )
form . InvoiceStatus . FillValue ( r )
form . InvoiceNumber . FillValue ( r )
form . FromDate . FillValue ( r )
form . ToDate . FillValue ( r )
form . Tags . FillValue ( r )
return nil
}
Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.
I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.
I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance. This works, but i would need a Chrome running as a
daemon.
I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.
To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option. I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.
Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 16:26:09 +00:00
func ServeInvoice ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
2023-02-11 21:16:48 +00:00
conn := getConn ( r )
company := mustGetCompany ( r )
slug := params [ 0 ] . Value
if slug == "new" {
2023-03-13 14:00:35 +00:00
locale := getLocale ( r )
form := newInvoiceForm ( r . Context ( ) , conn , locale , company )
2023-03-08 10:26:02 +00:00
if invoiceToDuplicate := r . URL . Query ( ) . Get ( "duplicate" ) ; invoiceToDuplicate != "" {
form . MustFillFromDatabase ( r . Context ( ) , conn , invoiceToDuplicate )
2023-03-13 14:00:35 +00:00
form . InvoiceStatus . Selected = [ ] string { "created" }
2023-03-08 10:26:02 +00:00
form . Number . Val = ""
}
2023-02-11 21:16:48 +00:00
form . Date . Val = time . Now ( ) . Format ( "2006-01-02" )
w . WriteHeader ( http . StatusOK )
mustRenderNewInvoiceForm ( w , r , form )
return
}
2023-02-24 11:22:15 +00:00
Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.
I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.
I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance. This works, but i would need a Chrome running as a
daemon.
I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.
To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option. I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.
Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 16:26:09 +00:00
pdf := false
if strings . HasSuffix ( slug , ".pdf" ) {
pdf = true
slug = slug [ : len ( slug ) - len ( ".pdf" ) ]
}
2023-03-09 11:11:53 +00:00
inv := mustGetInvoice ( r . Context ( ) , conn , company , slug )
if inv == nil {
2023-02-24 11:22:15 +00:00
http . NotFound ( w , r )
return
}
Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.
I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.
I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance. This works, but i would need a Chrome running as a
daemon.
I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.
To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option. I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.
Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 16:26:09 +00:00
if pdf {
w . Header ( ) . Set ( "Content-Type" , "application/pdf" )
2023-03-09 11:11:53 +00:00
mustWriteInvoicePdf ( w , r , inv )
} else {
2023-03-28 07:57:48 +00:00
mustRenderMainTemplate ( w , r , "invoices/view.gohtml" , inv )
2023-03-09 11:11:53 +00:00
}
}
func mustWriteInvoicePdf ( w io . Writer , r * http . Request , inv * invoice ) {
cmd := exec . Command ( "weasyprint" , "--format" , "pdf" , "--stylesheet" , "web/static/invoice.css" , "-" , "-" )
var stderr bytes . Buffer
cmd . Stderr = & stderr
stdin , err := cmd . StdinPipe ( )
if err != nil {
panic ( err )
}
stdout , err := cmd . StdoutPipe ( )
if err != nil {
panic ( err )
}
defer func ( ) {
err := stdout . Close ( )
if ! errors . Is ( err , os . ErrClosed ) {
Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.
I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.
I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance. This works, but i would need a Chrome running as a
daemon.
I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.
To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option. I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.
Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 16:26:09 +00:00
panic ( err )
}
2023-03-09 11:11:53 +00:00
} ( )
if err = cmd . Start ( ) ; err != nil {
panic ( err )
}
go func ( ) {
defer mustClose ( stdin )
mustRenderAppTemplate ( stdin , r , "invoices/view.gohtml" , inv )
} ( )
if _ , err = io . Copy ( w , stdout ) ; err != nil {
panic ( err )
}
if err := cmd . Wait ( ) ; err != nil {
log . Printf ( "ERR - %v\n" , stderr . String ( ) )
panic ( err )
Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.
I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.
I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance. This works, but i would need a Chrome running as a
daemon.
I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.
To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option. I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.
Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 16:26:09 +00:00
}
2023-02-24 11:22:15 +00:00
}
2023-02-27 11:55:18 +00:00
func mustClose ( closer io . Closer ) {
if err := closer . Close ( ) ; err != nil {
panic ( err )
}
}
2023-02-24 11:22:15 +00:00
type invoice struct {
2023-03-05 17:50:57 +00:00
Number string
Slug string
Date time . Time
Invoicer taxDetails
Invoicee taxDetails
Notes string
PaymentInstructions string
Products [ ] * invoiceProduct
Subtotal string
Taxes [ ] [ ] string
TaxClasses [ ] string
HasDiscounts bool
Total string
LegalDisclaimer string
2023-02-24 11:22:15 +00:00
}
type taxDetails struct {
Name string
VATIN string
Address string
City string
PostalCode string
Province string
Email string
Phone string
}
type invoiceProduct struct {
Name string
Description string
Price string
2023-03-01 13:08:12 +00:00
Discount int
2023-02-24 11:22:15 +00:00
Quantity int
2023-03-01 13:08:12 +00:00
Taxes map [ string ] int
Subtotal string
2023-02-24 11:22:15 +00:00
Total string
}
func mustGetInvoice ( ctx context . Context , conn * Conn , company * Company , slug string ) * invoice {
Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.
I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.
I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance. This works, but i would need a Chrome running as a
daemon.
I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards. Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.
To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option. I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.
Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 16:26:09 +00:00
inv := & invoice {
Slug : slug ,
}
2023-02-24 11:22:15 +00:00
var invoiceId int
var decimalDigits int
2023-03-05 17:50:57 +00:00
if notFoundErrorOrPanic ( conn . QueryRow ( ctx , "select invoice_id, decimal_digits, invoice_number, invoice_date, notes, instructions, business_name, vatin, phone, email, address, city, province, postal_code, to_price(subtotal, decimal_digits), to_price(total, decimal_digits) from invoice join payment_method using (payment_method_id) join contact using (contact_id) join invoice_amount using (invoice_id) join currency using (currency_code) where invoice.slug = $1" , slug ) . Scan ( & invoiceId , & decimalDigits , & inv . Number , & inv . Date , & inv . Notes , & inv . PaymentInstructions , & inv . Invoicee . Name , & inv . Invoicee . VATIN , & inv . Invoicee . Phone , & inv . Invoicee . Email , & inv . Invoicee . Address , & inv . Invoicee . City , & inv . Invoicee . Province , & inv . Invoicee . PostalCode , & inv . Subtotal , & inv . Total ) ) {
2023-02-24 11:22:15 +00:00
return nil
}
2023-03-02 09:24:44 +00:00
if err := conn . QueryRow ( ctx , "select business_name, vatin, phone, email, address, city, province, postal_code, legal_disclaimer from company where company_id = $1" , company . Id ) . Scan ( & inv . Invoicer . Name , & inv . Invoicer . VATIN , & inv . Invoicer . Phone , & inv . Invoicer . Email , & inv . Invoicer . Address , & inv . Invoicer . City , & inv . Invoicer . Province , & inv . Invoicer . PostalCode , & inv . LegalDisclaimer ) ; err != nil {
2023-02-24 11:22:15 +00:00
panic ( err )
}
if err := conn . QueryRow ( ctx , "select array_agg(array[name, to_price(amount, $2)]) from invoice_tax_amount join tax using (tax_id) where invoice_id = $1" , invoiceId , decimalDigits ) . Scan ( & inv . Taxes ) ; err != nil {
panic ( err )
}
2023-03-01 13:08:12 +00:00
rows := conn . MustQuery ( ctx , "select invoice_product.name, description, to_price(price, $2), (discount_rate * 100)::integer, quantity, to_price(subtotal, $2), to_price(total, $2), array_agg(array[tax_class.name, (tax_rate * 100)::integer::text]) from invoice_product join invoice_product_amount using (invoice_product_id) left join invoice_product_tax using (invoice_product_id) left join tax using (tax_id) left join tax_class using (tax_class_id) where invoice_id = $1 group by invoice_product.name, description, discount_rate, price, quantity, subtotal, total" , invoiceId , decimalDigits )
2023-02-24 11:22:15 +00:00
defer rows . Close ( )
2023-03-01 13:08:12 +00:00
taxClasses := map [ string ] bool { }
2023-02-24 11:22:15 +00:00
for rows . Next ( ) {
2023-03-01 13:08:12 +00:00
product := & invoiceProduct {
Taxes : make ( map [ string ] int ) ,
}
var taxes [ ] [ ] string
if err := rows . Scan ( & product . Name , & product . Description , & product . Price , & product . Discount , & product . Quantity , & product . Subtotal , & product . Total , & taxes ) ; err != nil {
2023-02-24 11:22:15 +00:00
panic ( err )
}
2023-03-01 13:08:12 +00:00
for _ , tax := range taxes {
taxClass := tax [ 0 ]
taxClasses [ taxClass ] = true
product . Taxes [ taxClass ] , _ = strconv . Atoi ( tax [ 1 ] )
}
if product . Discount > 0 {
inv . HasDiscounts = true
}
2023-02-24 11:22:15 +00:00
inv . Products = append ( inv . Products , product )
}
2023-03-01 13:08:12 +00:00
for taxClass := range taxClasses {
inv . TaxClasses = append ( inv . TaxClasses , taxClass )
}
sort . Strings ( inv . TaxClasses )
2023-02-24 11:22:15 +00:00
if rows . Err ( ) != nil {
panic ( rows . Err ( ) )
}
return inv
2023-02-11 21:16:48 +00:00
}
2023-02-23 14:31:57 +00:00
type newInvoicePage struct {
Form * invoiceForm
Subtotal string
Taxes [ ] [ ] string
Total string
}
func newNewInvoicePage ( form * invoiceForm , r * http . Request ) * newInvoicePage {
page := & newInvoicePage {
Form : form ,
}
conn := getConn ( r )
company := mustGetCompany ( r )
err := conn . QueryRow ( r . Context ( ) , "select subtotal, taxes, total from compute_new_invoice_amount($1, $2)" , company . Id , NewInvoiceProductArray ( form . Products ) ) . Scan ( & page . Subtotal , & page . Taxes , & page . Total )
if err != nil {
panic ( err )
}
return page
}
2023-02-11 21:16:48 +00:00
func mustRenderNewInvoiceForm ( w http . ResponseWriter , r * http . Request , form * invoiceForm ) {
2023-02-14 11:55:19 +00:00
locale := getLocale ( r )
form . Customer . EmptyLabel = gettext ( "Select a customer to bill." , locale )
2023-02-23 14:31:57 +00:00
page := newNewInvoicePage ( form , r )
mustRenderAppTemplate ( w , r , "invoices/new.gohtml" , page )
2023-02-11 21:16:48 +00:00
}
2023-03-13 14:00:35 +00:00
func mustRenderNewInvoiceProductsForm ( w http . ResponseWriter , r * http . Request , action string , form * invoiceForm ) {
2023-02-12 20:06:48 +00:00
conn := getConn ( r )
company := mustGetCompany ( r )
page := newInvoiceProductsPage {
2023-03-13 14:00:35 +00:00
Action : companyURI ( company , action ) ,
2023-02-12 20:06:48 +00:00
Form : form ,
Products : mustGetProductChoices ( r . Context ( ) , conn , company ) ,
}
mustRenderAppTemplate ( w , r , "invoices/products.gohtml" , page )
}
func mustGetProductChoices ( ctx context . Context , conn * Conn , company * Company ) [ ] * productChoice {
rows := conn . MustQuery ( ctx , "select product.product_id, product.name, to_price(price, decimal_digits) from product join company using (company_id) join currency using (currency_code) where company_id = $1 order by name" , company . Id )
defer rows . Close ( )
var choices [ ] * productChoice
for rows . Next ( ) {
entry := & productChoice { }
if err := rows . Scan ( & entry . Id , & entry . Name , & entry . Price ) ; err != nil {
panic ( err )
}
choices = append ( choices , entry )
}
if rows . Err ( ) != nil {
panic ( rows . Err ( ) )
}
return choices
}
type newInvoiceProductsPage struct {
2023-03-13 14:00:35 +00:00
Action string
2023-02-12 20:06:48 +00:00
Form * invoiceForm
Products [ ] * productChoice
}
type productChoice struct {
Id int
Name string
Price string
}
2023-02-11 21:16:48 +00:00
func HandleAddInvoice ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
2023-02-12 20:06:48 +00:00
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
}
2023-02-27 12:13:28 +00:00
if ! form . Validate ( ) {
w . WriteHeader ( http . StatusUnprocessableEntity )
2023-02-12 20:06:48 +00:00
mustRenderNewInvoiceForm ( w , r , form )
2023-02-27 12:13:28 +00:00
return
2023-02-12 20:06:48 +00:00
}
Add tags for contacts too
With Oriol we agreed that contacts should have tags, too, and that the
“tag pool”, as it were, should be shared with the one for invoices (and
all future tags we might add).
I added the contact_tag relation and tag_contact function, just like
with invoices, and then realized that the SQL queries that Go had to
execute were becoming “complex” enough: i had to get not only the slug,
but the contact id to call tag_contact, and all inside a transaction.
Therefore, i opted to create the add_contact and edit_contact functions,
that mirror those for invoice and products, so now each “major” section
has these functions. They also simplified a bit the handling of the
VATIN and phone numbers, because it is now encapsuled inside the
PL/pgSQL function and Go does not know how to assemble the parts.
2023-03-26 00:32:53 +00:00
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 . Tags , NewInvoiceProductArray ( form . Products ) )
2023-02-27 12:13:28 +00:00
http . Redirect ( w , r , companyURI ( company , "/invoices/" + slug ) , http . StatusSeeOther )
2023-02-11 21:16:48 +00:00
}
2023-03-13 14:00:35 +00:00
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 )
2023-02-27 12:13:28 +00:00
default :
2023-03-13 14:00:35 +00:00
http . Error ( w , "Method Not Allowed" , http . StatusMethodNotAllowed )
2023-02-12 20:06:48 +00:00
}
2023-02-11 21:16:48 +00:00
}
2023-03-09 11:11:53 +00:00
func HandleBatchInvoiceAction ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
if err := r . ParseForm ( ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
if err := verifyCsrfTokenValid ( r ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusForbidden )
return
}
slugs := r . Form [ "invoice" ]
if len ( slugs ) == 0 {
http . Redirect ( w , r , companyURI ( mustGetCompany ( r ) , "/invoices" ) , http . StatusSeeOther )
return
}
locale := getLocale ( r )
switch r . Form . Get ( "action" ) {
case "download" :
invoices := mustWriteInvoicesPdf ( r , slugs )
w . Header ( ) . Set ( "Content-Type" , "application/zip" )
w . Header ( ) . Set ( "Content-Disposition" , fmt . Sprintf ( "attachment; filename=%s" , gettext ( "invoices.zip" , locale ) ) )
w . WriteHeader ( http . StatusOK )
if _ , err := w . Write ( invoices ) ; err != nil {
panic ( err )
}
default :
http . Error ( w , gettext ( "Invalid action" , locale ) , http . StatusBadRequest )
}
}
func mustWriteInvoicesPdf ( r * http . Request , slugs [ ] string ) [ ] byte {
conn := getConn ( r )
company := mustGetCompany ( r )
buf := new ( bytes . Buffer )
w := zip . NewWriter ( buf )
for _ , slug := range slugs {
inv := mustGetInvoice ( r . Context ( ) , conn , company , slug )
if inv == nil {
continue
}
f , err := w . Create ( inv . Number + ".pdf" )
if err != nil {
panic ( err )
}
mustWriteInvoicePdf ( f , r , inv )
}
mustClose ( w )
return buf . Bytes ( )
}
2023-02-11 21:16:48 +00:00
type invoiceForm struct {
2023-03-04 21:15:52 +00:00
locale * Locale
company * Company
2023-03-13 14:00:35 +00:00
InvoiceStatus * SelectField
2023-03-04 21:15:52 +00:00
Customer * SelectField
Number * InputField
Date * InputField
Notes * InputField
PaymentMethod * SelectField
Add tags for contacts too
With Oriol we agreed that contacts should have tags, too, and that the
“tag pool”, as it were, should be shared with the one for invoices (and
all future tags we might add).
I added the contact_tag relation and tag_contact function, just like
with invoices, and then realized that the SQL queries that Go had to
execute were becoming “complex” enough: i had to get not only the slug,
but the contact id to call tag_contact, and all inside a transaction.
Therefore, i opted to create the add_contact and edit_contact functions,
that mirror those for invoice and products, so now each “major” section
has these functions. They also simplified a bit the handling of the
VATIN and phone numbers, because it is now encapsuled inside the
PL/pgSQL function and Go does not know how to assemble the parts.
2023-03-26 00:32:53 +00:00
Tags * TagsField
2023-03-04 21:15:52 +00:00
Products [ ] * invoiceProductForm
2023-02-11 21:16:48 +00:00
}
func newInvoiceForm ( ctx context . Context , conn * Conn , locale * Locale , company * Company ) * invoiceForm {
return & invoiceForm {
locale : locale ,
company : company ,
2023-03-13 14:00:35 +00:00
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 ( ) ) ,
} ,
2023-02-11 21:16:48 +00:00
Customer : & SelectField {
Name : "customer" ,
Label : pgettext ( "input" , "Customer" , locale ) ,
Required : true ,
Options : MustGetOptions ( ctx , conn , "select contact_id::text, business_name from contact where company_id = $1 order by business_name" , company . Id ) ,
} ,
Number : & InputField {
2023-03-10 13:02:55 +00:00
Name : "number" ,
Label : pgettext ( "input" , "Number" , locale ) ,
Type : "text" ,
2023-02-11 21:16:48 +00:00
} ,
Date : & InputField {
Name : "date" ,
Label : pgettext ( "input" , "Invoice Date" , locale ) ,
Type : "date" ,
Required : true ,
} ,
Notes : & InputField {
2023-03-04 21:15:52 +00:00
Name : "notes" ,
2023-02-11 21:16:48 +00:00
Label : pgettext ( "input" , "Notes" , locale ) ,
Type : "textarea" ,
} ,
Add tags for contacts too
With Oriol we agreed that contacts should have tags, too, and that the
“tag pool”, as it were, should be shared with the one for invoices (and
all future tags we might add).
I added the contact_tag relation and tag_contact function, just like
with invoices, and then realized that the SQL queries that Go had to
execute were becoming “complex” enough: i had to get not only the slug,
but the contact id to call tag_contact, and all inside a transaction.
Therefore, i opted to create the add_contact and edit_contact functions,
that mirror those for invoice and products, so now each “major” section
has these functions. They also simplified a bit the handling of the
VATIN and phone numbers, because it is now encapsuled inside the
PL/pgSQL function and Go does not know how to assemble the parts.
2023-03-26 00:32:53 +00:00
Tags : & TagsField {
2023-03-10 13:02:55 +00:00
Name : "tags" ,
Label : pgettext ( "input" , "Tags" , locale ) ,
} ,
2023-03-04 21:15:52 +00:00
PaymentMethod : & SelectField {
Name : "payment_method" ,
Required : true ,
Label : pgettext ( "input" , "Payment Method" , locale ) ,
Selected : [ ] string { conn . MustGetText ( ctx , "" , "select default_payment_method_id::text from company where company_id = $1" , company . Id ) } ,
Options : MustGetOptions ( ctx , conn , "select payment_method_id::text, name from payment_method where company_id = $1" , company . Id ) ,
2023-02-11 21:16:48 +00:00
} ,
}
}
2023-02-12 20:06:48 +00:00
func ( form * invoiceForm ) Parse ( r * http . Request ) error {
if err := r . ParseForm ( ) ; err != nil {
return err
}
2023-03-13 14:00:35 +00:00
form . InvoiceStatus . FillValue ( r )
2023-02-12 20:06:48 +00:00
form . Customer . FillValue ( r )
form . Number . FillValue ( r )
form . Date . FillValue ( r )
form . Notes . FillValue ( r )
2023-03-10 13:02:55 +00:00
form . Tags . FillValue ( r )
2023-03-08 10:19:59 +00:00
form . PaymentMethod . FillValue ( r )
2023-02-13 09:32:26 +00:00
if _ , ok := r . Form [ "product.id.0" ] ; ok {
2023-03-04 21:15:52 +00:00
taxOptions := mustGetTaxOptions ( r . Context ( ) , getConn ( r ) , form . company )
2023-02-13 09:32:26 +00:00
for index := 0 ; true ; index ++ {
if _ , ok := r . Form [ "product.id." + strconv . Itoa ( index ) ] ; ! ok {
break
}
2023-03-04 21:15:52 +00:00
productForm := newInvoiceProductForm ( index , form . company , form . locale , taxOptions )
2023-02-12 20:06:48 +00:00
if err := productForm . Parse ( r ) ; err != nil {
return err
}
form . Products = append ( form . Products , productForm )
}
}
return nil
}
func ( form * invoiceForm ) Validate ( ) bool {
validator := newFormValidator ( )
2023-03-13 14:00:35 +00:00
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 ) )
2023-02-12 20:06:48 +00:00
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 ) )
}
2023-03-04 21:15:52 +00:00
validator . CheckValidSelectOption ( form . PaymentMethod , gettext ( "Selected payment method is not valid." , form . locale ) )
2023-02-12 20:06:48 +00:00
allOK := validator . AllOK ( )
for _ , product := range form . Products {
allOK = product . Validate ( ) && allOK
}
return allOK
}
func ( form * invoiceForm ) Update ( ) {
products := form . Products
form . Products = nil
2023-02-21 12:57:40 +00:00
for n , product := range products {
2023-02-12 20:06:48 +00:00
if product . Quantity . Val != "0" {
2023-02-21 12:57:40 +00:00
if n != len ( form . Products ) {
product . Reindex ( len ( form . Products ) )
}
2023-02-12 20:06:48 +00:00
form . Products = append ( form . Products , product )
}
}
}
2023-02-27 12:13:28 +00:00
func ( form * invoiceForm ) AddProducts ( ctx context . Context , conn * Conn , productsId [ ] string ) {
2023-03-13 14:00:35 +00:00
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 )
2023-03-08 10:26:02 +00:00
}
func ( form * invoiceForm ) mustAddProductsFromQuery ( ctx context . Context , conn * Conn , sql string , args ... interface { } ) {
2023-02-27 12:13:28 +00:00
index := len ( form . Products )
2023-03-05 17:43:22 +00:00
taxOptions := mustGetTaxOptions ( ctx , conn , form . company )
2023-03-08 10:26:02 +00:00
rows := conn . MustQuery ( ctx , sql , args ... )
2023-02-27 12:13:28 +00:00
defer rows . Close ( )
for rows . Next ( ) {
2023-03-04 21:15:52 +00:00
product := newInvoiceProductForm ( index , form . company , form . locale , taxOptions )
2023-03-13 14:00:35 +00:00
if err := rows . Scan ( product . InvoiceProductId , product . ProductId , product . Name , product . Description , product . Price , product . Quantity , product . Discount , product . Tax ) ; err != nil {
2023-02-27 12:13:28 +00:00
panic ( err )
}
form . Products = append ( form . Products , product )
index ++
}
if rows . Err ( ) != nil {
panic ( rows . Err ( ) )
}
}
2023-03-13 14:00:35 +00:00
func ( form * invoiceForm ) MustFillFromDatabase ( ctx context . Context , conn * Conn , slug string ) bool {
2023-03-08 10:26:02 +00:00
var invoiceId int
2023-03-13 14:00:35 +00:00
selectedInvoiceStatus := form . InvoiceStatus . Selected
form . InvoiceStatus . Clear ( )
2023-03-08 10:26:02 +00:00
selectedPaymentMethod := form . PaymentMethod . Selected
form . PaymentMethod . Clear ( )
2023-03-10 13:02:55 +00:00
if notFoundErrorOrPanic ( conn . QueryRow ( ctx , `
select invoice_id
2023-03-13 14:00:35 +00:00
, invoice_status
2023-03-10 13:02:55 +00:00
, contact_id
, invoice_number
, invoice_date
, notes
, payment_method_id
2023-03-19 22:10:01 +00:00
, string_agg ( tag . name , ',' )
2023-03-10 13:02:55 +00:00
from invoice
left join invoice_tag using ( invoice_id )
left join tag using ( tag_id ) where slug = $ 1
group by invoice_id
, contact_id
, invoice_number
, invoice_date
, notes
, payment_method_id
2023-03-13 14:00:35 +00:00
` , slug ) . Scan ( & invoiceId , form . InvoiceStatus , form . Customer , form . Number , form . Date , form . Notes , form . PaymentMethod , form . Tags ) ) {
2023-03-08 10:26:02 +00:00
form . PaymentMethod . Selected = selectedPaymentMethod
2023-03-13 14:00:35 +00:00
form . InvoiceStatus . Selected = selectedInvoiceStatus
return false
2023-03-08 10:26:02 +00:00
}
form . Products = [ ] * invoiceProductForm { }
2023-03-13 14:00:35 +00:00
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
2023-03-08 10:26:02 +00:00
}
2023-02-12 20:06:48 +00:00
func mustGetTaxOptions ( ctx context . Context , conn * Conn , company * Company ) [ ] * SelectOption {
2023-03-01 10:40:23 +00:00
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 )
2023-02-12 20:06:48 +00:00
}
type invoiceProductForm struct {
2023-03-13 14:00:35 +00:00
locale * Locale
company * Company
InvoiceProductId * InputField
ProductId * InputField
Name * InputField
Description * InputField
Price * InputField
Quantity * InputField
Discount * InputField
Tax * SelectField
2023-02-12 20:06:48 +00:00
}
func newInvoiceProductForm ( index int , company * Company , locale * Locale , taxOptions [ ] * SelectOption ) * invoiceProductForm {
2023-02-21 12:57:40 +00:00
form := & invoiceProductForm {
2023-02-12 20:06:48 +00:00
locale : locale ,
company : company ,
2023-03-13 14:00:35 +00:00
InvoiceProductId : & InputField {
Label : pgettext ( "input" , "Id" , locale ) ,
Type : "hidden" ,
Required : true ,
} ,
2023-02-11 21:16:48 +00:00
ProductId : & InputField {
Label : pgettext ( "input" , "Id" , locale ) ,
Type : "hidden" ,
Required : true ,
} ,
Name : & InputField {
Label : pgettext ( "input" , "Name" , locale ) ,
Type : "text" ,
Required : true ,
} ,
Description : & InputField {
Label : pgettext ( "input" , "Description" , locale ) ,
Type : "textarea" ,
} ,
Price : & InputField {
Label : pgettext ( "input" , "Price" , locale ) ,
Type : "number" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
` min="0" ` ,
template . HTMLAttr ( fmt . Sprintf ( ` step="%v" ` , company . MinCents ( ) ) ) ,
} ,
} ,
Quantity : & InputField {
Label : pgettext ( "input" , "Quantity" , locale ) ,
Type : "number" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
` min="0" ` ,
} ,
} ,
Discount : & InputField {
Label : pgettext ( "input" , "Discount (%)" , locale ) ,
Type : "number" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
` min="0" ` ,
` max="100" ` ,
} ,
} ,
Tax : & SelectField {
Label : pgettext ( "input" , "Taxes" , locale ) ,
Multiple : true ,
2023-02-12 20:06:48 +00:00
Options : taxOptions ,
2023-02-11 21:16:48 +00:00
} ,
}
2023-02-21 12:57:40 +00:00
form . Reindex ( index )
return form
}
func ( form * invoiceProductForm ) Reindex ( index int ) {
suffix := "." + strconv . Itoa ( index )
2023-03-13 14:00:35 +00:00
form . InvoiceProductId . Name = "product.invoice_product_id" + suffix
2023-02-21 12:57:40 +00:00
form . ProductId . Name = "product.id" + suffix
form . Name . Name = "product.name" + suffix
form . Description . Name = "product.description" + suffix
form . Price . Name = "product.price" + suffix
form . Quantity . Name = "product.quantity" + suffix
form . Discount . Name = "product.discount" + suffix
form . Tax . Name = "product.tax" + suffix
2023-02-11 21:16:48 +00:00
}
2023-02-12 20:06:48 +00:00
func ( form * invoiceProductForm ) Parse ( r * http . Request ) error {
if err := r . ParseForm ( ) ; err != nil {
return err
}
2023-03-13 14:00:35 +00:00
form . InvoiceProductId . FillValue ( r )
2023-02-12 20:06:48 +00:00
form . ProductId . FillValue ( r )
form . Name . FillValue ( r )
form . Description . FillValue ( r )
form . Price . FillValue ( r )
form . Quantity . FillValue ( r )
form . Discount . FillValue ( r )
form . Tax . FillValue ( r )
return nil
}
func ( form * invoiceProductForm ) Validate ( ) bool {
validator := newFormValidator ( )
2023-03-13 14:00:35 +00:00
validator . CheckRequiredInput ( form . ProductId , gettext ( "Product ID can not be empty." , form . locale ) )
2023-02-12 20:06:48 +00:00
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 ) )
}
if validator . CheckRequiredInput ( form . Quantity , gettext ( "Quantity can not be empty." , form . locale ) ) {
2023-02-13 09:32:26 +00:00
validator . CheckValidInteger ( form . Quantity , 1 , math . MaxInt32 , gettext ( "Quantity must be a number greater than zero." , form . locale ) )
2023-02-12 20:06:48 +00:00
}
if validator . CheckRequiredInput ( form . Discount , gettext ( "Discount can not be empty." , form . locale ) ) {
validator . CheckValidInteger ( form . Discount , 0 , 100 , gettext ( "Discount must be a percentage between 0 and 100." , form . locale ) )
}
validator . CheckValidSelectOption ( form . Tax , gettext ( "Selected tax is not valid." , form . locale ) )
2023-03-01 10:55:26 +00:00
validator . CheckAtMostOneOfEachGroup ( form . Tax , gettext ( "You can only select a tax of each class." , form . locale ) )
2023-02-12 20:06:48 +00:00
return validator . AllOK ( )
}
2023-03-07 10:52:09 +00:00
func HandleUpdateInvoice ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
2023-03-13 14:00:35 +00:00
locale := getLocale ( r )
2023-03-07 10:52:09 +00:00
conn := getConn ( r )
2023-03-13 14:00:35 +00:00
company := mustGetCompany ( r )
form := newInvoiceForm ( r . Context ( ) , conn , locale , company )
if err := form . Parse ( r ) ; err != nil {
2023-03-07 10:52:09 +00:00
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
if err := verifyCsrfTokenValid ( r ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusForbidden )
return
}
2023-03-13 14:00:35 +00:00
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
}
Add tags for contacts too
With Oriol we agreed that contacts should have tags, too, and that the
“tag pool”, as it were, should be shared with the one for invoices (and
all future tags we might add).
I added the contact_tag relation and tag_contact function, just like
with invoices, and then realized that the SQL queries that Go had to
execute were becoming “complex” enough: i had to get not only the slug,
but the contact id to call tag_contact, and all inside a transaction.
Therefore, i opted to create the add_contact and edit_contact functions,
that mirror those for invoice and products, so now each “major” section
has these functions. They also simplified a bit the handling of the
VATIN and phone numbers, because it is now encapsuled inside the
PL/pgSQL function and Go does not know how to assemble the parts.
2023-03-26 00:32:53 +00:00
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 . Tags , EditedInvoiceProductArray ( form . Products ) )
2023-03-13 14:00:35 +00:00
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 ) {
2023-03-07 10:52:09 +00:00
http . NotFound ( w , r )
2023-03-13 14:00:35 +00:00
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 )
2023-03-07 10:52:09 +00:00
}
}