Keep products in invoices/quotes sorted by (roughly) insertion order

There was no explicit `order by` in the queries that list the products
of quotes and invoices, so PostgreSQL was free to use any order it
wanted.  In this case, since was am grouping first by name, the result
was sorted by product name.

This is not an issue in most cases, albeit a bit rude to the user,
except for when the products *have* to in the same order the user
entered them, because they are monthly fees or something like that, that
must be ordered by month _number_, not by their _name_; the user will
usually input them in the correct order they want them on the invoice or
quote.

Sorting by *_product_id does *not* guarantee that they will always be
in insertion order, because the sequence can “wrap”, but i think i am
going to have bigger problems at that point.

Closes #63
This commit is contained in:
jordi fita mas 2023-07-07 10:39:22 +02:00
parent 58dd69773a
commit 1bb6870f26
2 changed files with 121 additions and 8 deletions

View File

@ -416,13 +416,63 @@ func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug stri
&inv.Total)) {
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)
}
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)
}
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()
taxClasses := map[string]bool{}
for rows.Next() {
@ -430,7 +480,15 @@ func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug stri
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 {
if err := rows.Scan(
&product.Name,
&product.Description,
&product.Price,
&product.Discount,
&product.Quantity,
&product.Subtotal,
&product.Total,
&taxes); err != nil {
panic(err)
}
for _, tax := range taxes {

View File

@ -410,13 +410,60 @@ func mustGetQuote(ctx context.Context, conn *Conn, company *Company, slug string
&quo.Total)) {
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)
}
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)
}
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()
taxClasses := map[string]bool{}
for rows.Next() {
@ -424,7 +471,15 @@ func mustGetQuote(ctx context.Context, conn *Conn, company *Company, slug string
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 {
if err := rows.Scan(
&product.Name,
&product.Description,
&product.Price,
&product.Discount,
&product.Quantity,
&product.Subtotal,
&product.Total,
&taxes); err != nil {
panic(err)
}
for _, tax := range taxes {