From 97ef02b0f90a16d384966e97fe456b82bd163c88 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Wed, 22 Feb 2023 14:39:38 +0100 Subject: [PATCH] Add views to compute taxes and total amount of invoices They are not functions because i need to join them with the main invoice relation, and although possible is a bit more awkward with functions. The taxes have their own relation because i will need them grouped by their name in the PDF, so it will probably be a select for that relation. --- deploy/invoice_amount.sql | 32 ++++++++ deploy/invoice_tax_amount.sql | 23 ++++++ pkg/invoices.go | 5 +- po/ca.po | 113 +++++++++++++++-------------- po/es.po | 113 +++++++++++++++-------------- revert/invoice_amount.sql | 7 ++ revert/invoice_tax_amount.sql | 7 ++ sqitch.plan | 2 + test/invoice_amount.sql | 101 ++++++++++++++++++++++++++ test/invoice_tax_amount.sql | 107 +++++++++++++++++++++++++++ verify/invoice_amount.sql | 11 +++ verify/invoice_tax_amount.sql | 11 +++ web/static/numerus.css | 4 + web/template/invoices/index.gohtml | 2 + 14 files changed, 428 insertions(+), 110 deletions(-) create mode 100644 deploy/invoice_amount.sql create mode 100644 deploy/invoice_tax_amount.sql create mode 100644 revert/invoice_amount.sql create mode 100644 revert/invoice_tax_amount.sql create mode 100644 test/invoice_amount.sql create mode 100644 test/invoice_tax_amount.sql create mode 100644 verify/invoice_amount.sql create mode 100644 verify/invoice_tax_amount.sql diff --git a/deploy/invoice_amount.sql b/deploy/invoice_amount.sql new file mode 100644 index 0000000..9d3133a --- /dev/null +++ b/deploy/invoice_amount.sql @@ -0,0 +1,32 @@ +-- Deploy numerus:invoice_amount to pg +-- requires: schema_numerus +-- requires: invoice_product +-- requires: invoice_tax_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) +; + +grant select on table invoice_amount to invoicer; +grant select on table invoice_amount to admin; + +commit; diff --git a/deploy/invoice_tax_amount.sql b/deploy/invoice_tax_amount.sql new file mode 100644 index 0000000..1fb3a9a --- /dev/null +++ b/deploy/invoice_tax_amount.sql @@ -0,0 +1,23 @@ +-- Deploy numerus:invoice_tax_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_tax_amount as +select invoice_id + , tax_id + , sum(round(round(price * quantity * (1 - discount_rate))::integer * tax_rate)::integer)::integer as amount +from invoice_product +join invoice_product_tax using (invoice_product_id) +group by invoice_id + , tax_id +; + +grant select on table invoice_tax_amount to invoicer; +grant select on table invoice_tax_amount to admin; + +commit; diff --git a/pkg/invoices.go b/pkg/invoices.go index 170085d..a097fed 100644 --- a/pkg/invoices.go +++ b/pkg/invoices.go @@ -15,6 +15,7 @@ type InvoiceEntry struct { Slug string Date time.Time Number string + Total string CustomerName string CustomerSlug string Status string @@ -33,13 +34,13 @@ func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) } func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale) []*InvoiceEntry { - rows := conn.MustQuery(ctx, "select invoice.slug, invoice_date, invoice_number, contact.business_name, contact.slug, invoice.invoice_status, isi18n.name from invoice join contact using (contact_id) join invoice_status_i18n isi18n on invoice.invoice_status = isi18n.invoice_status and isi18n.lang_tag = $2 where invoice.company_id = $1 order by invoice_date, invoice_number", company.Id, locale.Language.String()) + rows := conn.MustQuery(ctx, "select invoice.slug, invoice_date, invoice_number, contact.business_name, contact.slug, invoice.invoice_status, isi18n.name, to_price(total, decimal_digits) from invoice join contact using (contact_id) join invoice_status_i18n isi18n on invoice.invoice_status = isi18n.invoice_status and isi18n.lang_tag = $2 join invoice_amount using (invoice_id) join currency using (currency_code) where invoice.company_id = $1 order by invoice_date, invoice_number", company.Id, locale.Language.String()) defer rows.Close() var entries []*InvoiceEntry for rows.Next() { entry := &InvoiceEntry{} - if err := rows.Scan(&entry.Slug, &entry.Date, &entry.Number, &entry.CustomerName, &entry.CustomerSlug, &entry.Status, &entry.StatusLabel); err != nil { + if err := rows.Scan(&entry.Slug, &entry.Date, &entry.Number, &entry.CustomerName, &entry.CustomerSlug, &entry.Status, &entry.StatusLabel, &entry.Total); err != nil { panic(err) } entries = append(entries, entry) diff --git a/po/ca.po b/po/ca.po index b086249..b295911 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-12 20:51+0100\n" +"POT-Creation-Date: 2023-02-22 14:35+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -68,17 +68,17 @@ msgstr "Preu" msgid "No products added yet." msgstr "No hi ha cap producte." -#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:37 +#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:38 msgctxt "action" msgid "Add products" msgstr "Afegeix productes" -#: web/template/invoices/new.gohtml:38 +#: web/template/invoices/new.gohtml:40 msgctxt "action" msgid "Update" msgstr "Actualitza" -#: web/template/invoices/new.gohtml:40 web/template/invoices/index.gohtml:13 +#: web/template/invoices/new.gohtml:42 web/template/invoices/index.gohtml:13 msgctxt "action" msgid "New invoice" msgstr "Nova factura" @@ -115,10 +115,15 @@ msgstr "Etiqueta" #: web/template/invoices/index.gohtml:27 msgctxt "title" +msgid "Amount" +msgstr "Import" + +#: web/template/invoices/index.gohtml:28 +msgctxt "title" msgid "Download" msgstr "Descàrrega" -#: web/template/invoices/index.gohtml:45 +#: web/template/invoices/index.gohtml:47 msgid "No invoices added yet." msgstr "No hi ha cap factura." @@ -306,7 +311,7 @@ msgctxt "action" msgid "Update product" msgstr "Actualitza producte" -#: pkg/login.go:36 pkg/profile.go:40 pkg/contacts.go:178 +#: pkg/login.go:36 pkg/profile.go:40 pkg/contacts.go:172 msgctxt "input" msgid "Email" msgstr "Correu-e" @@ -316,11 +321,11 @@ msgctxt "input" msgid "Password" msgstr "Contrasenya" -#: pkg/login.go:69 pkg/profile.go:89 pkg/contacts.go:269 +#: pkg/login.go:69 pkg/profile.go:89 pkg/contacts.go:263 msgid "Email can not be empty." msgstr "No podeu deixar el correu-e en blanc." -#: pkg/login.go:70 pkg/profile.go:90 pkg/contacts.go:270 +#: pkg/login.go:70 pkg/profile.go:90 pkg/contacts.go:264 msgid "This value is not a valid email. It should be like name@domain.com." msgstr "Aquest valor no és un correu-e vàlid. Hauria de ser similar a nom@domini.cat." @@ -332,40 +337,40 @@ msgstr "No podeu deixar la contrasenya en blanc." msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." -#: pkg/products.go:194 pkg/invoices.go:321 +#: pkg/products.go:165 pkg/invoices.go:304 msgctxt "input" msgid "Name" msgstr "Nom" -#: pkg/products.go:200 pkg/invoices.go:327 +#: pkg/products.go:171 pkg/invoices.go:309 msgctxt "input" msgid "Description" msgstr "Descripció" -#: pkg/products.go:205 pkg/invoices.go:332 +#: pkg/products.go:176 pkg/invoices.go:313 msgctxt "input" msgid "Price" msgstr "Preu" -#: pkg/products.go:215 pkg/invoices.go:237 pkg/invoices.go:361 +#: pkg/products.go:186 pkg/invoices.go:219 pkg/invoices.go:339 msgctxt "input" msgid "Taxes" msgstr "Imposts" -#: pkg/products.go:235 pkg/profile.go:92 pkg/invoices.go:267 -#: pkg/invoices.go:384 +#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:252 +#: pkg/invoices.go:375 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." -#: pkg/products.go:236 pkg/invoices.go:385 +#: pkg/products.go:207 pkg/invoices.go:376 msgid "Price can not be empty." msgstr "No podeu deixar el preu en blanc." -#: pkg/products.go:237 pkg/invoices.go:386 +#: pkg/products.go:208 pkg/invoices.go:377 msgid "Price must be a number greater than zero." msgstr "El preu ha de ser un número major a zero." -#: pkg/products.go:239 pkg/invoices.go:271 pkg/invoices.go:394 +#: pkg/products.go:210 pkg/invoices.go:256 pkg/invoices.go:385 msgid "Selected tax is not valid." msgstr "Heu seleccionat un impost que no és vàlid." @@ -428,168 +433,168 @@ 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:66 +#: pkg/invoices.go:71 msgid "Select a customer to bill." msgstr "Escolliu un client a facturar." -#: pkg/invoices.go:163 +#: pkg/invoices.go:145 msgid "Invalid action" msgstr "Acció invàlida." -#: pkg/invoices.go:214 +#: pkg/invoices.go:196 msgctxt "input" msgid "Customer" msgstr "Client" -#: pkg/invoices.go:220 +#: pkg/invoices.go:202 msgctxt "input" msgid "Number" msgstr "Número" -#: pkg/invoices.go:226 +#: pkg/invoices.go:208 msgctxt "input" msgid "Invoice Date" msgstr "Data de factura" -#: pkg/invoices.go:232 +#: pkg/invoices.go:214 msgctxt "input" msgid "Notes" msgstr "Notes" -#: pkg/invoices.go:268 +#: pkg/invoices.go:253 msgid "Invoice date can not be empty." msgstr "No podeu deixar la data de la factura en blanc." -#: pkg/invoices.go:269 +#: pkg/invoices.go:254 msgid "Invoice date must be a valid date." msgstr "La data de facturació ha de ser vàlida." -#: pkg/invoices.go:315 +#: pkg/invoices.go:299 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/invoices.go:342 +#: pkg/invoices.go:322 msgctxt "input" msgid "Quantity" msgstr "Quantitat" -#: pkg/invoices.go:351 +#: pkg/invoices.go:330 msgctxt "input" msgid "Discount (%)" msgstr "Descompte (%)" -#: pkg/invoices.go:388 +#: pkg/invoices.go:379 msgid "Quantity can not be empty." msgstr "No podeu deixar la quantitat en blanc." -#: pkg/invoices.go:389 +#: pkg/invoices.go:380 msgid "Quantity must be a number greater than zero." msgstr "La quantitat ha de ser un número major a zero." -#: pkg/invoices.go:391 +#: pkg/invoices.go:382 msgid "Discount can not be empty." msgstr "No podeu deixar el descompte en blanc." -#: pkg/invoices.go:392 +#: pkg/invoices.go:383 msgid "Discount must be a percentage between 0 and 100." msgstr "El descompte ha de ser un percentatge entre 0 i 100." -#: pkg/contacts.go:149 +#: pkg/contacts.go:143 msgctxt "input" msgid "Business name" msgstr "Nom i cognoms" -#: pkg/contacts.go:158 +#: pkg/contacts.go:152 msgctxt "input" msgid "VAT number" msgstr "DNI / NIF" -#: pkg/contacts.go:164 +#: pkg/contacts.go:158 msgctxt "input" msgid "Trade name" msgstr "Nom comercial" -#: pkg/contacts.go:169 +#: pkg/contacts.go:163 msgctxt "input" msgid "Phone" msgstr "Telèfon" -#: pkg/contacts.go:187 +#: pkg/contacts.go:181 msgctxt "input" msgid "Web" msgstr "Web" -#: pkg/contacts.go:195 +#: pkg/contacts.go:189 msgctxt "input" msgid "Address" msgstr "Adreça" -#: pkg/contacts.go:204 +#: pkg/contacts.go:198 msgctxt "input" msgid "City" msgstr "Població" -#: pkg/contacts.go:210 +#: pkg/contacts.go:204 msgctxt "input" msgid "Province" msgstr "Província" -#: pkg/contacts.go:216 +#: pkg/contacts.go:210 msgctxt "input" msgid "Postal code" msgstr "Codi postal" -#: pkg/contacts.go:225 +#: pkg/contacts.go:219 msgctxt "input" msgid "Country" msgstr "País" -#: pkg/contacts.go:258 +#: pkg/contacts.go:252 msgid "Selected country is not valid." msgstr "Heu seleccionat un país que no és vàlid." -#: pkg/contacts.go:262 +#: pkg/contacts.go:256 msgid "Business name can not be empty." msgstr "No podeu deixar el nom i els cognoms en blanc." -#: pkg/contacts.go:263 +#: pkg/contacts.go:257 msgid "VAT number can not be empty." msgstr "No podeu deixar el DNI o NIF en blanc." -#: pkg/contacts.go:264 +#: pkg/contacts.go:258 msgid "This value is not a valid VAT number." msgstr "Aquest valor no és un DNI o NIF vàlid." -#: pkg/contacts.go:266 +#: pkg/contacts.go:260 msgid "Phone can not be empty." msgstr "No podeu deixar el telèfon en blanc." -#: pkg/contacts.go:267 +#: pkg/contacts.go:261 msgid "This value is not a valid phone number." msgstr "Aquest valor no és un telèfon vàlid." -#: pkg/contacts.go:273 +#: pkg/contacts.go:267 msgid "This value is not a valid web address. It should be like https://domain.com/." msgstr "Aquest valor no és una adreça web vàlida. Hauria de ser similar a https://domini.cat/." -#: pkg/contacts.go:275 +#: pkg/contacts.go:269 msgid "Address can not be empty." msgstr "No podeu deixar l’adreça en blanc." -#: pkg/contacts.go:276 +#: pkg/contacts.go:270 msgid "City can not be empty." msgstr "No podeu deixar la població en blanc." -#: pkg/contacts.go:277 +#: pkg/contacts.go:271 msgid "Province can not be empty." msgstr "No podeu deixar la província en blanc." -#: pkg/contacts.go:278 +#: pkg/contacts.go:272 msgid "Postal code can not be empty." msgstr "No podeu deixar el codi postal en blanc." -#: pkg/contacts.go:279 +#: pkg/contacts.go:273 msgid "This value is not a valid postal code." msgstr "Aquest valor no és un codi postal vàlid." diff --git a/po/es.po b/po/es.po index 3479ad3..f5b1c58 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-12 20:51+0100\n" +"POT-Creation-Date: 2023-02-22 14:35+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -68,17 +68,17 @@ msgstr "Precio" msgid "No products added yet." msgstr "No hay productos." -#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:37 +#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:38 msgctxt "action" msgid "Add products" msgstr "Añadir productos" -#: web/template/invoices/new.gohtml:38 +#: web/template/invoices/new.gohtml:40 msgctxt "action" msgid "Update" msgstr "Actualizar" -#: web/template/invoices/new.gohtml:40 web/template/invoices/index.gohtml:13 +#: web/template/invoices/new.gohtml:42 web/template/invoices/index.gohtml:13 msgctxt "action" msgid "New invoice" msgstr "Nueva factura" @@ -115,10 +115,15 @@ msgstr "Etiqueta" #: web/template/invoices/index.gohtml:27 msgctxt "title" +msgid "Amount" +msgstr "Importe" + +#: web/template/invoices/index.gohtml:28 +msgctxt "title" msgid "Download" msgstr "Descargar" -#: web/template/invoices/index.gohtml:45 +#: web/template/invoices/index.gohtml:47 msgid "No invoices added yet." msgstr "No hay facturas." @@ -306,7 +311,7 @@ msgctxt "action" msgid "Update product" msgstr "Actualizar producto" -#: pkg/login.go:36 pkg/profile.go:40 pkg/contacts.go:178 +#: pkg/login.go:36 pkg/profile.go:40 pkg/contacts.go:172 msgctxt "input" msgid "Email" msgstr "Correo-e" @@ -316,11 +321,11 @@ msgctxt "input" msgid "Password" msgstr "Contraseña" -#: pkg/login.go:69 pkg/profile.go:89 pkg/contacts.go:269 +#: pkg/login.go:69 pkg/profile.go:89 pkg/contacts.go:263 msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." -#: pkg/login.go:70 pkg/profile.go:90 pkg/contacts.go:270 +#: pkg/login.go:70 pkg/profile.go:90 pkg/contacts.go:264 msgid "This value is not a valid email. It should be like name@domain.com." msgstr "Este valor no es un correo-e válido. Tiene que ser parecido a nombre@dominio.es." @@ -332,40 +337,40 @@ 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:194 pkg/invoices.go:321 +#: pkg/products.go:165 pkg/invoices.go:304 msgctxt "input" msgid "Name" msgstr "Nombre" -#: pkg/products.go:200 pkg/invoices.go:327 +#: pkg/products.go:171 pkg/invoices.go:309 msgctxt "input" msgid "Description" msgstr "Descripción" -#: pkg/products.go:205 pkg/invoices.go:332 +#: pkg/products.go:176 pkg/invoices.go:313 msgctxt "input" msgid "Price" msgstr "Precio" -#: pkg/products.go:215 pkg/invoices.go:237 pkg/invoices.go:361 +#: pkg/products.go:186 pkg/invoices.go:219 pkg/invoices.go:339 msgctxt "input" msgid "Taxes" msgstr "Impuestos" -#: pkg/products.go:235 pkg/profile.go:92 pkg/invoices.go:267 -#: pkg/invoices.go:384 +#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:252 +#: pkg/invoices.go:375 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." -#: pkg/products.go:236 pkg/invoices.go:385 +#: pkg/products.go:207 pkg/invoices.go:376 msgid "Price can not be empty." msgstr "No podéis dejar el precio en blanco." -#: pkg/products.go:237 pkg/invoices.go:386 +#: pkg/products.go:208 pkg/invoices.go:377 msgid "Price must be a number greater than zero." msgstr "El precio tiene que ser un número mayor a cero." -#: pkg/products.go:239 pkg/invoices.go:271 pkg/invoices.go:394 +#: pkg/products.go:210 pkg/invoices.go:256 pkg/invoices.go:385 msgid "Selected tax is not valid." msgstr "Habéis escogido un impuesto que no es válido." @@ -428,168 +433,168 @@ 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:66 +#: pkg/invoices.go:71 msgid "Select a customer to bill." msgstr "Escoged un cliente a facturar." -#: pkg/invoices.go:163 +#: pkg/invoices.go:145 msgid "Invalid action" msgstr "Acción inválida." -#: pkg/invoices.go:214 +#: pkg/invoices.go:196 msgctxt "input" msgid "Customer" msgstr "Cliente" -#: pkg/invoices.go:220 +#: pkg/invoices.go:202 msgctxt "input" msgid "Number" msgstr "Número" -#: pkg/invoices.go:226 +#: pkg/invoices.go:208 msgctxt "input" msgid "Invoice Date" msgstr "Fecha de factura" -#: pkg/invoices.go:232 +#: pkg/invoices.go:214 msgctxt "input" msgid "Notes" msgstr "Notas" -#: pkg/invoices.go:268 +#: pkg/invoices.go:253 msgid "Invoice date can not be empty." msgstr "No podéis dejar la fecha de la factura en blanco." -#: pkg/invoices.go:269 +#: pkg/invoices.go:254 msgid "Invoice date must be a valid date." msgstr "La fecha de factura debe ser válida." -#: pkg/invoices.go:315 +#: pkg/invoices.go:299 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/invoices.go:342 +#: pkg/invoices.go:322 msgctxt "input" msgid "Quantity" msgstr "Cantidad" -#: pkg/invoices.go:351 +#: pkg/invoices.go:330 msgctxt "input" msgid "Discount (%)" msgstr "Descuento (%)" -#: pkg/invoices.go:388 +#: pkg/invoices.go:379 msgid "Quantity can not be empty." msgstr "No podéis dejar la cantidad en blanco." -#: pkg/invoices.go:389 +#: pkg/invoices.go:380 msgid "Quantity must be a number greater than zero." msgstr "La cantidad tiene que ser un número mayor a cero." -#: pkg/invoices.go:391 +#: pkg/invoices.go:382 msgid "Discount can not be empty." msgstr "No podéis dejar el descuento en blanco." -#: pkg/invoices.go:392 +#: pkg/invoices.go:383 msgid "Discount must be a percentage between 0 and 100." msgstr "El descuento tiene que ser un percentage entre 0 y 100." -#: pkg/contacts.go:149 +#: pkg/contacts.go:143 msgctxt "input" msgid "Business name" msgstr "Nombre y apellidos" -#: pkg/contacts.go:158 +#: pkg/contacts.go:152 msgctxt "input" msgid "VAT number" msgstr "DNI / NIF" -#: pkg/contacts.go:164 +#: pkg/contacts.go:158 msgctxt "input" msgid "Trade name" msgstr "Nombre comercial" -#: pkg/contacts.go:169 +#: pkg/contacts.go:163 msgctxt "input" msgid "Phone" msgstr "Teléfono" -#: pkg/contacts.go:187 +#: pkg/contacts.go:181 msgctxt "input" msgid "Web" msgstr "Web" -#: pkg/contacts.go:195 +#: pkg/contacts.go:189 msgctxt "input" msgid "Address" msgstr "Dirección" -#: pkg/contacts.go:204 +#: pkg/contacts.go:198 msgctxt "input" msgid "City" msgstr "Población" -#: pkg/contacts.go:210 +#: pkg/contacts.go:204 msgctxt "input" msgid "Province" msgstr "Provincia" -#: pkg/contacts.go:216 +#: pkg/contacts.go:210 msgctxt "input" msgid "Postal code" msgstr "Código postal" -#: pkg/contacts.go:225 +#: pkg/contacts.go:219 msgctxt "input" msgid "Country" msgstr "País" -#: pkg/contacts.go:258 +#: pkg/contacts.go:252 msgid "Selected country is not valid." msgstr "Habéis escogido un país que no es válido." -#: pkg/contacts.go:262 +#: pkg/contacts.go:256 msgid "Business name can not be empty." msgstr "No podéis dejar el nombre y los apellidos en blanco." -#: pkg/contacts.go:263 +#: pkg/contacts.go:257 msgid "VAT number can not be empty." msgstr "No podéis dejar el DNI o NIF en blanco." -#: pkg/contacts.go:264 +#: pkg/contacts.go:258 msgid "This value is not a valid VAT number." msgstr "Este valor no es un DNI o NIF válido." -#: pkg/contacts.go:266 +#: pkg/contacts.go:260 msgid "Phone can not be empty." msgstr "No podéis dejar el teléfono en blanco." -#: pkg/contacts.go:267 +#: pkg/contacts.go:261 msgid "This value is not a valid phone number." msgstr "Este valor no es un teléfono válido." -#: pkg/contacts.go:273 +#: pkg/contacts.go:267 msgid "This value is not a valid web address. It should be like https://domain.com/." msgstr "Este valor no es una dirección web válida. Tiene que ser parecida a https://dominio.es/." -#: pkg/contacts.go:275 +#: pkg/contacts.go:269 msgid "Address can not be empty." msgstr "No podéis dejar la dirección en blanco." -#: pkg/contacts.go:276 +#: pkg/contacts.go:270 msgid "City can not be empty." msgstr "No podéis dejar la población en blanco." -#: pkg/contacts.go:277 +#: pkg/contacts.go:271 msgid "Province can not be empty." msgstr "No podéis dejar la provincia en blanco." -#: pkg/contacts.go:278 +#: pkg/contacts.go:272 msgid "Postal code can not be empty." msgstr "No podéis dejar el código postal en blanco." -#: pkg/contacts.go:279 +#: pkg/contacts.go:273 msgid "This value is not a valid postal code." msgstr "Este valor no es un código postal válido válido." diff --git a/revert/invoice_amount.sql b/revert/invoice_amount.sql new file mode 100644 index 0000000..7254f5c --- /dev/null +++ b/revert/invoice_amount.sql @@ -0,0 +1,7 @@ +-- Revert numerus:invoice_amount from pg + +begin; + +drop view if exists numerus.invoice_amount; + +commit; diff --git a/revert/invoice_tax_amount.sql b/revert/invoice_tax_amount.sql new file mode 100644 index 0000000..14159e6 --- /dev/null +++ b/revert/invoice_tax_amount.sql @@ -0,0 +1,7 @@ +-- Revert numerus:invoice_tax_amount from pg + +begin; + +drop view if exists numerus.invoice_tax_amount; + +commit; diff --git a/sqitch.plan b/sqitch.plan index 898cf29..0238e02 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -56,3 +56,5 @@ new_invoice_product [schema_numerus] 2023-02-16T21:06:01Z jordi fita mas # Add relation to count invoice numbers 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 diff --git a/test/invoice_amount.sql b/test/invoice_amount.sql new file mode 100644 index 0000000..9b01ed5 --- /dev/null +++ b/test/invoice_amount.sql @@ -0,0 +1,101 @@ +-- Test invoice_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_amount'); +select table_privs_are('invoice_amount', 'guest', array[]::text[]); +select table_privs_are('invoice_amount', 'invoicer', array['SELECT']); +select table_privs_are('invoice_amount', 'admin', array['SELECT']); +select table_privs_are('invoice_amount', 'authenticator', array[]::text[]); + +select has_column('invoice_amount', 'invoice_id'); +select col_type_is('invoice_amount', 'invoice_id', 'integer'); + +select has_column('invoice_amount', 'subtotal'); +select col_type_is('invoice_amount', 'subtotal', 'integer'); + +select has_column('invoice_amount', 'total'); +select col_type_is('invoice_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 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 (tax_id, company_id, name, rate) +values (2, 1, 'IRPF -15 %', -0.15) + , (3, 1, 'IVA 4 %', 0.04) + , (4, 1, 'IVA 10 %', 0.10) + , (5, 1, '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_id, subtotal, total from invoice_amount $$, + $$ values ( 8, 460, 480) + , ( 9, 1732, 1999) + , (10, 5217, 6654) + , (11, 6216, 6216) + $$, + 'Should compute the amount for all taxes in the invoiced products.' +); + +select * +from finish(); + +rollback; diff --git a/test/invoice_tax_amount.sql b/test/invoice_tax_amount.sql new file mode 100644 index 0000000..1b537da --- /dev/null +++ b/test/invoice_tax_amount.sql @@ -0,0 +1,107 @@ +-- Test invoice_tax_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_tax_amount'); +select table_privs_are('invoice_tax_amount', 'guest', array[]::text[]); +select table_privs_are('invoice_tax_amount', 'invoicer', array['SELECT']); +select table_privs_are('invoice_tax_amount', 'admin', array['SELECT']); +select table_privs_are('invoice_tax_amount', 'authenticator', array[]::text[]); + +select has_column('invoice_tax_amount', 'invoice_id'); +select col_type_is('invoice_tax_amount', 'invoice_id', 'integer'); + +select has_column('invoice_tax_amount', 'tax_id'); +select col_type_is('invoice_tax_amount', 'tax_id', 'integer'); + +select has_column('invoice_tax_amount', 'amount'); +select col_type_is('invoice_tax_amount', 'amount', '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 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 (tax_id, company_id, name, rate) +values (2, 1, 'IRPF -15 %', -0.15) + , (3, 1, 'IVA 4 %', 0.04) + , (4, 1, 'IVA 10 %', 0.10) + , (5, 1, '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_id, tax_id, amount from invoice_tax_amount $$, + $$ values ( 8, 2, -15) + , ( 8, 3, 14) + , ( 8, 5, 21) + , ( 9, 2, -47) + , ( 9, 4, 174) + , ( 9, 5, 140) + , (10, 3, 120) + , (10, 4, 222) + , (10, 5, 1095) + $$, + 'Should compute the amount for all taxes in the invoiced products.' +); + + +select * +from finish(); + +rollback; diff --git a/verify/invoice_amount.sql b/verify/invoice_amount.sql new file mode 100644 index 0000000..e2823b1 --- /dev/null +++ b/verify/invoice_amount.sql @@ -0,0 +1,11 @@ +-- Verify numerus:invoice_amount on pg + +begin; + +select invoice_id + , subtotal + , total +from numerus.invoice_amount +where false; + +rollback; diff --git a/verify/invoice_tax_amount.sql b/verify/invoice_tax_amount.sql new file mode 100644 index 0000000..b9a3e36 --- /dev/null +++ b/verify/invoice_tax_amount.sql @@ -0,0 +1,11 @@ +-- Verify numerus:invoice_tax_amount on pg + +begin; + +select invoice_id + , tax_id + , amount +from numerus.invoice_tax_amount +where false; + +rollback; diff --git a/web/static/numerus.css b/web/static/numerus.css index be51d77..11c07d2 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -504,6 +504,10 @@ main > nav { height: 100%; } +.numeric { + text-align: right; +} + /* Remix Icon */ @font-face { diff --git a/web/template/invoices/index.gohtml b/web/template/invoices/index.gohtml index bd7045e..2838866 100644 --- a/web/template/invoices/index.gohtml +++ b/web/template/invoices/index.gohtml @@ -24,6 +24,7 @@ {{( pgettext "Customer" "title" )}} {{( pgettext "Status" "title" )}} {{( pgettext "Label" "title" )}} + {{( pgettext "Amount" "title" )}} {{( pgettext "Download" "title" )}} @@ -37,6 +38,7 @@ {{ .CustomerName }} {{ .StatusLabel }} + {{ .Total|formatPrice }} {{- end }}