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.
This commit is contained in:
jordi fita mas 2023-02-22 14:39:38 +01:00
parent 32fdab4217
commit 97ef02b0f9
14 changed files with 428 additions and 110 deletions

32
deploy/invoice_amount.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -15,6 +15,7 @@ type InvoiceEntry struct {
Slug string Slug string
Date time.Time Date time.Time
Number string Number string
Total string
CustomerName string CustomerName string
CustomerSlug string CustomerSlug string
Status 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 { 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() defer rows.Close()
var entries []*InvoiceEntry var entries []*InvoiceEntry
for rows.Next() { for rows.Next() {
entry := &InvoiceEntry{} 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) panic(err)
} }
entries = append(entries, entry) entries = append(entries, entry)

113
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\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" "PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -68,17 +68,17 @@ msgstr "Preu"
msgid "No products added yet." msgid "No products added yet."
msgstr "No hi ha cap producte." 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" msgctxt "action"
msgid "Add products" msgid "Add products"
msgstr "Afegeix productes" msgstr "Afegeix productes"
#: web/template/invoices/new.gohtml:38 #: web/template/invoices/new.gohtml:40
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
msgstr "Actualitza" 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" msgctxt "action"
msgid "New invoice" msgid "New invoice"
msgstr "Nova factura" msgstr "Nova factura"
@ -115,10 +115,15 @@ msgstr "Etiqueta"
#: web/template/invoices/index.gohtml:27 #: web/template/invoices/index.gohtml:27
msgctxt "title" msgctxt "title"
msgid "Amount"
msgstr "Import"
#: web/template/invoices/index.gohtml:28
msgctxt "title"
msgid "Download" msgid "Download"
msgstr "Descàrrega" msgstr "Descàrrega"
#: web/template/invoices/index.gohtml:45 #: web/template/invoices/index.gohtml:47
msgid "No invoices added yet." msgid "No invoices added yet."
msgstr "No hi ha cap factura." msgstr "No hi ha cap factura."
@ -306,7 +311,7 @@ msgctxt "action"
msgid "Update product" msgid "Update product"
msgstr "Actualitza producte" 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" msgctxt "input"
msgid "Email" msgid "Email"
msgstr "Correu-e" msgstr "Correu-e"
@ -316,11 +321,11 @@ msgctxt "input"
msgid "Password" msgid "Password"
msgstr "Contrasenya" 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." msgid "Email can not be empty."
msgstr "No podeu deixar el correu-e en blanc." 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." 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." 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." msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes." msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/products.go:194 pkg/invoices.go:321 #: pkg/products.go:165 pkg/invoices.go:304
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: pkg/products.go:200 pkg/invoices.go:327 #: pkg/products.go:171 pkg/invoices.go:309
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: pkg/products.go:205 pkg/invoices.go:332 #: pkg/products.go:176 pkg/invoices.go:313
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Preu" 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" msgctxt "input"
msgid "Taxes" msgid "Taxes"
msgstr "Imposts" msgstr "Imposts"
#: pkg/products.go:235 pkg/profile.go:92 pkg/invoices.go:267 #: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:252
#: pkg/invoices.go:384 #: pkg/invoices.go:375
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc." 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." msgid "Price can not be empty."
msgstr "No podeu deixar el preu en blanc." 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." msgid "Price must be a number greater than zero."
msgstr "El preu ha de ser un número major a 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." msgid "Selected tax is not valid."
msgstr "Heu seleccionat un impost que no és vàlid." 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." msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid." msgstr "Heu seleccionat un idioma que no és vàlid."
#: pkg/invoices.go:66 #: pkg/invoices.go:71
msgid "Select a customer to bill." msgid "Select a customer to bill."
msgstr "Escolliu un client a facturar." msgstr "Escolliu un client a facturar."
#: pkg/invoices.go:163 #: pkg/invoices.go:145
msgid "Invalid action" msgid "Invalid action"
msgstr "Acció invàlida." msgstr "Acció invàlida."
#: pkg/invoices.go:214 #: pkg/invoices.go:196
msgctxt "input" msgctxt "input"
msgid "Customer" msgid "Customer"
msgstr "Client" msgstr "Client"
#: pkg/invoices.go:220 #: pkg/invoices.go:202
msgctxt "input" msgctxt "input"
msgid "Number" msgid "Number"
msgstr "Número" msgstr "Número"
#: pkg/invoices.go:226 #: pkg/invoices.go:208
msgctxt "input" msgctxt "input"
msgid "Invoice Date" msgid "Invoice Date"
msgstr "Data de factura" msgstr "Data de factura"
#: pkg/invoices.go:232 #: pkg/invoices.go:214
msgctxt "input" msgctxt "input"
msgid "Notes" msgid "Notes"
msgstr "Notes" msgstr "Notes"
#: pkg/invoices.go:268 #: pkg/invoices.go:253
msgid "Invoice date can not be empty." msgid "Invoice date can not be empty."
msgstr "No podeu deixar la data de la factura en blanc." 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." msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida." msgstr "La data de facturació ha de ser vàlida."
#: pkg/invoices.go:315 #: pkg/invoices.go:299
msgctxt "input" msgctxt "input"
msgid "Id" msgid "Id"
msgstr "Identificador" msgstr "Identificador"
#: pkg/invoices.go:342 #: pkg/invoices.go:322
msgctxt "input" msgctxt "input"
msgid "Quantity" msgid "Quantity"
msgstr "Quantitat" msgstr "Quantitat"
#: pkg/invoices.go:351 #: pkg/invoices.go:330
msgctxt "input" msgctxt "input"
msgid "Discount (%)" msgid "Discount (%)"
msgstr "Descompte (%)" msgstr "Descompte (%)"
#: pkg/invoices.go:388 #: pkg/invoices.go:379
msgid "Quantity can not be empty." msgid "Quantity can not be empty."
msgstr "No podeu deixar la quantitat en blanc." 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." msgid "Quantity must be a number greater than zero."
msgstr "La quantitat ha de ser un número major a 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." msgid "Discount can not be empty."
msgstr "No podeu deixar el descompte en blanc." 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." msgid "Discount must be a percentage between 0 and 100."
msgstr "El descompte ha de ser un percentatge entre 0 i 100." msgstr "El descompte ha de ser un percentatge entre 0 i 100."
#: pkg/contacts.go:149 #: pkg/contacts.go:143
msgctxt "input" msgctxt "input"
msgid "Business name" msgid "Business name"
msgstr "Nom i cognoms" msgstr "Nom i cognoms"
#: pkg/contacts.go:158 #: pkg/contacts.go:152
msgctxt "input" msgctxt "input"
msgid "VAT number" msgid "VAT number"
msgstr "DNI / NIF" msgstr "DNI / NIF"
#: pkg/contacts.go:164 #: pkg/contacts.go:158
msgctxt "input" msgctxt "input"
msgid "Trade name" msgid "Trade name"
msgstr "Nom comercial" msgstr "Nom comercial"
#: pkg/contacts.go:169 #: pkg/contacts.go:163
msgctxt "input" msgctxt "input"
msgid "Phone" msgid "Phone"
msgstr "Telèfon" msgstr "Telèfon"
#: pkg/contacts.go:187 #: pkg/contacts.go:181
msgctxt "input" msgctxt "input"
msgid "Web" msgid "Web"
msgstr "Web" msgstr "Web"
#: pkg/contacts.go:195 #: pkg/contacts.go:189
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
msgstr "Adreça" msgstr "Adreça"
#: pkg/contacts.go:204 #: pkg/contacts.go:198
msgctxt "input" msgctxt "input"
msgid "City" msgid "City"
msgstr "Població" msgstr "Població"
#: pkg/contacts.go:210 #: pkg/contacts.go:204
msgctxt "input" msgctxt "input"
msgid "Province" msgid "Province"
msgstr "Província" msgstr "Província"
#: pkg/contacts.go:216 #: pkg/contacts.go:210
msgctxt "input" msgctxt "input"
msgid "Postal code" msgid "Postal code"
msgstr "Codi postal" msgstr "Codi postal"
#: pkg/contacts.go:225 #: pkg/contacts.go:219
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
msgstr "País" msgstr "País"
#: pkg/contacts.go:258 #: pkg/contacts.go:252
msgid "Selected country is not valid." msgid "Selected country is not valid."
msgstr "Heu seleccionat un país que no és vàlid." 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." msgid "Business name can not be empty."
msgstr "No podeu deixar el nom i els cognoms en blanc." 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." msgid "VAT number can not be empty."
msgstr "No podeu deixar el DNI o NIF en blanc." 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." msgid "This value is not a valid VAT number."
msgstr "Aquest valor no és un DNI o NIF vàlid." 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." msgid "Phone can not be empty."
msgstr "No podeu deixar el telèfon en blanc." 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." msgid "This value is not a valid phone number."
msgstr "Aquest valor no és un telèfon vàlid." 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/." 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/." 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." msgid "Address can not be empty."
msgstr "No podeu deixar ladreça en blanc." msgstr "No podeu deixar ladreça en blanc."
#: pkg/contacts.go:276 #: pkg/contacts.go:270
msgid "City can not be empty." msgid "City can not be empty."
msgstr "No podeu deixar la població en blanc." msgstr "No podeu deixar la població en blanc."
#: pkg/contacts.go:277 #: pkg/contacts.go:271
msgid "Province can not be empty." msgid "Province can not be empty."
msgstr "No podeu deixar la província en blanc." msgstr "No podeu deixar la província en blanc."
#: pkg/contacts.go:278 #: pkg/contacts.go:272
msgid "Postal code can not be empty." msgid "Postal code can not be empty."
msgstr "No podeu deixar el codi postal en blanc." 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." msgid "This value is not a valid postal code."
msgstr "Aquest valor no és un codi postal vàlid." msgstr "Aquest valor no és un codi postal vàlid."

113
po/es.po
View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\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" "PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -68,17 +68,17 @@ msgstr "Precio"
msgid "No products added yet." msgid "No products added yet."
msgstr "No hay productos." 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" msgctxt "action"
msgid "Add products" msgid "Add products"
msgstr "Añadir productos" msgstr "Añadir productos"
#: web/template/invoices/new.gohtml:38 #: web/template/invoices/new.gohtml:40
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
msgstr "Actualizar" 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" msgctxt "action"
msgid "New invoice" msgid "New invoice"
msgstr "Nueva factura" msgstr "Nueva factura"
@ -115,10 +115,15 @@ msgstr "Etiqueta"
#: web/template/invoices/index.gohtml:27 #: web/template/invoices/index.gohtml:27
msgctxt "title" msgctxt "title"
msgid "Amount"
msgstr "Importe"
#: web/template/invoices/index.gohtml:28
msgctxt "title"
msgid "Download" msgid "Download"
msgstr "Descargar" msgstr "Descargar"
#: web/template/invoices/index.gohtml:45 #: web/template/invoices/index.gohtml:47
msgid "No invoices added yet." msgid "No invoices added yet."
msgstr "No hay facturas." msgstr "No hay facturas."
@ -306,7 +311,7 @@ msgctxt "action"
msgid "Update product" msgid "Update product"
msgstr "Actualizar producto" 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" msgctxt "input"
msgid "Email" msgid "Email"
msgstr "Correo-e" msgstr "Correo-e"
@ -316,11 +321,11 @@ msgctxt "input"
msgid "Password" msgid "Password"
msgstr "Contraseña" 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." msgid "Email can not be empty."
msgstr "No podéis dejar el correo-e en blanco." 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." 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." 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." msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido." 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" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: pkg/products.go:200 pkg/invoices.go:327 #: pkg/products.go:171 pkg/invoices.go:309
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripción" msgstr "Descripción"
#: pkg/products.go:205 pkg/invoices.go:332 #: pkg/products.go:176 pkg/invoices.go:313
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Precio" 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" msgctxt "input"
msgid "Taxes" msgid "Taxes"
msgstr "Impuestos" msgstr "Impuestos"
#: pkg/products.go:235 pkg/profile.go:92 pkg/invoices.go:267 #: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:252
#: pkg/invoices.go:384 #: pkg/invoices.go:375
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco." 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." msgid "Price can not be empty."
msgstr "No podéis dejar el precio en blanco." 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." msgid "Price must be a number greater than zero."
msgstr "El precio tiene que ser un número mayor a cero." 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." msgid "Selected tax is not valid."
msgstr "Habéis escogido un impuesto que no es válido." 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." msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido." 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." msgid "Select a customer to bill."
msgstr "Escoged un cliente a facturar." msgstr "Escoged un cliente a facturar."
#: pkg/invoices.go:163 #: pkg/invoices.go:145
msgid "Invalid action" msgid "Invalid action"
msgstr "Acción inválida." msgstr "Acción inválida."
#: pkg/invoices.go:214 #: pkg/invoices.go:196
msgctxt "input" msgctxt "input"
msgid "Customer" msgid "Customer"
msgstr "Cliente" msgstr "Cliente"
#: pkg/invoices.go:220 #: pkg/invoices.go:202
msgctxt "input" msgctxt "input"
msgid "Number" msgid "Number"
msgstr "Número" msgstr "Número"
#: pkg/invoices.go:226 #: pkg/invoices.go:208
msgctxt "input" msgctxt "input"
msgid "Invoice Date" msgid "Invoice Date"
msgstr "Fecha de factura" msgstr "Fecha de factura"
#: pkg/invoices.go:232 #: pkg/invoices.go:214
msgctxt "input" msgctxt "input"
msgid "Notes" msgid "Notes"
msgstr "Notas" msgstr "Notas"
#: pkg/invoices.go:268 #: pkg/invoices.go:253
msgid "Invoice date can not be empty." msgid "Invoice date can not be empty."
msgstr "No podéis dejar la fecha de la factura en blanco." 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." msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida." msgstr "La fecha de factura debe ser válida."
#: pkg/invoices.go:315 #: pkg/invoices.go:299
msgctxt "input" msgctxt "input"
msgid "Id" msgid "Id"
msgstr "Identificador" msgstr "Identificador"
#: pkg/invoices.go:342 #: pkg/invoices.go:322
msgctxt "input" msgctxt "input"
msgid "Quantity" msgid "Quantity"
msgstr "Cantidad" msgstr "Cantidad"
#: pkg/invoices.go:351 #: pkg/invoices.go:330
msgctxt "input" msgctxt "input"
msgid "Discount (%)" msgid "Discount (%)"
msgstr "Descuento (%)" msgstr "Descuento (%)"
#: pkg/invoices.go:388 #: pkg/invoices.go:379
msgid "Quantity can not be empty." msgid "Quantity can not be empty."
msgstr "No podéis dejar la cantidad en blanco." 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." msgid "Quantity must be a number greater than zero."
msgstr "La cantidad tiene que ser un número mayor a cero." 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." msgid "Discount can not be empty."
msgstr "No podéis dejar el descuento en blanco." 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." msgid "Discount must be a percentage between 0 and 100."
msgstr "El descuento tiene que ser un percentage entre 0 y 100." msgstr "El descuento tiene que ser un percentage entre 0 y 100."
#: pkg/contacts.go:149 #: pkg/contacts.go:143
msgctxt "input" msgctxt "input"
msgid "Business name" msgid "Business name"
msgstr "Nombre y apellidos" msgstr "Nombre y apellidos"
#: pkg/contacts.go:158 #: pkg/contacts.go:152
msgctxt "input" msgctxt "input"
msgid "VAT number" msgid "VAT number"
msgstr "DNI / NIF" msgstr "DNI / NIF"
#: pkg/contacts.go:164 #: pkg/contacts.go:158
msgctxt "input" msgctxt "input"
msgid "Trade name" msgid "Trade name"
msgstr "Nombre comercial" msgstr "Nombre comercial"
#: pkg/contacts.go:169 #: pkg/contacts.go:163
msgctxt "input" msgctxt "input"
msgid "Phone" msgid "Phone"
msgstr "Teléfono" msgstr "Teléfono"
#: pkg/contacts.go:187 #: pkg/contacts.go:181
msgctxt "input" msgctxt "input"
msgid "Web" msgid "Web"
msgstr "Web" msgstr "Web"
#: pkg/contacts.go:195 #: pkg/contacts.go:189
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
msgstr "Dirección" msgstr "Dirección"
#: pkg/contacts.go:204 #: pkg/contacts.go:198
msgctxt "input" msgctxt "input"
msgid "City" msgid "City"
msgstr "Población" msgstr "Población"
#: pkg/contacts.go:210 #: pkg/contacts.go:204
msgctxt "input" msgctxt "input"
msgid "Province" msgid "Province"
msgstr "Provincia" msgstr "Provincia"
#: pkg/contacts.go:216 #: pkg/contacts.go:210
msgctxt "input" msgctxt "input"
msgid "Postal code" msgid "Postal code"
msgstr "Código postal" msgstr "Código postal"
#: pkg/contacts.go:225 #: pkg/contacts.go:219
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
msgstr "País" msgstr "País"
#: pkg/contacts.go:258 #: pkg/contacts.go:252
msgid "Selected country is not valid." msgid "Selected country is not valid."
msgstr "Habéis escogido un país que no es válido." 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." msgid "Business name can not be empty."
msgstr "No podéis dejar el nombre y los apellidos en blanco." 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." msgid "VAT number can not be empty."
msgstr "No podéis dejar el DNI o NIF en blanco." 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." msgid "This value is not a valid VAT number."
msgstr "Este valor no es un DNI o NIF válido." 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." msgid "Phone can not be empty."
msgstr "No podéis dejar el teléfono en blanco." 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." msgid "This value is not a valid phone number."
msgstr "Este valor no es un teléfono válido." 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/." 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/." 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." msgid "Address can not be empty."
msgstr "No podéis dejar la dirección en blanco." msgstr "No podéis dejar la dirección en blanco."
#: pkg/contacts.go:276 #: pkg/contacts.go:270
msgid "City can not be empty." msgid "City can not be empty."
msgstr "No podéis dejar la población en blanco." msgstr "No podéis dejar la población en blanco."
#: pkg/contacts.go:277 #: pkg/contacts.go:271
msgid "Province can not be empty." msgid "Province can not be empty."
msgstr "No podéis dejar la provincia en blanco." msgstr "No podéis dejar la provincia en blanco."
#: pkg/contacts.go:278 #: pkg/contacts.go:272
msgid "Postal code can not be empty." msgid "Postal code can not be empty."
msgstr "No podéis dejar el código postal en blanco." 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." msgid "This value is not a valid postal code."
msgstr "Este valor no es un código postal válido válido." msgstr "Este valor no es un código postal válido válido."

View File

@ -0,0 +1,7 @@
-- Revert numerus:invoice_amount from pg
begin;
drop view if exists numerus.invoice_amount;
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:invoice_tax_amount from pg
begin;
drop view if exists numerus.invoice_tax_amount;
commit;

View File

@ -56,3 +56,5 @@ new_invoice_product [schema_numerus] 2023-02-16T21:06:01Z jordi fita mas <jordi@
invoice_number_counter [schema_numerus company] 2023-02-17T13:04:48Z jordi fita mas <jordi@tandem.blog> # Add relation to count invoice numbers invoice_number_counter [schema_numerus company] 2023-02-17T13:04:48Z jordi fita mas <jordi@tandem.blog> # Add relation to count invoice numbers
next_invoice_number [schema_numerus invoice_number_counter] 2023-02-17T13:21:48Z jordi fita mas <jordi@tandem.blog> # Add function to retrieve the next invoice number next_invoice_number [schema_numerus invoice_number_counter] 2023-02-17T13:21:48Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to create new invoices 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 <jordi@tandem.blog> # Add function to create new invoices
invoice_tax_amount [schema_numerus invoice_product invoice_product_tax] 2023-02-22T12:08:35Z jordi fita mas <jordi@tandem.blog> # Add view for invoice tax amount
invoice_amount [schema_numerus invoice_product invoice_tax_amount] 2023-02-22T12:58:46Z jordi fita mas <jordi@tandem.blog> # Add view to compute subtotal and total for invoices

101
test/invoice_amount.sql Normal file
View File

@ -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;

107
test/invoice_tax_amount.sql Normal file
View File

@ -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;

11
verify/invoice_amount.sql Normal file
View File

@ -0,0 +1,11 @@
-- Verify numerus:invoice_amount on pg
begin;
select invoice_id
, subtotal
, total
from numerus.invoice_amount
where false;
rollback;

View File

@ -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;

View File

@ -504,6 +504,10 @@ main > nav {
height: 100%; height: 100%;
} }
.numeric {
text-align: right;
}
/* Remix Icon */ /* Remix Icon */
@font-face { @font-face {

View File

@ -24,6 +24,7 @@
<th>{{( pgettext "Customer" "title" )}}</th> <th>{{( pgettext "Customer" "title" )}}</th>
<th>{{( pgettext "Status" "title" )}}</th> <th>{{( pgettext "Status" "title" )}}</th>
<th>{{( pgettext "Label" "title" )}}</th> <th>{{( pgettext "Label" "title" )}}</th>
<th>{{( pgettext "Amount" "title" )}}</th>
<th>{{( pgettext "Download" "title" )}}</th> <th>{{( pgettext "Download" "title" )}}</th>
</tr> </tr>
</thead> </thead>
@ -37,6 +38,7 @@
<td><a href="{{ companyURI "/contacts/"}}{{ .CustomerSlug }}">{{ .CustomerName }}</a></td> <td><a href="{{ companyURI "/contacts/"}}{{ .CustomerSlug }}">{{ .CustomerName }}</a></td>
<td class="invoice-status-{{ .Status }}">{{ .StatusLabel }}</td> <td class="invoice-status-{{ .Status }}">{{ .StatusLabel }}</td>
<td></td> <td></td>
<td class="numeric">{{ .Total|formatPrice }}</td>
<td></td> <td></td>
</tr> </tr>
{{- end }} {{- end }}