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 }}