Add invoice tags

I followed the same restrictions as Gitea’s topics, arbitrarily, because
if it is enough for repositories it should be for invoices too,
apparently.
This commit is contained in:
jordi fita mas 2023-03-10 14:02:55 +01:00
parent 5dedaefc22
commit 2bc05e948c
24 changed files with 724 additions and 128 deletions

View File

@ -60,14 +60,15 @@ select add_product(1, 'Cavall Fort', 'Revista quinzenal en llengua catalana i de
select add_product(1, 'Palla', 'Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.', '25.00', array[3]);
select add_product(1, 'Teia', 'Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.', '7.00', array[2]);
alter sequence tag_tag_id_seq restart;
alter sequence invoice_invoice_id_seq restart;
alter sequence invoice_product_invoice_product_id_seq restart;
select add_invoice(1, '', (current_date - '28 days'::interval)::date, 6, 'Vol esmorzar!', 1, '{"(1,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '24 days'::interval)::date, 5, '', 1, '{"(1,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{3})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '17 days'::interval)::date, 4, '', 1, '{"(1,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{4})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '7 days'::interval)::date, 3, '', 1, '{"(1,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '4 days'::interval)::date, 2, '', 1, '{"(1,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '1 days'::interval)::date, 1, '', 1, '{"(1,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '28 days'::interval)::date, 6, 'Vol esmorzar!', 1, '{producte}','{"(1,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '24 days'::interval)::date, 5, '', 1, '{producte,bestia}','{"(1,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{3})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '17 days'::interval)::date, 4, '', 1, '{producte,higiene}','{"(1,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{4})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '7 days'::interval)::date, 3, '', 1, '{producte,mag}','{"(1,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '4 days'::interval)::date, 2, '', 1, '{producte,mag}','{"(1,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{2})"}');
select add_invoice(1, '', (current_date - '1 days'::interval)::date, 1, '', 1, '{producte,mag}','{"(1,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{2})"}');
update invoice set invoice_status = 'paid' where invoice_id in (1, 5);
update invoice set invoice_status = 'unpaid' where invoice_id = 3;

View File

@ -9,12 +9,15 @@
-- requires: invoice_product
-- requires: invoice_product_tax
-- requires: next_invoice_number
-- requires: tag_name
-- requires: tag
-- requires: invoice_tag
begin;
set search_path to numerus, public;
create or replace function add_invoice(company_id integer, invoice_number text, invoice_date date, contact_id integer, notes text, payment_method_id integer, products new_invoice_product[]) returns uuid as
create or replace function add_invoice(company integer, invoice_number text, invoice_date date, contact_id integer, notes text, payment_method_id integer, tags tag_name[], products new_invoice_product[]) returns uuid as
$$
declare
iid integer;
@ -24,11 +27,11 @@ declare
ipid integer;
begin
if invoice_number is null or length(trim(invoice_number)) = 0 then
invoice_number = next_invoice_number(company_id, invoice_date);
invoice_number = next_invoice_number(company, invoice_date);
end if;
insert into invoice (company_id, invoice_number, invoice_date, contact_id, notes, currency_code, payment_method_id)
select company.company_id
select company_id
, invoice_number
, invoice_date
, contact_id
@ -36,7 +39,7 @@ begin
, currency_code
, add_invoice.payment_method_id
from company
where company.company_id = add_invoice.company_id
where company.company_id = add_invoice.company
returning invoice_id, slug, currency_code
into iid, pslug, ccode;
@ -61,13 +64,27 @@ begin
join unnest(product.tax) as ptax(tax_id) using (tax_id);
end loop;
if array_length(tags, 1) > 0 then
insert into tag (company_id, name)
select add_invoice.company, new_tag.name
from unnest (tags) as new_tag(name)
on conflict (company_id, name) do nothing
;
insert into invoice_tag (invoice_id, tag_id)
select iid, tag_id
from tag
join unnest (tags) as new_tag(name) on company_id = add_invoice.company and tag.name = new_tag.name
;
end if;
return pslug;
end;
$$
language plpgsql;
revoke execute on function add_invoice(integer, text, date, integer, text, integer, new_invoice_product[]) from public;
grant execute on function add_invoice(integer, text, date, integer, text, integer, new_invoice_product[]) to invoicer;
grant execute on function add_invoice(integer, text, date, integer, text, integer, new_invoice_product[]) to admin;
revoke execute on function add_invoice(integer, text, date, integer, text, integer, tag_name[], new_invoice_product[]) from public;
grant execute on function add_invoice(integer, text, date, integer, text, integer, tag_name[], new_invoice_product[]) to invoicer;
grant execute on function add_invoice(integer, text, date, integer, text, integer, tag_name[], new_invoice_product[]) to admin;
commit;

31
deploy/invoice_tag.sql Normal file
View File

@ -0,0 +1,31 @@
-- Deploy numerus:invoice_tag to pg
-- requires: schema_numerus
-- requires: tag
-- requires: invoice
begin;
set search_path to numerus, public;
create table invoice_tag (
invoice_id integer not null references invoice,
tag_id integer not null references tag,
primary key (invoice_id, tag_id)
);
grant select, insert, update, delete on table invoice_tag to invoicer;
grant select, insert, update, delete on table invoice_tag to admin;
alter table invoice_tag enable row level security;
create policy company_policy
on invoice_tag
using (
exists(
select 1
from invoice
where invoice.invoice_id = invoice_tag.invoice_id
)
);
commit;

35
deploy/tag.sql Normal file
View File

@ -0,0 +1,35 @@
-- Deploy numerus:tag to pg
-- requires: schema_numerus
-- requires: tag_name
begin;
set search_path to numerus, public;
create table tag (
tag_id serial primary key,
company_id integer not null references company,
name tag_name not null,
unique (company_id, name)
);
grant select, insert, update, delete on table tag to invoicer;
grant select, insert, update, delete on table tag to admin;
grant usage on sequence tag_tag_id_seq to invoicer;
grant usage on sequence tag_tag_id_seq to admin;
alter table tag enable row level security;
create policy company_policy
on tag
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = tag.company_id
)
);
commit;

12
deploy/tag_name.sql Normal file
View File

@ -0,0 +1,12 @@
-- Deploy numerus:tag_name to pg
-- requires: schema_numerus
begin;
set search_path to numerus, public;
-- The same as a topic on gitea
create domain tag_name as text
check ( value ~ '^[a-z0-9][a-z0-9-]{0,34}$' );
commit;

View File

@ -14,6 +14,7 @@ import (
"net/http"
"os"
"os/exec"
"regexp"
"sort"
"strconv"
"strings"
@ -27,6 +28,7 @@ type InvoiceEntry struct {
Total string
CustomerName string
CustomerSlug string
Tags []string
Status string
StatusLabel string
}
@ -39,21 +41,51 @@ type InvoicesIndexPage struct {
func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
tag := r.URL.Query().Get("tag")
page := &InvoicesIndexPage{
Invoices: mustCollectInvoiceEntries(r.Context(), conn, mustGetCompany(r), locale),
Invoices: mustCollectInvoiceEntries(r.Context(), conn, mustGetCompany(r), locale, tag),
InvoiceStatuses: mustCollectInvoiceStatuses(r.Context(), conn, locale),
}
mustRenderAppTemplate(w, r, "invoices/index.gohtml", page)
}
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, 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 desc, invoice_number desc", company.Id, locale.Language.String())
func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale, tag string) []*InvoiceEntry {
rows := conn.MustQuery(ctx, `
select invoice.slug
, invoice_date
, invoice_number
, contact.business_name
, contact.slug
, array_agg(tag.name::text)
, invoice.invoice_status
, isi18n.name
, to_price(total, decimal_digits)
from invoice
left join invoice_tag using (invoice_id)
left join tag using(tag_id)
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 and (($3 = '') or (tag.name = $3))
group by invoice.slug
, invoice_date
, invoice_number
, contact.business_name
, contact.slug
, invoice.invoice_status
, isi18n.name
, total
, decimal_digits
order by invoice_date desc
, invoice_number desc
`, company.Id, locale.Language.String(), tag)
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, &entry.Total); err != nil {
if err := rows.Scan(&entry.Slug, &entry.Date, &entry.Number, &entry.CustomerName, &entry.CustomerSlug, &entry.Tags, &entry.Status, &entry.StatusLabel, &entry.Total); err != nil {
panic(err)
}
entries = append(entries, entry)
@ -330,7 +362,9 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param
mustRenderNewInvoiceForm(w, r, form)
return
}
slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, NewInvoiceProductArray(form.Products))
reg := regexp.MustCompile("[^a-z0-9-]+")
tags := strings.Split(reg.ReplaceAllString(form.Tags.Val, " "), " ")
slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7, $8)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, tags, NewInvoiceProductArray(form.Products))
http.Redirect(w, r, companyURI(company, "/invoices/"+slug), http.StatusSeeOther)
}
@ -421,6 +455,7 @@ type invoiceForm struct {
Date *InputField
Notes *InputField
PaymentMethod *SelectField
Tags *InputField
Products []*invoiceProductForm
}
@ -435,10 +470,9 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Options: MustGetOptions(ctx, conn, "select contact_id::text, business_name from contact where company_id = $1 order by business_name", company.Id),
},
Number: &InputField{
Name: "number",
Label: pgettext("input", "Number", locale),
Type: "text",
Required: false,
Name: "number",
Label: pgettext("input", "Number", locale),
Type: "text",
},
Date: &InputField{
Name: "date",
@ -451,6 +485,11 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Label: pgettext("input", "Notes", locale),
Type: "textarea",
},
Tags: &InputField{
Name: "tags",
Label: pgettext("input", "Tags", locale),
Type: "text",
},
PaymentMethod: &SelectField{
Name: "payment_method",
Required: true,
@ -469,6 +508,7 @@ func (form *invoiceForm) Parse(r *http.Request) error {
form.Number.FillValue(r)
form.Date.FillValue(r)
form.Notes.FillValue(r)
form.Tags.FillValue(r)
form.PaymentMethod.FillValue(r)
if _, ok := r.Form["product.id.0"]; ok {
taxOptions := mustGetTaxOptions(r.Context(), getConn(r), form.company)
@ -541,7 +581,24 @@ func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
var invoiceId int
selectedPaymentMethod := form.PaymentMethod.Selected
form.PaymentMethod.Clear()
if notFoundErrorOrPanic(conn.QueryRow(ctx, "select invoice_id, contact_id, invoice_number, invoice_date, notes, payment_method_id from invoice where slug = $1", slug).Scan(&invoiceId, form.Customer, form.Number, form.Date, form.Notes, form.PaymentMethod)) {
if notFoundErrorOrPanic(conn.QueryRow(ctx, `
select invoice_id
, contact_id
, invoice_number
, invoice_date
, notes
, payment_method_id
, string_agg(tag.name, ', ')
from invoice
left join invoice_tag using (invoice_id)
left join tag using(tag_id) where slug = $1
group by invoice_id
, contact_id
, invoice_number
, invoice_date
, notes
, payment_method_id
`, slug).Scan(&invoiceId, form.Customer, form.Number, form.Date, form.Notes, form.PaymentMethod, form.Tags)) {
form.PaymentMethod.Selected = selectedPaymentMethod
return
}

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-03-09 12:08+0100\n"
"POT-Creation-Date: 2023-03-10 13:59+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"
@ -46,51 +46,51 @@ msgctxt "title"
msgid "New Invoice"
msgstr "Nova factura"
#: web/template/invoices/products.gohtml:41
#: web/template/invoices/products.gohtml:42
#: web/template/products/index.gohtml:21
msgctxt "product"
msgid "All"
msgstr "Tots"
#: web/template/invoices/products.gohtml:42
#: web/template/invoices/products.gohtml:43
#: web/template/products/index.gohtml:22
msgctxt "title"
msgid "Name"
msgstr "Nom"
#: web/template/invoices/products.gohtml:43
#: web/template/invoices/products.gohtml:44
#: web/template/invoices/view.gohtml:54 web/template/products/index.gohtml:23
msgctxt "title"
msgid "Price"
msgstr "Preu"
#: web/template/invoices/products.gohtml:57
#: web/template/invoices/products.gohtml:58
#: web/template/products/index.gohtml:37
msgid "No products added yet."
msgstr "No hi ha cap producte."
#: web/template/invoices/products.gohtml:65 web/template/invoices/new.gohtml:61
#: web/template/invoices/products.gohtml:66 web/template/invoices/new.gohtml:62
msgctxt "action"
msgid "Add products"
msgstr "Afegeix productes"
#: web/template/invoices/new.gohtml:42 web/template/invoices/view.gohtml:59
#: web/template/invoices/new.gohtml:43 web/template/invoices/view.gohtml:59
msgctxt "title"
msgid "Subtotal"
msgstr "Subtotal"
#: web/template/invoices/new.gohtml:52 web/template/invoices/view.gohtml:63
#: web/template/invoices/new.gohtml:53 web/template/invoices/view.gohtml:63
#: web/template/invoices/view.gohtml:103
msgctxt "title"
msgid "Total"
msgstr "Total"
#: web/template/invoices/new.gohtml:64
#: web/template/invoices/new.gohtml:65
msgctxt "action"
msgid "Update"
msgstr "Actualitza"
#: web/template/invoices/new.gohtml:66 web/template/invoices/index.gohtml:19
#: web/template/invoices/new.gohtml:67 web/template/invoices/index.gohtml:19
msgctxt "action"
msgid "New invoice"
msgstr "Nova factura"
@ -127,8 +127,8 @@ msgstr "Estat"
#: web/template/invoices/index.gohtml:33
msgctxt "title"
msgid "Label"
msgstr "Etiqueta"
msgid "Tags"
msgstr "Etiquetes"
#: web/template/invoices/index.gohtml:34
msgctxt "title"
@ -150,12 +150,12 @@ msgctxt "action"
msgid "Select invoice %v"
msgstr "Selecciona factura %v"
#: web/template/invoices/index.gohtml:86 web/template/invoices/view.gohtml:14
#: web/template/invoices/index.gohtml:91 web/template/invoices/view.gohtml:14
msgctxt "action"
msgid "Duplicate"
msgstr "Duplica"
#: web/template/invoices/index.gohtml:96
#: web/template/invoices/index.gohtml:101
msgid "No invoices added yet."
msgstr "No hi ha cap factura."
@ -428,44 +428,44 @@ msgstr "No podeu deixar la contrasenya en blanc."
msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/products.go:165 pkg/invoices.go:578
#: pkg/products.go:165 pkg/invoices.go:635
msgctxt "input"
msgid "Name"
msgstr "Nom"
#: pkg/products.go:171 pkg/invoices.go:583
#: pkg/products.go:171 pkg/invoices.go:640
msgctxt "input"
msgid "Description"
msgstr "Descripció"
#: pkg/products.go:176 pkg/invoices.go:587
#: pkg/products.go:176 pkg/invoices.go:644
msgctxt "input"
msgid "Price"
msgstr "Preu"
#: pkg/products.go:186 pkg/invoices.go:613
#: pkg/products.go:186 pkg/invoices.go:670
msgctxt "input"
msgid "Taxes"
msgstr "Imposts"
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:492
#: pkg/invoices.go:649
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:532
#: pkg/invoices.go:706
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
#: pkg/products.go:207 pkg/invoices.go:650
#: pkg/products.go:207 pkg/invoices.go:707
msgid "Price can not be empty."
msgstr "No podeu deixar el preu en blanc."
#: pkg/products.go:208 pkg/invoices.go:651
#: pkg/products.go:208 pkg/invoices.go:708
msgid "Price must be a number greater than zero."
msgstr "El preu ha de ser un número major a zero."
#: pkg/products.go:210 pkg/invoices.go:659
#: pkg/products.go:210 pkg/invoices.go:716
msgid "Selected tax is not valid."
msgstr "Heu seleccionat un impost que no és vàlid."
#: pkg/products.go:211 pkg/invoices.go:660
#: pkg/products.go:211 pkg/invoices.go:717
msgid "You can only select a tax of each class."
msgstr "Només podeu seleccionar un impost de cada classe."
@ -573,83 +573,88 @@ 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:270
#: pkg/invoices.go:302
msgid "Select a customer to bill."
msgstr "Escolliu un client a facturar."
#: pkg/invoices.go:363 pkg/invoices.go:392
#: pkg/invoices.go:397 pkg/invoices.go:426
msgid "Invalid action"
msgstr "Acció invàlida."
#: pkg/invoices.go:386
#: pkg/invoices.go:420
msgid "invoices.zip"
msgstr "factures.zip"
#: pkg/invoices.go:433
#: pkg/invoices.go:468
msgctxt "input"
msgid "Customer"
msgstr "Client"
#: pkg/invoices.go:439
#: pkg/invoices.go:474
msgctxt "input"
msgid "Number"
msgstr "Número"
#: pkg/invoices.go:445
#: pkg/invoices.go:479
msgctxt "input"
msgid "Invoice Date"
msgstr "Data de factura"
#: pkg/invoices.go:451
#: pkg/invoices.go:485
msgctxt "input"
msgid "Notes"
msgstr "Notes"
#: pkg/invoices.go:457
#: pkg/invoices.go:490
msgctxt "input"
msgid "Tags"
msgstr "Etiquetes"
#: pkg/invoices.go:496
msgctxt "input"
msgid "Payment Method"
msgstr "Mètode de pagament"
#: pkg/invoices.go:493
#: pkg/invoices.go:533
msgid "Invoice date can not be empty."
msgstr "No podeu deixar la data de la factura en blanc."
#: pkg/invoices.go:494
#: pkg/invoices.go:534
msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida."
#: pkg/invoices.go:496
#: pkg/invoices.go:536
msgid "Selected payment method is not valid."
msgstr "Heu seleccionat un mètode de pagament que no és vàlid."
#: pkg/invoices.go:573
#: pkg/invoices.go:630
msgctxt "input"
msgid "Id"
msgstr "Identificador"
#: pkg/invoices.go:596
#: pkg/invoices.go:653
msgctxt "input"
msgid "Quantity"
msgstr "Quantitat"
#: pkg/invoices.go:604
#: pkg/invoices.go:661
msgctxt "input"
msgid "Discount (%)"
msgstr "Descompte (%)"
#: pkg/invoices.go:653
#: pkg/invoices.go:710
msgid "Quantity can not be empty."
msgstr "No podeu deixar la quantitat en blanc."
#: pkg/invoices.go:654
#: pkg/invoices.go:711
msgid "Quantity must be a number greater than zero."
msgstr "La quantitat ha de ser un número major a zero."
#: pkg/invoices.go:656
#: pkg/invoices.go:713
msgid "Discount can not be empty."
msgstr "No podeu deixar el descompte en blanc."
#: pkg/invoices.go:657
#: pkg/invoices.go:714
msgid "Discount must be a percentage between 0 and 100."
msgstr "El descompte ha de ser un percentatge entre 0 i 100."
@ -751,6 +756,10 @@ msgstr "No podeu deixar el codi postal en blanc."
msgid "This value is not a valid postal code."
msgstr "Aquest valor no és un codi postal vàlid."
#~ msgctxt "title"
#~ msgid "Label"
#~ msgstr "Etiqueta"
#~ msgid "Select a tax for this product."
#~ msgstr "Escolliu un impost per aquest producte."

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-03-09 12:08+0100\n"
"POT-Creation-Date: 2023-03-10 13:59+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"
@ -46,51 +46,51 @@ msgctxt "title"
msgid "New Invoice"
msgstr "Nueva factura"
#: web/template/invoices/products.gohtml:41
#: web/template/invoices/products.gohtml:42
#: web/template/products/index.gohtml:21
msgctxt "product"
msgid "All"
msgstr "Todos"
#: web/template/invoices/products.gohtml:42
#: web/template/invoices/products.gohtml:43
#: web/template/products/index.gohtml:22
msgctxt "title"
msgid "Name"
msgstr "Nombre"
#: web/template/invoices/products.gohtml:43
#: web/template/invoices/products.gohtml:44
#: web/template/invoices/view.gohtml:54 web/template/products/index.gohtml:23
msgctxt "title"
msgid "Price"
msgstr "Precio"
#: web/template/invoices/products.gohtml:57
#: web/template/invoices/products.gohtml:58
#: web/template/products/index.gohtml:37
msgid "No products added yet."
msgstr "No hay productos."
#: web/template/invoices/products.gohtml:65 web/template/invoices/new.gohtml:61
#: web/template/invoices/products.gohtml:66 web/template/invoices/new.gohtml:62
msgctxt "action"
msgid "Add products"
msgstr "Añadir productos"
#: web/template/invoices/new.gohtml:42 web/template/invoices/view.gohtml:59
#: web/template/invoices/new.gohtml:43 web/template/invoices/view.gohtml:59
msgctxt "title"
msgid "Subtotal"
msgstr "Subtotal"
#: web/template/invoices/new.gohtml:52 web/template/invoices/view.gohtml:63
#: web/template/invoices/new.gohtml:53 web/template/invoices/view.gohtml:63
#: web/template/invoices/view.gohtml:103
msgctxt "title"
msgid "Total"
msgstr "Total"
#: web/template/invoices/new.gohtml:64
#: web/template/invoices/new.gohtml:65
msgctxt "action"
msgid "Update"
msgstr "Actualizar"
#: web/template/invoices/new.gohtml:66 web/template/invoices/index.gohtml:19
#: web/template/invoices/new.gohtml:67 web/template/invoices/index.gohtml:19
msgctxt "action"
msgid "New invoice"
msgstr "Nueva factura"
@ -127,8 +127,8 @@ msgstr "Estado"
#: web/template/invoices/index.gohtml:33
msgctxt "title"
msgid "Label"
msgstr "Etiqueta"
msgid "Tags"
msgstr "Etiquetes"
#: web/template/invoices/index.gohtml:34
msgctxt "title"
@ -150,12 +150,12 @@ msgctxt "action"
msgid "Select invoice %v"
msgstr "Seleccionar factura %v"
#: web/template/invoices/index.gohtml:86 web/template/invoices/view.gohtml:14
#: web/template/invoices/index.gohtml:91 web/template/invoices/view.gohtml:14
msgctxt "action"
msgid "Duplicate"
msgstr "Duplicar"
#: web/template/invoices/index.gohtml:96
#: web/template/invoices/index.gohtml:101
msgid "No invoices added yet."
msgstr "No hay facturas."
@ -428,44 +428,44 @@ msgstr "No podéis dejar la contraseña en blanco."
msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido."
#: pkg/products.go:165 pkg/invoices.go:578
#: pkg/products.go:165 pkg/invoices.go:635
msgctxt "input"
msgid "Name"
msgstr "Nombre"
#: pkg/products.go:171 pkg/invoices.go:583
#: pkg/products.go:171 pkg/invoices.go:640
msgctxt "input"
msgid "Description"
msgstr "Descripción"
#: pkg/products.go:176 pkg/invoices.go:587
#: pkg/products.go:176 pkg/invoices.go:644
msgctxt "input"
msgid "Price"
msgstr "Precio"
#: pkg/products.go:186 pkg/invoices.go:613
#: pkg/products.go:186 pkg/invoices.go:670
msgctxt "input"
msgid "Taxes"
msgstr "Impuestos"
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:492
#: pkg/invoices.go:649
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:532
#: pkg/invoices.go:706
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
#: pkg/products.go:207 pkg/invoices.go:650
#: pkg/products.go:207 pkg/invoices.go:707
msgid "Price can not be empty."
msgstr "No podéis dejar el precio en blanco."
#: pkg/products.go:208 pkg/invoices.go:651
#: pkg/products.go:208 pkg/invoices.go:708
msgid "Price must be a number greater than zero."
msgstr "El precio tiene que ser un número mayor a cero."
#: pkg/products.go:210 pkg/invoices.go:659
#: pkg/products.go:210 pkg/invoices.go:716
msgid "Selected tax is not valid."
msgstr "Habéis escogido un impuesto que no es válido."
#: pkg/products.go:211 pkg/invoices.go:660
#: pkg/products.go:211 pkg/invoices.go:717
msgid "You can only select a tax of each class."
msgstr "Solo podéis escoger un impuesto de cada clase."
@ -573,83 +573,88 @@ 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:270
#: pkg/invoices.go:302
msgid "Select a customer to bill."
msgstr "Escoged un cliente a facturar."
#: pkg/invoices.go:363 pkg/invoices.go:392
#: pkg/invoices.go:397 pkg/invoices.go:426
msgid "Invalid action"
msgstr "Acción inválida."
#: pkg/invoices.go:386
#: pkg/invoices.go:420
msgid "invoices.zip"
msgstr "facturas.zip"
#: pkg/invoices.go:433
#: pkg/invoices.go:468
msgctxt "input"
msgid "Customer"
msgstr "Cliente"
#: pkg/invoices.go:439
#: pkg/invoices.go:474
msgctxt "input"
msgid "Number"
msgstr "Número"
#: pkg/invoices.go:445
#: pkg/invoices.go:479
msgctxt "input"
msgid "Invoice Date"
msgstr "Fecha de factura"
#: pkg/invoices.go:451
#: pkg/invoices.go:485
msgctxt "input"
msgid "Notes"
msgstr "Notas"
#: pkg/invoices.go:457
#: pkg/invoices.go:490
msgctxt "input"
msgid "Tags"
msgstr "Etiquetes"
#: pkg/invoices.go:496
msgctxt "input"
msgid "Payment Method"
msgstr "Método de pago"
#: pkg/invoices.go:493
#: pkg/invoices.go:533
msgid "Invoice date can not be empty."
msgstr "No podéis dejar la fecha de la factura en blanco."
#: pkg/invoices.go:494
#: pkg/invoices.go:534
msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida."
#: pkg/invoices.go:496
#: pkg/invoices.go:536
msgid "Selected payment method is not valid."
msgstr "Habéis escogido un método de pago que no es válido."
#: pkg/invoices.go:573
#: pkg/invoices.go:630
msgctxt "input"
msgid "Id"
msgstr "Identificador"
#: pkg/invoices.go:596
#: pkg/invoices.go:653
msgctxt "input"
msgid "Quantity"
msgstr "Cantidad"
#: pkg/invoices.go:604
#: pkg/invoices.go:661
msgctxt "input"
msgid "Discount (%)"
msgstr "Descuento (%)"
#: pkg/invoices.go:653
#: pkg/invoices.go:710
msgid "Quantity can not be empty."
msgstr "No podéis dejar la cantidad en blanco."
#: pkg/invoices.go:654
#: pkg/invoices.go:711
msgid "Quantity must be a number greater than zero."
msgstr "La cantidad tiene que ser un número mayor a cero."
#: pkg/invoices.go:656
#: pkg/invoices.go:713
msgid "Discount can not be empty."
msgstr "No podéis dejar el descuento en blanco."
#: pkg/invoices.go:657
#: pkg/invoices.go:714
msgid "Discount must be a percentage between 0 and 100."
msgstr "El descuento tiene que ser un porcentaje entre 0 y 100."
@ -751,6 +756,10 @@ msgstr "No podéis dejar el código postal en blanco."
msgid "This value is not a valid postal code."
msgstr "Este valor no es un código postal válido válido."
#~ msgctxt "title"
#~ msgid "Label"
#~ msgstr "Etiqueta"
#~ msgid "Select a tax for this product."
#~ msgstr "Escoged un impuesto para este producto."

View File

@ -2,6 +2,6 @@
begin;
drop function if exists numerus.add_invoice(integer, text, date, integer, text, integer, numerus.new_invoice_product[]);
drop function if exists numerus.add_invoice(integer, text, date, integer, text, integer, numerus.tag_name[], numerus.new_invoice_product[]);
commit;

7
revert/invoice_tag.sql Normal file
View File

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

7
revert/tag.sql Normal file
View File

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

7
revert/tag_name.sql Normal file
View File

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

View File

@ -58,7 +58,10 @@ invoice_product_tax [schema_numerus invoice_product tax tax_rate] 2023-02-15T13:
new_invoice_product [schema_numerus] 2023-02-16T21:06:01Z jordi fita mas <jordi@tandem.blog> # Add type for passing products to new invoices
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
tag_name [schema_numerus] 2023-03-10T11:06:11Z jordi fita mas <jordi@tandem.blog> # Add domain for tag names
tag [schema_numerus tag_name] 2023-03-10T11:04:24Z jordi fita mas <jordi@tandem.blog> # Add relation for tags
invoice_tag [schema_numerus tag invoice] 2023-03-10T11:37:43Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice tag
add_invoice [schema_numerus invoice company currency parse_price new_invoice_product tax invoice_product invoice_product_tax next_invoice_number tag_name tag invoice_tag] 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_product_amount [schema_numerus invoice_product invoice_product_tax] 2023-03-01T11:18:05Z jordi fita mas <jordi@tandem.blog> # Add view for invoice product subtotal and total
invoice_amount [schema_numerus invoice_product invoice_product_amount] 2023-02-22T12:58:46Z jordi fita mas <jordi@tandem.blog> # Add view to compute subtotal and total for invoices

View File

@ -5,22 +5,24 @@ reset client_min_messages;
begin;
select plan(17);
select plan(19);
set search_path to auth, numerus, public;
select has_function('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]']);
select function_lang_is('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'plpgsql');
select function_returns('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'uuid');
select isnt_definer('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]']);
select volatility_is('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'volatile');
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'authenticator', array []::text[]);
select has_function('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'tag_name[]', 'new_invoice_product[]']);
select function_lang_is('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'tag_name[]', 'new_invoice_product[]'], 'plpgsql');
select function_returns('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'tag_name[]', 'new_invoice_product[]'], 'uuid');
select isnt_definer('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'tag_name[]', 'new_invoice_product[]']);
select volatility_is('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'tag_name[]', 'new_invoice_product[]'], 'volatile');
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'tag_name[]', 'new_invoice_product[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'tag_name[]', 'new_invoice_product[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'tag_name[]', 'new_invoice_product[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'tag_name[]', 'new_invoice_product[]'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate invoice_tag cascade;
truncate tag cascade;
truncate invoice_number_counter cascade;
truncate invoice_product_tax cascade;
truncate invoice_product cascade;
@ -81,27 +83,27 @@ values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '',
select lives_ok(
$$ select add_invoice(1, 'INV001', '2023-02-15', 12, 'Notes 1', 111, '{"(7,Product 1,Description 1,12.24,2,0.0,{4})"}') $$,
$$ select add_invoice(1, 'INV001', '2023-02-15', 12, 'Notes 1', 111, '{tag1,tag2}','{"(7,Product 1,Description 1,12.24,2,0.0,{4})"}') $$,
'Should be able to insert an invoice for the first company with a product'
);
select lives_ok(
$$ select add_invoice(1, 'INV002', '2023-02-16', 13, 'Notes 2', 111, '{"(7,Product 1 bis,Description 1 bis,33.33,1,0.50,\"{4,3}\")","(8,Product 2,Description 2,24.00,3,0.75,{})"}') $$,
$$ select add_invoice(1, 'INV002', '2023-02-16', 13, 'Notes 2', 111, '{}', '{"(7,Product 1 bis,Description 1 bis,33.33,1,0.50,\"{4,3}\")","(8,Product 2,Description 2,24.00,3,0.75,{})"}') $$,
'Should be able to insert a second invoice for the first company with two product'
);
select lives_ok(
$$ select add_invoice(2, 'INV101', '2023-02-14', 15, 'Notes 3', 222, '{"(11,Product 4.3,,11.11,1,0.0,{6})"}') $$,
$$ select add_invoice(2, 'INV101', '2023-02-14', 15, 'Notes 3', 222, '{tag3}','{"(11,Product 4.3,,11.11,1,0.0,{6})"}') $$,
'Should be able to insert an invoice for the second company with a product'
);
select lives_ok(
$$ select add_invoice(1, NULL, '2023-03-15', 13, '', 111, '{"(7,PA1,DA1,44.33,1,0.50,{})"}') $$,
$$ select add_invoice(1, NULL, '2023-03-15', 13, '', 111, '{tag2}', '{"(7,PA1,DA1,44.33,1,0.50,{})"}') $$,
'Should be able to insert an invoice with an autogenerated number'
);
select lives_ok(
$$ select add_invoice(2, ' ', '2023-04-16', 14, '', 222, '{"(11,PA2,DA2,55.33,10,0.75,{})"}') $$,
$$ select add_invoice(2, ' ', '2023-04-16', 14, '', 222, '{tag2,tag3,tag4}','{"(11,PA2,DA2,55.33,10,0.75,{})"}') $$,
'Should consider non-null, but otherwise empty numbers the same as null and autogenerate it'
);
@ -138,6 +140,30 @@ select bag_eq(
'Should have created all invoice product taxes'
);
select bag_eq(
$$ select company_id, name from tag $$,
$$ values (1, 'tag1')
, (1, 'tag2')
, (2, 'tag2')
, (2, 'tag3')
, (2, 'tag4')
$$,
'Should have added all new tags once'
);
select bag_eq(
$$ select invoice_number, tag.name from invoice_tag join invoice using (invoice_id) join tag using (tag_id) $$,
$$ values ('INV001', 'tag1')
, ('INV001', 'tag2')
, ('INV101', 'tag3')
, ('F20230006', 'tag2')
, ('INV056-23', 'tag2')
, ('INV056-23', 'tag3')
, ('INV056-23', 'tag4')
$$,
'Should have assigned the tags to invoices'
);
select *
from finish();

136
test/invoice_tag.sql Normal file
View File

@ -0,0 +1,136 @@
-- Test invoice_tag
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(23);
set search_path to numerus, auth, public;
select has_table('invoice_tag');
select has_pk('invoice_tag' );
select col_is_pk('invoice_tag', array['invoice_id', 'tag_id']);
select table_privs_are('invoice_tag', 'guest', array []::text[]);
select table_privs_are('invoice_tag', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('invoice_tag', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('invoice_tag', 'authenticator', array []::text[]);
select has_column('invoice_tag', 'invoice_id');
select col_is_fk('invoice_tag', 'invoice_id');
select fk_ok('invoice_tag', 'invoice_id', 'invoice', 'invoice_id');
select col_type_is('invoice_tag', 'invoice_id', 'integer');
select col_not_null('invoice_tag', 'invoice_id');
select col_hasnt_default('invoice_tag', 'invoice_id');
select has_column('invoice_tag', 'tag_id');
select col_is_fk('invoice_tag', 'tag_id');
select fk_ok('invoice_tag', 'tag_id', 'tag', 'tag_id');
select col_type_is('invoice_tag', 'tag_id', 'integer');
select col_not_null('invoice_tag', 'tag_id');
select col_hasnt_default('invoice_tag', 'tag_id');
set client_min_messages to warning;
truncate invoice_tag cascade;
truncate invoice cascade;
truncate tag cascade;
truncate contact cascade;
truncate company_user cascade;
truncate company cascade;
truncate payment_method cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
values (6, 2, 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
, (8, 4, 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, contact_id, currency_code, payment_method_id)
values (10, 2, 'INV020001', 6, 'EUR', 222)
, (12, 4, 'INV040001', 8, 'EUR', 444)
;
insert into tag (tag_id, company_id, name)
values (14, 2, 'web')
, (15, 2, 'design')
, (16, 4, 'product')
, (17, 4, 'development')
, (18, 4, 'something-else')
, (19, 4, 'design')
;
insert into invoice_tag (invoice_id, tag_id)
values (10, 14)
, (10, 15)
, (12, 18)
;
prepare invoice_tag_data as
select invoice_id, tag_id
from invoice_tag
;
set role invoicer;
select is_empty('invoice_tag_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'invoice_tag_data',
$$ values ( 10, 14 )
, ( 10, 15 )
$$,
'Should only list invoice tags of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'invoice_tag_data',
$$ values ( 12, 18 )
$$,
'Should only list invoice tags of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'invoice_tag_data',
'42501', 'permission denied for table invoice_tag',
'Should not allow select to guest users'
);
reset role;
select *
from finish();
rollback;

139
test/tag.sql Normal file
View File

@ -0,0 +1,139 @@
-- Test tag
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(33);
set search_path to numerus, auth, public;
select has_table('tag');
select has_pk('tag' );
select table_privs_are('tag', 'guest', array []::text[]);
select table_privs_are('tag', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('tag', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('tag', 'authenticator', array []::text[]);
select has_sequence('tag_tag_id_seq');
select sequence_privs_are('tag_tag_id_seq', 'guest', array[]::text[]);
select sequence_privs_are('tag_tag_id_seq', 'invoicer', array['USAGE']);
select sequence_privs_are('tag_tag_id_seq', 'admin', array['USAGE']);
select sequence_privs_are('tag_tag_id_seq', 'authenticator', array[]::text[]);
select has_column('tag', 'tag_id');
select col_is_pk('tag', 'tag_id');
select col_type_is('tag', 'tag_id', 'integer');
select col_not_null('tag', 'tag_id');
select col_has_default('tag', 'tag_id');
select col_default_is('tag', 'tag_id', 'nextval(''tag_tag_id_seq''::regclass)');
select has_column('tag', 'company_id');
select col_is_fk('tag', 'company_id');
select fk_ok('tag', 'company_id', 'company', 'company_id');
select col_type_is('tag', 'company_id', 'integer');
select col_not_null('tag', 'company_id');
select col_hasnt_default('tag', 'company_id');
select has_column('tag', 'name');
select col_type_is('tag', 'name', 'tag_name');
select col_not_null('tag', 'name');
select col_hasnt_default('tag', 'name');
select col_is_unique('tag', array['company_id', 'name']);
set client_min_messages to warning;
truncate tag cascade;
truncate company_user cascade;
truncate company cascade;
truncate payment_method cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into tag (company_id, name)
values (2, 'web')
, (2, 'design')
, (4, 'product')
, (4, 'development')
, (4, 'something-else')
, (4, 'design')
;
prepare tag_data as
select company_id, name
from tag
;
set role invoicer;
select is_empty('tag_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'tag_data',
$$ values ( 2, 'web' )
, ( 2, 'design' )
$$,
'Should only list tags of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'tag_data',
$$ values (4, 'product' )
, (4, 'development' )
, (4, 'something-else' )
, (4, 'design' )
$$,
'Should only list tags of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'tag_data',
'42501', 'permission denied for table tag',
'Should not allow select to guest users'
);
reset role;
select throws_ok( $$
insert into tag (company_id, name)
values (2, 'web')
$$,
'23505', 'duplicate key value violates unique constraint "tag_company_id_name_key"',
'Should not allow repeated tag names within the same company'
);
select *
from finish();
rollback;

59
test/tag_name.sql Normal file
View File

@ -0,0 +1,59 @@
-- Test tag_name
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, public;
select has_domain('tag_name');
select domain_type_is('tag_name', 'text');
select lives_ok($$ select 'abcdefghijklmnopqrstuvwxyz012345678'::tag_name $$, 'Should be able to cast strings with up to 35 lowercase alphanumeric characters to tag_name');
select lives_ok($$ select 'a'::tag_name $$, 'Should be able to cast strings with a single letter to tag_name');
select lives_ok($$ select '1'::tag_name $$, 'Should be able to cast strings with a single number to tag_name');
select lives_ok($$ select 'a-long-tag'::tag_name $$, 'Should be able to cast strings with dashes to tag_name');
select throws_ok(
$$ SELECT 'abcdefghijklmnopqrstuvwxyz0123456789'::tag_name $$,
23514, null,
'Should reject tag names with more than 35 characters'
);
select throws_ok(
$$ SELECT 'aB'::tag_name $$,
23514, null,
'Should reject tag names with uppercase characters'
);
select throws_ok(
$$ SELECT 'a$'::tag_name $$,
23514, null,
'Should reject tag names with symbols'
);
select throws_ok(
$$ SELECT 'a a'::tag_name $$,
23514, null,
'Should reject tag names with spaces'
);
select throws_ok(
$$ SELECT '-aa'::tag_name $$,
23514, null,
'Should reject tag names starting with a dash'
);
select throws_ok(
$$ SELECT ''::tag_name $$,
23514, null,
'Should reject empty tag names'
);
select *
from finish();
rollback;

View File

@ -2,6 +2,6 @@
begin;
select has_function_privilege('numerus.add_invoice(integer, text, date, integer, text, integer, numerus.new_invoice_product[])', 'execute');
select has_function_privilege('numerus.add_invoice(integer, text, date, integer, text, integer, numerus.tag_name[], numerus.new_invoice_product[])', 'execute');
rollback;

13
verify/invoice_tag.sql Normal file
View File

@ -0,0 +1,13 @@
-- Verify numerus:invoice_tag on pg
begin;
select invoice_id
, tag_id
from numerus.invoice_tag
where false;
select 1 / count(*) from pg_class where oid = 'numerus.invoice_tag'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.invoice_tag'::regclass;
rollback;

14
verify/tag.sql Normal file
View File

@ -0,0 +1,14 @@
-- Verify numerus:tag on pg
begin;
select tag_id
, company_id
, name
from numerus.tag
where false;
select 1 / count(*) from pg_class where oid = 'numerus.tag'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.tag'::regclass;
rollback;

7
verify/tag_name.sql Normal file
View File

@ -0,0 +1,7 @@
-- Verify numerus:tag_name on pg
begin;
select pg_catalog.has_type_privilege('numerus.tag_name', 'usage');
rollback;

View File

@ -30,7 +30,7 @@
<th>{{( pgettext "Invoice Num." "title" )}}</th>
<th>{{( pgettext "Customer" "title" )}}</th>
<th>{{( pgettext "Status" "title" )}}</th>
<th>{{( pgettext "Label" "title" )}}</th>
<th>{{( pgettext "Tags" "title" )}}</th>
<th>{{( pgettext "Amount" "title" )}}</th>
<th>{{( pgettext "Download" "title" )}}</th>
<th>{{( pgettext "Actions" "title" )}}</th>
@ -69,7 +69,12 @@
</form>
</details>
</td>
<td></td>
<td>
{{- range $index, $tag := .Tags }}
{{- if gt $index 0 }}, {{ end -}}
<a href="?tag={{ . }}">{{ . }}</a>
{{- end }}
</td>
<td class="numeric">{{ .Total|formatPrice }}</td>
<td class="invoice-download"><a href="{{ companyURI "/invoices/"}}{{ .Slug }}.pdf"
download="{{ .Number}}.pdf"

View File

@ -20,8 +20,9 @@
{{ template "select-field" .Customer }}
{{ template "input-field" .Number }}
{{ template "input-field" .Date }}
{{ template "input-field" .Notes }}
{{ template "input-field" .Tags }}
{{ template "select-field" .PaymentMethod }}
{{ template "input-field" .Notes }}
{{- range $product := .Products }}
<fieldset class="new-invoice-product">

View File

@ -21,6 +21,7 @@
{{ template "hidden-field" .Number }}
{{ template "hidden-field" .Date }}
{{ template "hidden-field" .Notes }}
{{ template "hidden-field" .Tags }}
{{- range $product := .Products }}
<fieldset>