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, '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]); 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_invoice_id_seq restart;
alter sequence invoice_product_invoice_product_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 - '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, '{"(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 - '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, '{"(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 - '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, '{"(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 - '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, '{"(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 - '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, '{"(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 - '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 = 'paid' where invoice_id in (1, 5);
update invoice set invoice_status = 'unpaid' where invoice_id = 3; update invoice set invoice_status = 'unpaid' where invoice_id = 3;

View File

@ -9,12 +9,15 @@
-- requires: invoice_product -- requires: invoice_product
-- requires: invoice_product_tax -- requires: invoice_product_tax
-- requires: next_invoice_number -- requires: next_invoice_number
-- requires: tag_name
-- requires: tag
-- requires: invoice_tag
begin; begin;
set search_path to numerus, public; 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 declare
iid integer; iid integer;
@ -24,11 +27,11 @@ declare
ipid integer; ipid integer;
begin begin
if invoice_number is null or length(trim(invoice_number)) = 0 then 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; end if;
insert into invoice (company_id, invoice_number, invoice_date, contact_id, notes, currency_code, payment_method_id) 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_number
, invoice_date , invoice_date
, contact_id , contact_id
@ -36,7 +39,7 @@ begin
, currency_code , currency_code
, add_invoice.payment_method_id , add_invoice.payment_method_id
from company from company
where company.company_id = add_invoice.company_id where company.company_id = add_invoice.company
returning invoice_id, slug, currency_code returning invoice_id, slug, currency_code
into iid, pslug, ccode; into iid, pslug, ccode;
@ -61,13 +64,27 @@ begin
join unnest(product.tax) as ptax(tax_id) using (tax_id); join unnest(product.tax) as ptax(tax_id) using (tax_id);
end loop; 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; return pslug;
end; end;
$$ $$
language plpgsql; language plpgsql;
revoke execute on function add_invoice(integer, text, date, integer, text, integer, new_invoice_product[]) from public; 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, new_invoice_product[]) to invoicer; 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, new_invoice_product[]) to admin; grant execute on function add_invoice(integer, text, date, integer, text, integer, tag_name[], new_invoice_product[]) to admin;
commit; 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" "net/http"
"os" "os"
"os/exec" "os/exec"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -27,6 +28,7 @@ type InvoiceEntry struct {
Total string Total string
CustomerName string CustomerName string
CustomerSlug string CustomerSlug string
Tags []string
Status string Status string
StatusLabel string StatusLabel string
} }
@ -39,21 +41,51 @@ type InvoicesIndexPage struct {
func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r) conn := getConn(r)
locale := getLocale(r) locale := getLocale(r)
tag := r.URL.Query().Get("tag")
page := &InvoicesIndexPage{ 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), InvoiceStatuses: mustCollectInvoiceStatuses(r.Context(), conn, locale),
} }
mustRenderAppTemplate(w, r, "invoices/index.gohtml", page) mustRenderAppTemplate(w, r, "invoices/index.gohtml", page)
} }
func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale) []*InvoiceEntry { 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, 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()) 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() 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, &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) panic(err)
} }
entries = append(entries, entry) entries = append(entries, entry)
@ -330,7 +362,9 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param
mustRenderNewInvoiceForm(w, r, form) mustRenderNewInvoiceForm(w, r, form)
return 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) http.Redirect(w, r, companyURI(company, "/invoices/"+slug), http.StatusSeeOther)
} }
@ -421,6 +455,7 @@ type invoiceForm struct {
Date *InputField Date *InputField
Notes *InputField Notes *InputField
PaymentMethod *SelectField PaymentMethod *SelectField
Tags *InputField
Products []*invoiceProductForm 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), Options: MustGetOptions(ctx, conn, "select contact_id::text, business_name from contact where company_id = $1 order by business_name", company.Id),
}, },
Number: &InputField{ Number: &InputField{
Name: "number", Name: "number",
Label: pgettext("input", "Number", locale), Label: pgettext("input", "Number", locale),
Type: "text", Type: "text",
Required: false,
}, },
Date: &InputField{ Date: &InputField{
Name: "date", Name: "date",
@ -451,6 +485,11 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Label: pgettext("input", "Notes", locale), Label: pgettext("input", "Notes", locale),
Type: "textarea", Type: "textarea",
}, },
Tags: &InputField{
Name: "tags",
Label: pgettext("input", "Tags", locale),
Type: "text",
},
PaymentMethod: &SelectField{ PaymentMethod: &SelectField{
Name: "payment_method", Name: "payment_method",
Required: true, Required: true,
@ -469,6 +508,7 @@ func (form *invoiceForm) Parse(r *http.Request) error {
form.Number.FillValue(r) form.Number.FillValue(r)
form.Date.FillValue(r) form.Date.FillValue(r)
form.Notes.FillValue(r) form.Notes.FillValue(r)
form.Tags.FillValue(r)
form.PaymentMethod.FillValue(r) form.PaymentMethod.FillValue(r)
if _, ok := r.Form["product.id.0"]; ok { if _, ok := r.Form["product.id.0"]; ok {
taxOptions := mustGetTaxOptions(r.Context(), getConn(r), form.company) 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 var invoiceId int
selectedPaymentMethod := form.PaymentMethod.Selected selectedPaymentMethod := form.PaymentMethod.Selected
form.PaymentMethod.Clear() 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 form.PaymentMethod.Selected = selectedPaymentMethod
return return
} }

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-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" "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"
@ -46,51 +46,51 @@ msgctxt "title"
msgid "New Invoice" msgid "New Invoice"
msgstr "Nova factura" msgstr "Nova factura"
#: web/template/invoices/products.gohtml:41 #: web/template/invoices/products.gohtml:42
#: web/template/products/index.gohtml:21 #: web/template/products/index.gohtml:21
msgctxt "product" msgctxt "product"
msgid "All" msgid "All"
msgstr "Tots" msgstr "Tots"
#: web/template/invoices/products.gohtml:42 #: web/template/invoices/products.gohtml:43
#: web/template/products/index.gohtml:22 #: web/template/products/index.gohtml:22
msgctxt "title" msgctxt "title"
msgid "Name" msgid "Name"
msgstr "Nom" 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 #: web/template/invoices/view.gohtml:54 web/template/products/index.gohtml:23
msgctxt "title" msgctxt "title"
msgid "Price" msgid "Price"
msgstr "Preu" msgstr "Preu"
#: web/template/invoices/products.gohtml:57 #: web/template/invoices/products.gohtml:58
#: web/template/products/index.gohtml:37 #: web/template/products/index.gohtml:37
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:65 web/template/invoices/new.gohtml:61 #: web/template/invoices/products.gohtml:66 web/template/invoices/new.gohtml:62
msgctxt "action" msgctxt "action"
msgid "Add products" msgid "Add products"
msgstr "Afegeix productes" 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" msgctxt "title"
msgid "Subtotal" msgid "Subtotal"
msgstr "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 #: web/template/invoices/view.gohtml:103
msgctxt "title" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
#: web/template/invoices/new.gohtml:64 #: web/template/invoices/new.gohtml:65
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
msgstr "Actualitza" 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" msgctxt "action"
msgid "New invoice" msgid "New invoice"
msgstr "Nova factura" msgstr "Nova factura"
@ -127,8 +127,8 @@ msgstr "Estat"
#: web/template/invoices/index.gohtml:33 #: web/template/invoices/index.gohtml:33
msgctxt "title" msgctxt "title"
msgid "Label" msgid "Tags"
msgstr "Etiqueta" msgstr "Etiquetes"
#: web/template/invoices/index.gohtml:34 #: web/template/invoices/index.gohtml:34
msgctxt "title" msgctxt "title"
@ -150,12 +150,12 @@ msgctxt "action"
msgid "Select invoice %v" msgid "Select invoice %v"
msgstr "Selecciona factura %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" msgctxt "action"
msgid "Duplicate" msgid "Duplicate"
msgstr "Duplica" msgstr "Duplica"
#: web/template/invoices/index.gohtml:96 #: web/template/invoices/index.gohtml:101
msgid "No invoices added yet." msgid "No invoices added yet."
msgstr "No hi ha cap factura." msgstr "No hi ha cap factura."
@ -428,44 +428,44 @@ 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:165 pkg/invoices.go:578 #: pkg/products.go:165 pkg/invoices.go:635
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: pkg/products.go:171 pkg/invoices.go:583 #: pkg/products.go:171 pkg/invoices.go:640
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: pkg/products.go:176 pkg/invoices.go:587 #: pkg/products.go:176 pkg/invoices.go:644
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Preu" msgstr "Preu"
#: pkg/products.go:186 pkg/invoices.go:613 #: pkg/products.go:186 pkg/invoices.go:670
msgctxt "input" msgctxt "input"
msgid "Taxes" msgid "Taxes"
msgstr "Imposts" msgstr "Imposts"
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:492 #: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:532
#: pkg/invoices.go:649 #: pkg/invoices.go:706
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:207 pkg/invoices.go:650 #: pkg/products.go:207 pkg/invoices.go:707
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:208 pkg/invoices.go:651 #: pkg/products.go:208 pkg/invoices.go:708
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:210 pkg/invoices.go:659 #: pkg/products.go:210 pkg/invoices.go:716
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."
#: 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." msgid "You can only select a tax of each class."
msgstr "Només podeu seleccionar un impost de cada classe." 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." 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:270 #: pkg/invoices.go:302
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:363 pkg/invoices.go:392 #: pkg/invoices.go:397 pkg/invoices.go:426
msgid "Invalid action" msgid "Invalid action"
msgstr "Acció invàlida." msgstr "Acció invàlida."
#: pkg/invoices.go:386 #: pkg/invoices.go:420
msgid "invoices.zip" msgid "invoices.zip"
msgstr "factures.zip" msgstr "factures.zip"
#: pkg/invoices.go:433 #: pkg/invoices.go:468
msgctxt "input" msgctxt "input"
msgid "Customer" msgid "Customer"
msgstr "Client" msgstr "Client"
#: pkg/invoices.go:439 #: pkg/invoices.go:474
msgctxt "input" msgctxt "input"
msgid "Number" msgid "Number"
msgstr "Número" msgstr "Número"
#: pkg/invoices.go:445 #: pkg/invoices.go:479
msgctxt "input" msgctxt "input"
msgid "Invoice Date" msgid "Invoice Date"
msgstr "Data de factura" msgstr "Data de factura"
#: pkg/invoices.go:451 #: pkg/invoices.go:485
msgctxt "input" msgctxt "input"
msgid "Notes" msgid "Notes"
msgstr "Notes" msgstr "Notes"
#: pkg/invoices.go:457 #: pkg/invoices.go:490
msgctxt "input"
msgid "Tags"
msgstr "Etiquetes"
#: pkg/invoices.go:496
msgctxt "input" msgctxt "input"
msgid "Payment Method" msgid "Payment Method"
msgstr "Mètode de pagament" msgstr "Mètode de pagament"
#: pkg/invoices.go:493 #: pkg/invoices.go:533
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:494 #: pkg/invoices.go:534
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:496 #: pkg/invoices.go:536
msgid "Selected payment method is not valid." msgid "Selected payment method is not valid."
msgstr "Heu seleccionat un mètode de pagament que no és vàlid." msgstr "Heu seleccionat un mètode de pagament que no és vàlid."
#: pkg/invoices.go:573 #: pkg/invoices.go:630
msgctxt "input" msgctxt "input"
msgid "Id" msgid "Id"
msgstr "Identificador" msgstr "Identificador"
#: pkg/invoices.go:596 #: pkg/invoices.go:653
msgctxt "input" msgctxt "input"
msgid "Quantity" msgid "Quantity"
msgstr "Quantitat" msgstr "Quantitat"
#: pkg/invoices.go:604 #: pkg/invoices.go:661
msgctxt "input" msgctxt "input"
msgid "Discount (%)" msgid "Discount (%)"
msgstr "Descompte (%)" msgstr "Descompte (%)"
#: pkg/invoices.go:653 #: pkg/invoices.go:710
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:654 #: pkg/invoices.go:711
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:656 #: pkg/invoices.go:713
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:657 #: pkg/invoices.go:714
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."
@ -751,6 +756,10 @@ msgstr "No podeu deixar el codi postal en blanc."
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."
#~ msgctxt "title"
#~ msgid "Label"
#~ msgstr "Etiqueta"
#~ msgid "Select a tax for this product." #~ msgid "Select a tax for this product."
#~ msgstr "Escolliu un impost per aquest producte." #~ msgstr "Escolliu un impost per aquest producte."

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-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" "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"
@ -46,51 +46,51 @@ msgctxt "title"
msgid "New Invoice" msgid "New Invoice"
msgstr "Nueva factura" msgstr "Nueva factura"
#: web/template/invoices/products.gohtml:41 #: web/template/invoices/products.gohtml:42
#: web/template/products/index.gohtml:21 #: web/template/products/index.gohtml:21
msgctxt "product" msgctxt "product"
msgid "All" msgid "All"
msgstr "Todos" msgstr "Todos"
#: web/template/invoices/products.gohtml:42 #: web/template/invoices/products.gohtml:43
#: web/template/products/index.gohtml:22 #: web/template/products/index.gohtml:22
msgctxt "title" msgctxt "title"
msgid "Name" msgid "Name"
msgstr "Nombre" 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 #: web/template/invoices/view.gohtml:54 web/template/products/index.gohtml:23
msgctxt "title" msgctxt "title"
msgid "Price" msgid "Price"
msgstr "Precio" msgstr "Precio"
#: web/template/invoices/products.gohtml:57 #: web/template/invoices/products.gohtml:58
#: web/template/products/index.gohtml:37 #: web/template/products/index.gohtml:37
msgid "No products added yet." msgid "No products added yet."
msgstr "No hay productos." 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" msgctxt "action"
msgid "Add products" msgid "Add products"
msgstr "Añadir productos" 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" msgctxt "title"
msgid "Subtotal" msgid "Subtotal"
msgstr "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 #: web/template/invoices/view.gohtml:103
msgctxt "title" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
#: web/template/invoices/new.gohtml:64 #: web/template/invoices/new.gohtml:65
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
msgstr "Actualizar" 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" msgctxt "action"
msgid "New invoice" msgid "New invoice"
msgstr "Nueva factura" msgstr "Nueva factura"
@ -127,8 +127,8 @@ msgstr "Estado"
#: web/template/invoices/index.gohtml:33 #: web/template/invoices/index.gohtml:33
msgctxt "title" msgctxt "title"
msgid "Label" msgid "Tags"
msgstr "Etiqueta" msgstr "Etiquetes"
#: web/template/invoices/index.gohtml:34 #: web/template/invoices/index.gohtml:34
msgctxt "title" msgctxt "title"
@ -150,12 +150,12 @@ msgctxt "action"
msgid "Select invoice %v" msgid "Select invoice %v"
msgstr "Seleccionar factura %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" msgctxt "action"
msgid "Duplicate" msgid "Duplicate"
msgstr "Duplicar" msgstr "Duplicar"
#: web/template/invoices/index.gohtml:96 #: web/template/invoices/index.gohtml:101
msgid "No invoices added yet." msgid "No invoices added yet."
msgstr "No hay facturas." msgstr "No hay facturas."
@ -428,44 +428,44 @@ 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:165 pkg/invoices.go:578 #: pkg/products.go:165 pkg/invoices.go:635
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: pkg/products.go:171 pkg/invoices.go:583 #: pkg/products.go:171 pkg/invoices.go:640
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripción" msgstr "Descripción"
#: pkg/products.go:176 pkg/invoices.go:587 #: pkg/products.go:176 pkg/invoices.go:644
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Precio" msgstr "Precio"
#: pkg/products.go:186 pkg/invoices.go:613 #: pkg/products.go:186 pkg/invoices.go:670
msgctxt "input" msgctxt "input"
msgid "Taxes" msgid "Taxes"
msgstr "Impuestos" msgstr "Impuestos"
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:492 #: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:532
#: pkg/invoices.go:649 #: pkg/invoices.go:706
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:207 pkg/invoices.go:650 #: pkg/products.go:207 pkg/invoices.go:707
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:208 pkg/invoices.go:651 #: pkg/products.go:208 pkg/invoices.go:708
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:210 pkg/invoices.go:659 #: pkg/products.go:210 pkg/invoices.go:716
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."
#: 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." msgid "You can only select a tax of each class."
msgstr "Solo podéis escoger un impuesto de cada clase." 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." 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:270 #: pkg/invoices.go:302
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:363 pkg/invoices.go:392 #: pkg/invoices.go:397 pkg/invoices.go:426
msgid "Invalid action" msgid "Invalid action"
msgstr "Acción inválida." msgstr "Acción inválida."
#: pkg/invoices.go:386 #: pkg/invoices.go:420
msgid "invoices.zip" msgid "invoices.zip"
msgstr "facturas.zip" msgstr "facturas.zip"
#: pkg/invoices.go:433 #: pkg/invoices.go:468
msgctxt "input" msgctxt "input"
msgid "Customer" msgid "Customer"
msgstr "Cliente" msgstr "Cliente"
#: pkg/invoices.go:439 #: pkg/invoices.go:474
msgctxt "input" msgctxt "input"
msgid "Number" msgid "Number"
msgstr "Número" msgstr "Número"
#: pkg/invoices.go:445 #: pkg/invoices.go:479
msgctxt "input" msgctxt "input"
msgid "Invoice Date" msgid "Invoice Date"
msgstr "Fecha de factura" msgstr "Fecha de factura"
#: pkg/invoices.go:451 #: pkg/invoices.go:485
msgctxt "input" msgctxt "input"
msgid "Notes" msgid "Notes"
msgstr "Notas" msgstr "Notas"
#: pkg/invoices.go:457 #: pkg/invoices.go:490
msgctxt "input"
msgid "Tags"
msgstr "Etiquetes"
#: pkg/invoices.go:496
msgctxt "input" msgctxt "input"
msgid "Payment Method" msgid "Payment Method"
msgstr "Método de pago" msgstr "Método de pago"
#: pkg/invoices.go:493 #: pkg/invoices.go:533
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:494 #: pkg/invoices.go:534
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:496 #: pkg/invoices.go:536
msgid "Selected payment method is not valid." msgid "Selected payment method is not valid."
msgstr "Habéis escogido un método de pago que no es válido." msgstr "Habéis escogido un método de pago que no es válido."
#: pkg/invoices.go:573 #: pkg/invoices.go:630
msgctxt "input" msgctxt "input"
msgid "Id" msgid "Id"
msgstr "Identificador" msgstr "Identificador"
#: pkg/invoices.go:596 #: pkg/invoices.go:653
msgctxt "input" msgctxt "input"
msgid "Quantity" msgid "Quantity"
msgstr "Cantidad" msgstr "Cantidad"
#: pkg/invoices.go:604 #: pkg/invoices.go:661
msgctxt "input" msgctxt "input"
msgid "Discount (%)" msgid "Discount (%)"
msgstr "Descuento (%)" msgstr "Descuento (%)"
#: pkg/invoices.go:653 #: pkg/invoices.go:710
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:654 #: pkg/invoices.go:711
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:656 #: pkg/invoices.go:713
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:657 #: pkg/invoices.go:714
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 porcentaje entre 0 y 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." 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."
#~ msgctxt "title"
#~ msgid "Label"
#~ msgstr "Etiqueta"
#~ msgid "Select a tax for this product." #~ msgid "Select a tax for this product."
#~ msgstr "Escoged un impuesto para este producto." #~ msgstr "Escoged un impuesto para este producto."

View File

@ -2,6 +2,6 @@
begin; 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; 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 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 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 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_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_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 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; begin;
select plan(17); select plan(19);
set search_path to auth, numerus, public; set search_path to auth, numerus, public;
select has_function('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]']); 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', 'new_invoice_product[]'], 'plpgsql'); 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', 'new_invoice_product[]'], 'uuid'); 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', 'new_invoice_product[]']); 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', 'new_invoice_product[]'], 'volatile'); 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', '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[]'], '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', 'tag_name[]', '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', 'tag_name[]', '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 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; set client_min_messages to warning;
truncate invoice_tag cascade;
truncate tag cascade;
truncate invoice_number_counter cascade; truncate invoice_number_counter cascade;
truncate invoice_product_tax cascade; truncate invoice_product_tax cascade;
truncate invoice_product 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 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' 'Should be able to insert an invoice for the first company with a product'
); );
select lives_ok( 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' 'Should be able to insert a second invoice for the first company with two product'
); );
select lives_ok( 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' 'Should be able to insert an invoice for the second company with a product'
); );
select lives_ok( 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' 'Should be able to insert an invoice with an autogenerated number'
); );
select lives_ok( 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' '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' '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 * select *
from finish(); 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; 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; 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 "Invoice Num." "title" )}}</th>
<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 "Tags" "title" )}}</th>
<th>{{( pgettext "Amount" "title" )}}</th> <th>{{( pgettext "Amount" "title" )}}</th>
<th>{{( pgettext "Download" "title" )}}</th> <th>{{( pgettext "Download" "title" )}}</th>
<th>{{( pgettext "Actions" "title" )}}</th> <th>{{( pgettext "Actions" "title" )}}</th>
@ -69,7 +69,12 @@
</form> </form>
</details> </details>
</td> </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="numeric">{{ .Total|formatPrice }}</td>
<td class="invoice-download"><a href="{{ companyURI "/invoices/"}}{{ .Slug }}.pdf" <td class="invoice-download"><a href="{{ companyURI "/invoices/"}}{{ .Slug }}.pdf"
download="{{ .Number}}.pdf" download="{{ .Number}}.pdf"

View File

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

View File

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