Compare commits
3 Commits
1164210d84
...
1c23cbb7d7
Author | SHA1 | Date |
---|---|---|
jordi fita mas | 1c23cbb7d7 | |
jordi fita mas | 7644a3a17a | |
jordi fita mas | 8c31fc63c5 |
|
@ -10,8 +10,9 @@ Build-Depends:
|
||||||
golang-github-jackc-pgx-v4-dev,
|
golang-github-jackc-pgx-v4-dev,
|
||||||
golang-github-julienschmidt-httprouter-dev,
|
golang-github-julienschmidt-httprouter-dev,
|
||||||
golang-github-leonelquinteros-gotext-dev,
|
golang-github-leonelquinteros-gotext-dev,
|
||||||
golang-golang-x-text-dev,
|
golang-github-rainycape-unidecode-dev,
|
||||||
golang-github-tealeg-xlsx-dev,
|
golang-github-tealeg-xlsx-dev,
|
||||||
|
golang-golang-x-text-dev,
|
||||||
postgresql-all (>= 217~),
|
postgresql-all (>= 217~),
|
||||||
sqitch,
|
sqitch,
|
||||||
pgtap,
|
pgtap,
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -7,6 +7,7 @@ require (
|
||||||
github.com/jackc/pgx/v4 v4.15.0
|
github.com/jackc/pgx/v4 v4.15.0
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
github.com/leonelquinteros/gotext v1.5.0
|
github.com/leonelquinteros/gotext v1.5.0
|
||||||
|
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8
|
||||||
github.com/tealeg/xlsx v0.0.0-20181024002044-dbf71b6a931e
|
github.com/tealeg/xlsx v0.0.0-20181024002044-dbf71b6a931e
|
||||||
golang.org/x/text v0.7.0
|
golang.org/x/text v0.7.0
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -92,6 +92,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8 h1:iZTHFqK/oFrjyFDkiw5U/RjQxkMlkpq6tHQIO407i+s=
|
||||||
|
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
|
|
|
@ -416,13 +416,63 @@ func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug stri
|
||||||
&inv.Total)) {
|
&inv.Total)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
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 {
|
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 {
|
||||||
panic(err)
|
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 {
|
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)
|
panic(err)
|
||||||
}
|
}
|
||||||
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]) filter (where tax_rate is not null) 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)
|
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]) filter (where tax_rate is not null)
|
||||||
|
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_id
|
||||||
|
, invoice_product.name
|
||||||
|
, description
|
||||||
|
, discount_rate
|
||||||
|
, price
|
||||||
|
, quantity
|
||||||
|
, subtotal
|
||||||
|
, total
|
||||||
|
order by invoice_product_id
|
||||||
|
`, invoiceId, decimalDigits)
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
taxClasses := map[string]bool{}
|
taxClasses := map[string]bool{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
@ -430,7 +480,15 @@ func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug stri
|
||||||
Taxes: make(map[string]int),
|
Taxes: make(map[string]int),
|
||||||
}
|
}
|
||||||
var taxes [][]string
|
var taxes [][]string
|
||||||
if err := rows.Scan(&product.Name, &product.Description, &product.Price, &product.Discount, &product.Quantity, &product.Subtotal, &product.Total, &taxes); err != nil {
|
if err := rows.Scan(
|
||||||
|
&product.Name,
|
||||||
|
&product.Description,
|
||||||
|
&product.Price,
|
||||||
|
&product.Discount,
|
||||||
|
&product.Quantity,
|
||||||
|
&product.Subtotal,
|
||||||
|
&product.Total,
|
||||||
|
&taxes); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
for _, tax := range taxes {
|
for _, tax := range taxes {
|
||||||
|
@ -600,7 +658,7 @@ func mustWriteInvoicesPdf(r *http.Request, slugs []string) []byte {
|
||||||
if inv == nil {
|
if inv == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
f, err := w.Create(inv.Number + ".pdf")
|
f, err := w.Create(fmt.Sprintf("%s-%s.pdf", inv.Number, slugify(inv.Invoicee.Name)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
63
pkg/quote.go
63
pkg/quote.go
|
@ -410,13 +410,60 @@ func mustGetQuote(ctx context.Context, conn *Conn, company *Company, slug string
|
||||||
&quo.Total)) {
|
&quo.Total)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
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(&quo.Quoter.Name, &quo.Quoter.VATIN, &quo.Quoter.Phone, &quo.Quoter.Email, &quo.Quoter.Address, &quo.Quoter.City, &quo.Quoter.Province, &quo.Quoter.PostalCode, &quo.LegalDisclaimer); err != nil {
|
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(
|
||||||
|
&quo.Quoter.Name,
|
||||||
|
&quo.Quoter.VATIN,
|
||||||
|
&quo.Quoter.Phone,
|
||||||
|
&quo.Quoter.Email,
|
||||||
|
&quo.Quoter.Address,
|
||||||
|
&quo.Quoter.City,
|
||||||
|
&quo.Quoter.Province,
|
||||||
|
&quo.Quoter.PostalCode,
|
||||||
|
&quo.LegalDisclaimer); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := conn.QueryRow(ctx, "select array_agg(array[name, to_price(amount, $2)]) from quote_tax_amount join tax using (tax_id) where quote_id = $1", quoteId, decimalDigits).Scan(&quo.Taxes); err != nil {
|
if err := conn.QueryRow(ctx, `
|
||||||
|
select array_agg(array[name, to_price(amount, $2)]) from quote_tax_amount
|
||||||
|
join tax using (tax_id)
|
||||||
|
where quote_id = $1
|
||||||
|
`, quoteId, decimalDigits).Scan(&quo.Taxes); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
rows := conn.MustQuery(ctx, "select quote_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]) filter (where tax_rate is not null) from quote_product join quote_product_amount using (quote_product_id) left join quote_product_tax using (quote_product_id) left join tax using (tax_id) left join tax_class using (tax_class_id) where quote_id = $1 group by quote_product.name, description, discount_rate, price, quantity, subtotal, total", quoteId, decimalDigits)
|
rows := conn.MustQuery(ctx, `
|
||||||
|
select quote_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]) filter (where tax_rate is not null)
|
||||||
|
from quote_product
|
||||||
|
join quote_product_amount using (quote_product_id)
|
||||||
|
left join quote_product_tax using (quote_product_id)
|
||||||
|
left join tax using (tax_id)
|
||||||
|
left join tax_class using (tax_class_id)
|
||||||
|
where quote_id = $1
|
||||||
|
group by quote_product_id
|
||||||
|
, quote_product.name
|
||||||
|
, description
|
||||||
|
, discount_rate
|
||||||
|
, price
|
||||||
|
, quantity
|
||||||
|
, subtotal
|
||||||
|
, total
|
||||||
|
order by quote_product_id
|
||||||
|
`, quoteId, decimalDigits)
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
taxClasses := map[string]bool{}
|
taxClasses := map[string]bool{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
@ -424,7 +471,15 @@ func mustGetQuote(ctx context.Context, conn *Conn, company *Company, slug string
|
||||||
Taxes: make(map[string]int),
|
Taxes: make(map[string]int),
|
||||||
}
|
}
|
||||||
var taxes [][]string
|
var taxes [][]string
|
||||||
if err := rows.Scan(&product.Name, &product.Description, &product.Price, &product.Discount, &product.Quantity, &product.Subtotal, &product.Total, &taxes); err != nil {
|
if err := rows.Scan(
|
||||||
|
&product.Name,
|
||||||
|
&product.Description,
|
||||||
|
&product.Price,
|
||||||
|
&product.Discount,
|
||||||
|
&product.Quantity,
|
||||||
|
&product.Subtotal,
|
||||||
|
&product.Total,
|
||||||
|
&taxes); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
for _, tax := range taxes {
|
for _, tax := range taxes {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rainycape/unidecode"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
nonValidChars = regexp.MustCompile("[^a-z0-9-_]")
|
||||||
|
multipleDashes = regexp.MustCompile("-+")
|
||||||
|
)
|
||||||
|
|
||||||
|
func slugify(s string) (slug string) {
|
||||||
|
slug = strings.TrimSpace(s)
|
||||||
|
slug = unidecode.Unidecode(slug)
|
||||||
|
slug = strings.ToLower(slug)
|
||||||
|
slug = nonValidChars.ReplaceAllString(slug, "-")
|
||||||
|
slug = multipleDashes.ReplaceAllString(slug, "-")
|
||||||
|
slug = strings.Trim(slug, "-_")
|
||||||
|
return slug
|
||||||
|
}
|
|
@ -86,6 +86,9 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
|
||||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||||
return humanizeBytes(bytes, 1024, sizes)
|
return humanizeBytes(bytes, 1024, sizes)
|
||||||
},
|
},
|
||||||
|
"slugify": func(s string) string {
|
||||||
|
return slugify(s)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if _, err := t.ParseFiles(templateFile(filename), templateFile(layout), templateFile("form.gohtml")); err != nil {
|
if _, err := t.ParseFiles(templateFile(filename), templateFile(layout), templateFile("form.gohtml")); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
{{ define "breadcrumbs" -}}
|
{{ define "breadcrumbs" -}}
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InvoicesIndexPage*/ -}}
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InvoicesIndexPage*/ -}}
|
||||||
<nav data-hx-target="main" data-hx-boost="true">
|
<nav data-hx-target="main">
|
||||||
<p>
|
<p data-hx-boost="true">
|
||||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||||
<a>{{( pgettext "Invoices" "title" )}}</a>
|
<a>{{( pgettext "Invoices" "title" )}}</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
name="action" value="download"
|
name="action" value="download"
|
||||||
>{{( pgettext "Download invoices" "action" )}}</button>
|
>{{( pgettext "Download invoices" "action" )}}</button>
|
||||||
<a class="primary button"
|
<a class="primary button" data-hx-boost="true"
|
||||||
href="{{ companyURI "/invoices/new" }}">{{( pgettext "New invoice" "action" )}}</a>
|
href="{{ companyURI "/invoices/new" }}">{{( pgettext "New invoice" "action" )}}</a>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
<td class="numeric">{{ .Total|formatPrice }}</td>
|
<td class="numeric">{{ .Total|formatPrice }}</td>
|
||||||
{{- $title = .Number | printf (pgettext "Download invoice %s" "action") -}}
|
{{- $title = .Number | printf (pgettext "Download invoice %s" "action") -}}
|
||||||
<td class="invoice-download"><a href="{{ companyURI "/invoices/"}}{{ .Slug }}.pdf"
|
<td class="invoice-download"><a href="{{ companyURI "/invoices/"}}{{ .Slug }}.pdf"
|
||||||
download="{{ .Number}}.pdf"
|
download="{{ .Number}}-{{ .CustomerName | slugify}}.pdf"
|
||||||
title="{{( pgettext "Download invoice" "action" )}}"
|
title="{{( pgettext "Download invoice" "action" )}}"
|
||||||
aria-label="{{ $title }}"><i
|
aria-label="{{ $title }}"><i
|
||||||
class="ri-download-line"></i></a></td>
|
class="ri-download-line"></i></a></td>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
href="{{ companyURI "/invoices/"}}{{ .Slug }}/edit">{{( pgettext "Edit" "action" )}}</a>
|
href="{{ companyURI "/invoices/"}}{{ .Slug }}/edit">{{( pgettext "Edit" "action" )}}</a>
|
||||||
<a class="primary button"
|
<a class="primary button"
|
||||||
href="{{ companyURI "/invoices/" }}{{ .Slug }}.pdf"
|
href="{{ companyURI "/invoices/" }}{{ .Slug }}.pdf"
|
||||||
download="{{ .Number}}.pdf">{{( pgettext "Download invoice" "action" )}}</a>
|
download="{{ .Number}}-{{ .Invoicee.Name | slugify }}.pdf">{{( pgettext "Download invoice" "action" )}}</a>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
{{ define "breadcrumbs" -}}
|
{{ define "breadcrumbs" -}}
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.QuotesIndexPage*/ -}}
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.QuotesIndexPage*/ -}}
|
||||||
<nav data-hx-target="main" data-hx-boost="true">
|
<nav data-hx-target="main">
|
||||||
<p>
|
<p data-hx-boost="true">
|
||||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||||
<a>{{( pgettext "Quotations" "title" )}}</a>
|
<a>{{( pgettext "Quotations" "title" )}}</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
name="action" value="download"
|
name="action" value="download"
|
||||||
>{{( pgettext "Download quotations" "action" )}}</button>
|
>{{( pgettext "Download quotations" "action" )}}</button>
|
||||||
<a class="primary button"
|
<a class="primary button" data-hx-boost="true"
|
||||||
href="{{ companyURI "/quotes/new" }}">{{( pgettext "New quotation" "action" )}}</a>
|
href="{{ companyURI "/quotes/new" }}">{{( pgettext "New quotation" "action" )}}</a>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
Loading…
Reference in New Issue