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

113
po/ca.po
View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\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 dusuari 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 ladreç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."

113
po/es.po
View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\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."

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
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
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%;
}
.numeric {
text-align: right;
}
/* Remix Icon */
@font-face {

View File

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