From d6034ad7327166cef5457c3a4b93627b2ec97c10 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Wed, 1 Mar 2023 14:08:12 +0100 Subject: [PATCH] Add discount and tax classes columns to invoice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was actually the (first) reason we added the tax classes: to show them in columns on the invoice—without the class we would need a column for each tax rate, even though they are the same tax. The invoice design has the product total with taxes at the last column, above the tax base, that i am not so sure about, but it seems that it has not brought any problem whatsoever so far, so it remains as is. Had to reduce the invoice’s font size to give more space to the table or the columns would be right next to each other. Oriol also told me to add more vertical spacing to the table’s footer. --- deploy/invoice_amount.sql | 22 ++---- deploy/invoice_product_amount.sql | 22 ++++++ pkg/invoices.go | 48 +++++++++---- pkg/template.go | 13 ++++ po/ca.po | 76 ++++++++++++--------- po/es.po | 76 ++++++++++++--------- revert/invoice_product_amount.sql | 7 ++ sqitch.plan | 3 +- test/invoice_product_amount.sql | 110 ++++++++++++++++++++++++++++++ verify/invoice_product_amount.sql | 11 +++ web/static/invoice.css | 12 ++-- web/template/invoices/view.gohtml | 48 ++++++++----- 12 files changed, 334 insertions(+), 114 deletions(-) create mode 100644 deploy/invoice_product_amount.sql create mode 100644 revert/invoice_product_amount.sql create mode 100644 test/invoice_product_amount.sql create mode 100644 verify/invoice_product_amount.sql diff --git a/deploy/invoice_amount.sql b/deploy/invoice_amount.sql index 9d3133a..bea000e 100644 --- a/deploy/invoice_amount.sql +++ b/deploy/invoice_amount.sql @@ -1,29 +1,19 @@ -- Deploy numerus:invoice_amount to pg -- requires: schema_numerus -- requires: invoice_product --- requires: invoice_tax_amount +-- requires: invoice_product_amount begin; set search_path to numerus, public; create or replace view invoice_amount as -with taxable as ( - select invoice_id - , sum(round(price * quantity * (1 - discount_rate))::integer)::integer as subtotal - from invoice_product - group by invoice_id -), taxes as ( - select invoice_id - , sum(amount)::integer as tax_amount - from invoice_tax_amount - group by invoice_id -) select invoice_id - , subtotal - , subtotal + coalesce(tax_amount, 0) as total -from taxable -left join taxes using (invoice_id) + , sum(subtotal)::integer as subtotal + , sum(total)::integer as total +from invoice_product +join invoice_product_amount using (invoice_product_id) +group by invoice_id ; grant select on table invoice_amount to invoicer; diff --git a/deploy/invoice_product_amount.sql b/deploy/invoice_product_amount.sql new file mode 100644 index 0000000..af05409 --- /dev/null +++ b/deploy/invoice_product_amount.sql @@ -0,0 +1,22 @@ +-- Deploy numerus:invoice_product_amount to pg +-- requires: schema_numerus +-- requires: invoice_product +-- requires: invoice_product_tax + +begin; + +set search_path to numerus, public; + +create or replace view invoice_product_amount as +select invoice_product_id + , round(price * quantity * (1 - discount_rate))::integer as subtotal + , max(round(price * quantity * (1 - discount_rate))::integer) + coalesce(sum(round(round(price * quantity * (1 - discount_rate))::integer * tax_rate)::integer)::integer, 0) as total +from invoice_product +left join invoice_product_tax using (invoice_product_id) +group by invoice_product_id, price, quantity, discount_rate +; + +grant select on table invoice_product_amount to invoicer; +grant select on table invoice_product_amount to admin; + +commit; diff --git a/pkg/invoices.go b/pkg/invoices.go index 8e24382..ce8f6fd 100644 --- a/pkg/invoices.go +++ b/pkg/invoices.go @@ -11,6 +11,7 @@ import ( "math" "net/http" "os/exec" + "sort" "strconv" "strings" "time" @@ -120,16 +121,18 @@ func mustClose(closer io.Closer) { } type invoice struct { - Number string - Slug string - Date time.Time - Invoicer taxDetails - Invoicee taxDetails - Notes string - Products []*invoiceProduct - Subtotal string - Taxes [][]string - Total string + Number string + Slug string + Date time.Time + Invoicer taxDetails + Invoicee taxDetails + Notes string + Products []*invoiceProduct + Subtotal string + Taxes [][]string + TaxClasses []string + HasDiscounts bool + Total string } type taxDetails struct { @@ -147,7 +150,10 @@ type invoiceProduct struct { Name string Description string Price string + Discount int Quantity int + Taxes map[string]int + Subtotal string Total string } @@ -166,15 +172,31 @@ func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug stri 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 name, description, to_price(price, $2), quantity, to_price(round(price * quantity * (1 - discount_rate))::integer, 2) from invoice_product where invoice_id = $1", 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]) 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) defer rows.Close() + taxClasses := map[string]bool{} for rows.Next() { - product := &invoiceProduct{} - if err := rows.Scan(&product.Name, &product.Description, &product.Price, &product.Quantity, &product.Total); err != nil { + 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 { panic(err) } + for _, tax := range taxes { + taxClass := tax[0] + taxClasses[taxClass] = true + product.Taxes[taxClass], _ = strconv.Atoi(tax[1]) + } + if product.Discount > 0 { + inv.HasDiscounts = true + } inv.Products = append(inv.Products, product) } + for taxClass := range taxClasses { + inv.TaxClasses = append(inv.TaxClasses, taxClass) + } + sort.Strings(inv.TaxClasses) if rows.Err() != nil { panic(rows.Err()) } diff --git a/pkg/template.go b/pkg/template.go index 5575d55..a11f95a 100644 --- a/pkg/template.go +++ b/pkg/template.go @@ -43,6 +43,9 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s "formatDate": func(time time.Time) template.HTML { return template.HTML(`") }, + "formatPercent": func(value int) string { + return fmt.Sprintf("%d %%", value) + }, "csrfToken": func() template.HTML { return template.HTML(fmt.Sprintf(``, csrfTokenField, user.CsrfToken)) }, @@ -54,6 +57,16 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s field.Attributes = append(field.Attributes, template.HTMLAttr(attr)) return field }, + "boolToInt": func(b bool) int { + if b { + return 1 + } else { + return 0 + } + }, + "add": func(y, x int) int { + return x + y + }, "sub": func(y, x int) int { return x - y }, diff --git a/po/ca.po b/po/ca.po index 754832b..a5b9126 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: numerus\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-02-28 11:56+0100\n" +"POT-Creation-Date: 2023-03-01 14:00+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -59,7 +59,7 @@ msgid "Name" msgstr "Nom" #: web/template/invoices/products.gohtml:43 -#: web/template/invoices/view.gohtml:59 web/template/products/index.gohtml:23 +#: web/template/invoices/view.gohtml:60 web/template/products/index.gohtml:23 msgctxt "title" msgid "Price" msgstr "Preu" @@ -74,13 +74,13 @@ msgctxt "action" msgid "Add products" msgstr "Afegeix productes" -#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:61 -#: web/template/invoices/view.gohtml:87 +#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:65 msgctxt "title" msgid "Subtotal" msgstr "Subtotal" -#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:97 +#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:69 +#: web/template/invoices/view.gohtml:109 msgctxt "title" msgid "Total" msgstr "Total" @@ -149,16 +149,26 @@ msgctxt "action" msgid "Download invoice" msgstr "Descarrega factura" -#: web/template/invoices/view.gohtml:58 +#: web/template/invoices/view.gohtml:59 msgctxt "title" msgid "Concept" msgstr "Concepte" -#: web/template/invoices/view.gohtml:60 +#: web/template/invoices/view.gohtml:62 +msgctxt "title" +msgid "Discount" +msgstr "Descompte" + +#: web/template/invoices/view.gohtml:64 msgctxt "title" msgid "Units" msgstr "Unitats" +#: web/template/invoices/view.gohtml:99 +msgctxt "title" +msgid "Tax Base" +msgstr "Base imposable" + #: web/template/dashboard.gohtml:2 msgctxt "title" msgid "Dashboard" @@ -374,43 +384,47 @@ msgstr "No podeu deixar la contrasenya en blanc." msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." -#: pkg/products.go:165 pkg/invoices.go:446 +#: pkg/products.go:165 pkg/invoices.go:468 msgctxt "input" msgid "Name" msgstr "Nom" -#: pkg/products.go:171 pkg/invoices.go:451 +#: pkg/products.go:171 pkg/invoices.go:473 msgctxt "input" msgid "Description" msgstr "Descripció" -#: pkg/products.go:176 pkg/invoices.go:455 +#: pkg/products.go:176 pkg/invoices.go:477 msgctxt "input" msgid "Price" msgstr "Preu" -#: pkg/products.go:186 pkg/invoices.go:344 pkg/invoices.go:481 +#: pkg/products.go:186 pkg/invoices.go:366 pkg/invoices.go:503 msgctxt "input" msgid "Taxes" msgstr "Imposts" -#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:377 -#: pkg/invoices.go:517 +#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:399 +#: pkg/invoices.go:539 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." -#: pkg/products.go:207 pkg/invoices.go:518 +#: pkg/products.go:207 pkg/invoices.go:540 msgid "Price can not be empty." msgstr "No podeu deixar el preu en blanc." -#: pkg/products.go:208 pkg/invoices.go:519 +#: pkg/products.go:208 pkg/invoices.go:541 msgid "Price must be a number greater than zero." msgstr "El preu ha de ser un número major a zero." -#: pkg/products.go:210 pkg/invoices.go:381 pkg/invoices.go:527 +#: pkg/products.go:210 pkg/invoices.go:403 pkg/invoices.go:549 msgid "Selected tax is not valid." msgstr "Heu seleccionat un impost que no és vàlid." +#: pkg/products.go:211 pkg/invoices.go:550 +msgid "You can only select a tax of each class." +msgstr "Només podeu seleccionar un impost de cada classe." + #: pkg/company.go:90 msgctxt "input" msgid "Currency" @@ -483,70 +497,70 @@ msgstr "La confirmació no és igual a la contrasenya." msgid "Selected language is not valid." msgstr "Heu seleccionat un idioma que no és vàlid." -#: pkg/invoices.go:207 +#: pkg/invoices.go:229 msgid "Select a customer to bill." msgstr "Escolliu un client a facturar." -#: pkg/invoices.go:300 +#: pkg/invoices.go:322 msgid "Invalid action" msgstr "Acció invàlida." -#: pkg/invoices.go:321 +#: pkg/invoices.go:343 msgctxt "input" msgid "Customer" msgstr "Client" -#: pkg/invoices.go:327 +#: pkg/invoices.go:349 msgctxt "input" msgid "Number" msgstr "Número" -#: pkg/invoices.go:333 +#: pkg/invoices.go:355 msgctxt "input" msgid "Invoice Date" msgstr "Data de factura" -#: pkg/invoices.go:339 +#: pkg/invoices.go:361 msgctxt "input" msgid "Notes" msgstr "Notes" -#: pkg/invoices.go:378 +#: pkg/invoices.go:400 msgid "Invoice date can not be empty." msgstr "No podeu deixar la data de la factura en blanc." -#: pkg/invoices.go:379 +#: pkg/invoices.go:401 msgid "Invoice date must be a valid date." msgstr "La data de facturació ha de ser vàlida." -#: pkg/invoices.go:441 +#: pkg/invoices.go:463 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/invoices.go:464 +#: pkg/invoices.go:486 msgctxt "input" msgid "Quantity" msgstr "Quantitat" -#: pkg/invoices.go:472 +#: pkg/invoices.go:494 msgctxt "input" msgid "Discount (%)" msgstr "Descompte (%)" -#: pkg/invoices.go:521 +#: pkg/invoices.go:543 msgid "Quantity can not be empty." msgstr "No podeu deixar la quantitat en blanc." -#: pkg/invoices.go:522 +#: pkg/invoices.go:544 msgid "Quantity must be a number greater than zero." msgstr "La quantitat ha de ser un número major a zero." -#: pkg/invoices.go:524 +#: pkg/invoices.go:546 msgid "Discount can not be empty." msgstr "No podeu deixar el descompte en blanc." -#: pkg/invoices.go:525 +#: pkg/invoices.go:547 msgid "Discount must be a percentage between 0 and 100." msgstr "El descompte ha de ser un percentatge entre 0 i 100." diff --git a/po/es.po b/po/es.po index 11ebb60..71cf600 100644 --- a/po/es.po +++ b/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: numerus\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-02-28 11:56+0100\n" +"POT-Creation-Date: 2023-03-01 14:00+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -59,7 +59,7 @@ msgid "Name" msgstr "Nombre" #: web/template/invoices/products.gohtml:43 -#: web/template/invoices/view.gohtml:59 web/template/products/index.gohtml:23 +#: web/template/invoices/view.gohtml:60 web/template/products/index.gohtml:23 msgctxt "title" msgid "Price" msgstr "Precio" @@ -74,13 +74,13 @@ msgctxt "action" msgid "Add products" msgstr "Añadir productos" -#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:61 -#: web/template/invoices/view.gohtml:87 +#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:65 msgctxt "title" msgid "Subtotal" msgstr "Subtotal" -#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:97 +#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:69 +#: web/template/invoices/view.gohtml:109 msgctxt "title" msgid "Total" msgstr "Total" @@ -149,16 +149,26 @@ msgctxt "action" msgid "Download invoice" msgstr "Descargar factura" -#: web/template/invoices/view.gohtml:58 +#: web/template/invoices/view.gohtml:59 msgctxt "title" msgid "Concept" msgstr "Concepto" -#: web/template/invoices/view.gohtml:60 +#: web/template/invoices/view.gohtml:62 +msgctxt "title" +msgid "Discount" +msgstr "Descuento" + +#: web/template/invoices/view.gohtml:64 msgctxt "title" msgid "Units" msgstr "Unidades" +#: web/template/invoices/view.gohtml:99 +msgctxt "title" +msgid "Tax Base" +msgstr "Base imponible" + #: web/template/dashboard.gohtml:2 msgctxt "title" msgid "Dashboard" @@ -374,43 +384,47 @@ msgstr "No podéis dejar la contraseña en blanco." msgid "Invalid user or password." msgstr "Nombre de usuario o contraseña inválido." -#: pkg/products.go:165 pkg/invoices.go:446 +#: pkg/products.go:165 pkg/invoices.go:468 msgctxt "input" msgid "Name" msgstr "Nombre" -#: pkg/products.go:171 pkg/invoices.go:451 +#: pkg/products.go:171 pkg/invoices.go:473 msgctxt "input" msgid "Description" msgstr "Descripción" -#: pkg/products.go:176 pkg/invoices.go:455 +#: pkg/products.go:176 pkg/invoices.go:477 msgctxt "input" msgid "Price" msgstr "Precio" -#: pkg/products.go:186 pkg/invoices.go:344 pkg/invoices.go:481 +#: pkg/products.go:186 pkg/invoices.go:366 pkg/invoices.go:503 msgctxt "input" msgid "Taxes" msgstr "Impuestos" -#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:377 -#: pkg/invoices.go:517 +#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:399 +#: pkg/invoices.go:539 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." -#: pkg/products.go:207 pkg/invoices.go:518 +#: pkg/products.go:207 pkg/invoices.go:540 msgid "Price can not be empty." msgstr "No podéis dejar el precio en blanco." -#: pkg/products.go:208 pkg/invoices.go:519 +#: pkg/products.go:208 pkg/invoices.go:541 msgid "Price must be a number greater than zero." msgstr "El precio tiene que ser un número mayor a cero." -#: pkg/products.go:210 pkg/invoices.go:381 pkg/invoices.go:527 +#: pkg/products.go:210 pkg/invoices.go:403 pkg/invoices.go:549 msgid "Selected tax is not valid." msgstr "Habéis escogido un impuesto que no es válido." +#: pkg/products.go:211 pkg/invoices.go:550 +msgid "You can only select a tax of each class." +msgstr "Solo podéis escojer un impuesto de cada clase." + #: pkg/company.go:90 msgctxt "input" msgid "Currency" @@ -483,70 +497,70 @@ msgstr "La confirmación no corresponde con la contraseña." msgid "Selected language is not valid." msgstr "Habéis escogido un idioma que no es válido." -#: pkg/invoices.go:207 +#: pkg/invoices.go:229 msgid "Select a customer to bill." msgstr "Escoged un cliente a facturar." -#: pkg/invoices.go:300 +#: pkg/invoices.go:322 msgid "Invalid action" msgstr "Acción inválida." -#: pkg/invoices.go:321 +#: pkg/invoices.go:343 msgctxt "input" msgid "Customer" msgstr "Cliente" -#: pkg/invoices.go:327 +#: pkg/invoices.go:349 msgctxt "input" msgid "Number" msgstr "Número" -#: pkg/invoices.go:333 +#: pkg/invoices.go:355 msgctxt "input" msgid "Invoice Date" msgstr "Fecha de factura" -#: pkg/invoices.go:339 +#: pkg/invoices.go:361 msgctxt "input" msgid "Notes" msgstr "Notas" -#: pkg/invoices.go:378 +#: pkg/invoices.go:400 msgid "Invoice date can not be empty." msgstr "No podéis dejar la fecha de la factura en blanco." -#: pkg/invoices.go:379 +#: pkg/invoices.go:401 msgid "Invoice date must be a valid date." msgstr "La fecha de factura debe ser válida." -#: pkg/invoices.go:441 +#: pkg/invoices.go:463 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/invoices.go:464 +#: pkg/invoices.go:486 msgctxt "input" msgid "Quantity" msgstr "Cantidad" -#: pkg/invoices.go:472 +#: pkg/invoices.go:494 msgctxt "input" msgid "Discount (%)" msgstr "Descuento (%)" -#: pkg/invoices.go:521 +#: pkg/invoices.go:543 msgid "Quantity can not be empty." msgstr "No podéis dejar la cantidad en blanco." -#: pkg/invoices.go:522 +#: pkg/invoices.go:544 msgid "Quantity must be a number greater than zero." msgstr "La cantidad tiene que ser un número mayor a cero." -#: pkg/invoices.go:524 +#: pkg/invoices.go:546 msgid "Discount can not be empty." msgstr "No podéis dejar el descuento en blanco." -#: pkg/invoices.go:525 +#: pkg/invoices.go:547 msgid "Discount must be a percentage between 0 and 100." msgstr "El descuento tiene que ser un percentage entre 0 y 100." diff --git a/revert/invoice_product_amount.sql b/revert/invoice_product_amount.sql new file mode 100644 index 0000000..462421f --- /dev/null +++ b/revert/invoice_product_amount.sql @@ -0,0 +1,7 @@ +-- Revert numerus:invoice_product_amount from pg + +begin; + +drop view if exists numerus.invoice_product_amount; + +commit; diff --git a/sqitch.plan b/sqitch.plan index f9854ff..9e2bc7c 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -58,6 +58,7 @@ invoice_number_counter [schema_numerus company] 2023-02-17T13:04:48Z jordi fita next_invoice_number [schema_numerus invoice_number_counter] 2023-02-17T13:21:48Z jordi fita mas # Add function to retrieve the next invoice number add_invoice [schema_numerus invoice company currency parse_price new_invoice_product tax invoice_product invoice_product_tax next_invoice_number] 2023-02-16T21:12:46Z jordi fita mas # Add function to create new invoices invoice_tax_amount [schema_numerus invoice_product invoice_product_tax] 2023-02-22T12:08:35Z jordi fita mas # Add view for invoice tax amount -invoice_amount [schema_numerus invoice_product invoice_tax_amount] 2023-02-22T12:58:46Z jordi fita mas # Add view to compute subtotal and total for invoices +invoice_product_amount [schema_numerus invoice_product invoice_product_tax] 2023-03-01T11:18:05Z jordi fita mas # Add view for invoice product subtotal and total +invoice_amount [schema_numerus invoice_product invoice_product_amount] 2023-02-22T12:58:46Z jordi fita mas # Add view to compute subtotal and total for invoices new_invoice_amount [schema_numerus] 2023-02-23T12:08:25Z jordi fita mas # Add type to return when computing new invoice amounts compute_new_invoice_amount [schema_numerus company currency tax new_invoice_product new_invoice_amount] 2023-02-23T12:20:13Z jordi fita mas # Add function to compute the subtotal, taxes, and total amounts for a new invoice diff --git a/test/invoice_product_amount.sql b/test/invoice_product_amount.sql new file mode 100644 index 0000000..ff9ce9a --- /dev/null +++ b/test/invoice_product_amount.sql @@ -0,0 +1,110 @@ +-- Test invoice_product_amount +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(12); + +set search_path to numerus, auth, public; + +select has_view('invoice_product_amount'); +select table_privs_are('invoice_product_amount', 'guest', array[]::text[]); +select table_privs_are('invoice_product_amount', 'invoicer', array['SELECT']); +select table_privs_are('invoice_product_amount', 'admin', array['SELECT']); +select table_privs_are('invoice_product_amount', 'authenticator', array[]::text[]); + +select has_column('invoice_product_amount', 'invoice_product_id'); +select col_type_is('invoice_product_amount', 'invoice_product_id', 'integer'); + +select has_column('invoice_product_amount', 'subtotal'); +select col_type_is('invoice_product_amount', 'subtotal', 'integer'); + +select has_column('invoice_product_amount', 'total'); +select col_type_is('invoice_product_amount', 'total', 'integer'); + + +set client_min_messages to warning; +truncate invoice_product_tax cascade; +truncate invoice_product cascade; +truncate invoice cascade; +truncate contact cascade; +truncate product cascade; +truncate tax cascade; +truncate tax_class cascade; +truncate company cascade; +reset client_min_messages; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code) +values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR') +; + +insert into tax_class (tax_class_id, company_id, name) +values (11, 1, 'tax') +; + +insert into tax (tax_id, company_id, tax_class_id, name, rate) +values (2, 1, 11, 'IRPF -15 %', -0.15) + , (3, 1, 11, 'IVA 4 %', 0.04) + , (4, 1, 11, 'IVA 10 %', 0.10) + , (5, 1, 11, 'IVA 21 %', 0.21) +; + +insert into product (product_id, company_id, name, price) +values (6, 1, 'Product', 1212) +; + +insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) +values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') +; + +insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code) +values ( 8, 1, 'I1', current_date, 7, 'EUR') + , ( 9, 1, 'I2', current_date, 7, 'EUR') + , (10, 1, 'I3', current_date, 7, 'EUR') + , (11, 1, 'I4', current_date, 7, 'EUR') +; + +insert into invoice_product (invoice_product_id, invoice_id, product_id, name, price, quantity, discount_rate) +values (12, 8, 6, 'P', 100, 1, 0.0) + , (13, 8, 6, 'P', 200, 2, 0.1) + , (14, 9, 6, 'P', 222, 3, 0.0) + , (15, 9, 6, 'P', 333, 4, 0.2) + , (16, 10, 6, 'P', 444, 5, 0.0) + , (17, 10, 6, 'P', 555, 6, 0.1) + , (18, 11, 6, 'P', 777, 8, 0.0) +; + +insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate) +values (12, 2, -0.15) + , (12, 5, 0.21) + , (13, 3, 0.04) + , (14, 4, 0.10) + , (14, 5, 0.21) + , (14, 2, -0.07) + , (15, 4, 0.10) + , (16, 4, 0.10) + , (16, 5, 0.21) + , (17, 5, 0.21) + , (17, 3, 0.04) +; + +select bag_eq( + $$ select invoice_product_id, subtotal, total from invoice_product_amount $$, + $$ values (12, 100, 106) + , (13, 360, 374) + , (14, 666, 826) + , (15, 1066, 1173) + , (16, 2220, 2908) + , (17, 2997, 3746) + , (18, 6216, 6216) + $$, + 'Should compute the subtotal and total for all products.' +); + + +select * +from finish(); + +rollback; diff --git a/verify/invoice_product_amount.sql b/verify/invoice_product_amount.sql new file mode 100644 index 0000000..d8367e0 --- /dev/null +++ b/verify/invoice_product_amount.sql @@ -0,0 +1,11 @@ +-- Verify numerus:invoice_product_amount on pg + +begin; + +select invoice_product_id + , subtotal + , total +from numerus.invoice_product_amount +where false; + +rollback; diff --git a/web/static/invoice.css b/web/static/invoice.css index f5cc9ef..c9efeb4 100644 --- a/web/static/invoice.css +++ b/web/static/invoice.css @@ -12,7 +12,7 @@ } .invoice h1 { - font-size: 1.6rem; + font-size: 1em; } .invoice > div { @@ -76,16 +76,20 @@ padding-top: 1em; } +.invoice .tfoot.separator th, .invoice .tfoot.separator td { + padding-top: 3em; +} + .invoice tbody .name td:first-child { font-weight: bold; } .invoice tbody td:first-child { - max-width: 20rem; + max-width: 15em; } .invoice .legal { - font-size: 1.2rem; + font-size: .75em; text-align: justify; } @@ -121,7 +125,7 @@ body { background-color: white; color: black; - font-size: 1.6rem; + font-size: 1rem; line-height: 1.5; -webkit-font-smoothing: antialiased; } diff --git a/web/template/invoices/view.gohtml b/web/template/invoices/view.gohtml index 0ab61b8..0088e7c 100644 --- a/web/template/invoices/view.gohtml +++ b/web/template/invoices/view.gohtml @@ -52,49 +52,61 @@ {{ .Invoicee.City }} ({{ .Invoicee.PostalCode}}), {{ .Invoicee.Province }}
+ {{- $columns := 5 | add (len .TaxClasses) | add (boolToInt .HasDiscounts) -}} + {{ if .HasDiscounts -}} + + {{ end -}} + {{ range $class := .TaxClasses -}} + + {{ end -}} + {{ $lastIndex := len .Products | sub 1 }} {{ range $index, $product := .Products -}} - {{ if .Description }} + {{- if .Description }} - + - + {{ end -}} + + {{- if .Description }} - - - - - {{ else }} - + {{- else }} - - - - - {{- end }} + {{- end -}} + + {{ if $.HasDiscounts -}} + + {{ end -}} + + + {{ range $class := $.TaxClasses -}} + + {{ end -}} + + {{ if (eq $index $lastIndex) }} - - + + {{ range $tax := $.Taxes -}} - + {{- end }} - + {{ end }}
{{( pgettext "Concept" "title" )}} {{( pgettext "Price" "title" )}}{{( pgettext "Discount" "title" )}}{{( pgettext "Units" "title" )}} {{( pgettext "Subtotal" "title" )}}{{ . }}{{( pgettext "Total" "title" )}}
{{ .Name }}{{ .Name }}
{{ .Description }}{{ .Price | formatPrice }}{{ .Quantity }}{{ .Total | formatPrice }}
{{ .Name }}{{ .Price | formatPrice }}{{ .Quantity }}{{ .Total | formatPrice }}
{{ .Price | formatPrice }}{{ $product.Discount | formatPercent }}{{ .Quantity }}{{ .Subtotal | formatPrice }}{{ index $product.Taxes $class | formatPercent }}{{ .Total | formatPrice }}
{{( pgettext "Subtotal" "title" )}}
{{( pgettext "Tax Base" "title" )}} {{ $.Subtotal | formatPrice }}
{{ index . 0 }}{{ index . 0 }} {{ index . 1 | formatPrice }}
{{( pgettext "Total" "title" )}}{{( pgettext "Total" "title" )}} {{ $.Total | formatPrice }}