Compare commits

...

4 Commits

Author SHA1 Message Date
jordi fita mas 2a98b9c0af Restart sequences for the demo
I was using explicit IDs because i need them to satisfy foreign key
constraints, and also to look them up within the file, but then i had
the problem that the sequences would be left at 1, preventing me to
add new contacts or products, for instance.

Now i use the sequence exactly how the application will (i.e., with
default values), but i have to reset them to 1 to make the ID stable
even when i make tests with pgTAP on the same database.
2023-02-07 16:59:00 +01:00
jordi fita mas 44e8f030b3 Add the invoice_status relation and its i18n 2023-02-07 16:45:27 +01:00
jordi fita mas 608ea16152 Document the requirement between product and tax 2023-02-07 15:34:41 +01:00
jordi fita mas 73ca559209 Add template for InputField of type textarea 2023-02-07 15:28:22 +01:00
17 changed files with 261 additions and 33 deletions

View File

@ -2,43 +2,48 @@ begin;
set search_path to auth, numerus, public; set search_path to auth, numerus, public;
insert into auth."user" (user_id, email, name, password, role) alter sequence user_user_id_seq restart;
values (1, 'demo@numerus', 'Demo User', 'demo', 'invoicer') insert into auth."user" (email, name, password, role)
, (2, 'admin@numerus', 'Demo Admin', 'admin', 'admin') values ('demo@numerus', 'Demo User', 'demo', 'invoicer')
, ('admin@numerus', 'Demo Admin', 'admin', 'admin')
; ;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code) alter sequence company_company_id_seq restart;
values (1, 'Juli Verd', 'ES40404040D', 'Pesebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de lHort', 'Castelló dEmpúries', 'Girona', '17486', 'ES', 'EUR'); insert into company (business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code)
values ('Juli Verd', 'ES40404040D', 'Pesebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de lHort', 'Castelló dEmpúries', 'Girona', '17486', 'ES', 'EUR');
insert into company_user (company_id, user_id) insert into company_user (company_id, user_id)
values (1, 1) values (1, 1)
, (1, 2) , (1, 2)
; ;
insert into tax (tax_id, company_id, name, rate) alter sequence tax_tax_id_seq restart;
values (1, 1, 'Retenció 15 %', -0.15) insert into tax (company_id, name, rate)
, (2, 1, 'IVA 21 %', 0.21) values (1, 'Retenció 15 %', -0.15)
, (3, 1, 'IVA 10 %', 0.10) , (1, 'IVA 21 %', 0.21)
, (4, 1, 'IVA 4 %', 0.04) , (1, 'IVA 10 %', 0.10)
, (1, 'IVA 4 %', 0.04)
; ;
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) alter sequence contact_contact_id_seq restart;
values (1, 1, 'Melcior', 'IR1', 'Rei Blanc', parse_packed_phone_number('0732621', 'IR'), 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR') insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
, (2, 1, 'Gaspar', 'IN2', 'Rei Ros', parse_packed_phone_number('111', 'IN'), 'gaspar@reismags.cat', '', 'C/ Principal, 2', 'Nova Delhi', 'Delhi', '2', 'IN') values (1, 'Melcior', 'IR1', 'Rei Blanc', parse_packed_phone_number('0732621', 'IR'), 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR')
, (3, 1, 'Baltasar', 'YE3', 'Rei Negre', parse_packed_phone_number('1-111-111', 'YE'), 'baltasar@reismags.cat', '', 'C/ Principal, 3', 'Sanaa', 'Sanaa', '3', 'YE') , (1, 'Gaspar', 'IN2', 'Rei Ros', parse_packed_phone_number('111', 'IN'), 'gaspar@reismags.cat', '', 'C/ Principal, 2', 'Nova Delhi', 'Delhi', '2', 'IN')
, (4, 1, 'Caganera', 'ES41414141L', '', parse_packed_phone_number('222 222 222', 'ES'), 'caganera@pesebre.cat', '', 'C/ De lHort, 4', 'Olot', 'Girona', '17800', 'ES') , (1, 'Baltasar', 'YE3', 'Rei Negre', parse_packed_phone_number('1-111-111', 'YE'), 'baltasar@reismags.cat', '', 'C/ Principal, 3', 'Sanaa', 'Sanaa', '3', 'YE')
, (5, 1, 'Bou', 'ES41414142C', '', parse_packed_phone_number('333 333 333', 'ES'), 'bou@pesebre.cat', '', 'C/ De la Palla, 5', 'Sant Climent Sescebes', 'Girona', '17751', 'ES') , (1, 'Caganera', 'ES41414141L', '', parse_packed_phone_number('222 222 222', 'ES'), 'caganera@pesebre.cat', '', 'C/ De lHort, 4', 'Olot', 'Girona', '17800', 'ES')
, (6, 1, 'Rabadà', 'ES41414143K', '', parse_packed_phone_number('444 444 444', 'ES'), 'rabada@pesebre.cat', '', 'C/ De les Ovelles, 6', 'Fornells de la Selva', 'Girona', '17458', 'ES') , (1, 'Bou', 'ES41414142C', '', parse_packed_phone_number('333 333 333', 'ES'), 'bou@pesebre.cat', '', 'C/ De la Palla, 5', 'Sant Climent Sescebes', 'Girona', '17751', 'ES')
, (1, 'Rabadà', 'ES41414143K', '', parse_packed_phone_number('444 444 444', 'ES'), 'rabada@pesebre.cat', '', 'C/ De les Ovelles, 6', 'Fornells de la Selva', 'Girona', '17458', 'ES')
; ;
insert into product(product_id, company_id, name, description, price, tax_id) alter sequence product_product_id_seq restart;
values (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.', 5592, 2) insert into product(company_id, name, description, price, tax_id)
, (2, 1, 'Encens', 'Goma resina fragrant que desprèn una olor característica quan es crema.', 215, 2) values (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.', 5592, 2)
, (3, 1, 'Mirra', 'Goma resinosa aromàtica de color gris groguenc i gust amargant.', 690, 2) , (1, 'Encens', 'Goma resina fragrant que desprèn una olor característica quan es crema.', 215, 2)
, (4, 1, 'Paper higiènic (pack de 32 U)', 'Paper que susa per mantenir la higiene personal després de defecar o orinar.', 799, 4) , (1, 'Mirra', 'Goma resinosa aromàtica de color gris groguenc i gust amargant.', 690, 2)
, (5, 1, 'Cavall Fort', 'Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.', 364, 2) , (1, 'Paper higiènic (pack de 32 U)', 'Paper que susa per mantenir la higiene personal després de defecar o orinar.', 799, 4)
, (6, 1, 'Palla', 'Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.', 2500, 3) , (1, 'Cavall Fort', 'Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.', 364, 2)
, (7, 1, 'Teia', 'Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.', 700, 2) , (1, 'Palla', 'Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.', 2500, 3)
, (1, 'Teia', 'Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.', 700, 2)
; ;
commit; commit;

View File

@ -0,0 +1,28 @@
-- Deploy numerus:available_invoice_status to pg
-- requires: schema_numerus
-- requires: invoice_status
-- requires: invoice_status_i18n
begin;
set search_path to numerus;
insert into invoice_status (invoice_status, name)
values ('created', 'Created')
, ('sent', 'Sent')
, ('paid', 'Paid')
, ('unpaid', 'Unpaid')
;
insert into invoice_status_i18n (invoice_status, lang_tag, name)
values ('created', 'ca', 'Creada')
, ('sent', 'ca', 'Enviada')
, ('paid', 'ca', 'Cobrada')
, ('unpaid', 'ca', 'No cobrada')
, ('created', 'es', 'Creada')
, ('sent', 'es', 'Enviada')
, ('paid', 'es', 'Cobrada')
, ('unpaid', 'es', 'No cobrada')
;
commit;

16
deploy/invoice_status.sql Normal file
View File

@ -0,0 +1,16 @@
-- Deploy numerus:invoice_status to pg
-- requires: schema_numerus
begin;
set search_path to numerus, public;
create table invoice_status (
invoice_status text primary key,
name text not null
);
grant select on table invoice_status to invoicer;
grant select on table invoice_status to admin;
commit;

View File

@ -0,0 +1,20 @@
-- Deploy numerus:invoice_status_i18n to pg
-- requires: schema_numerus
-- requires: invoice_status
-- requires: language
begin;
set search_path to numerus, public;
create table invoice_status_i18n (
invoice_status text not null references invoice_status,
lang_tag text not null references language,
name text not null,
primary key (invoice_status, lang_tag)
);
grant select on table invoice_status_i18n to invoicer;
grant select on table invoice_status_i18n to admin;
commit;

View File

@ -1,6 +1,7 @@
-- Deploy numerus:product to pg -- Deploy numerus:product to pg
-- requires: schema_numerus -- requires: schema_numerus
-- requires: company -- requires: company
-- requires: tax
begin; begin;

View File

@ -152,7 +152,7 @@ func newProductForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Description: &InputField{ Description: &InputField{
Name: "description", Name: "description",
Label: pgettext("input", "Description", locale), Label: pgettext("input", "Description", locale),
Type: "text", Type: "textarea",
}, },
Price: &InputField{ Price: &InputField{
Name: "price", Name: "price",

View File

@ -0,0 +1,10 @@
-- Revert numerus:available_invoice_status from pg
begin;
set search_path to numerus;
delete from invoice_status_i18n;
delete from invoice_status;
commit;

View File

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

View File

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

View File

@ -33,12 +33,15 @@ available_currencies [schema_numerus currency] 2023-01-24T14:54:18Z jordi fita m
country_code [schema_numerus] 2023-01-27T18:33:26Z jordi fita mas <jordi@tandem.blog> # Add domain for country codes country_code [schema_numerus] 2023-01-27T18:33:26Z jordi fita mas <jordi@tandem.blog> # Add domain for country codes
country [schema_numerus country_code] 2023-01-27T18:39:44Z jordi fita mas <jordi@tandem.blog> # Add the relation for countries country [schema_numerus country_code] 2023-01-27T18:39:44Z jordi fita mas <jordi@tandem.blog> # Add the relation for countries
country_i18n [schema_numerus country_code language country] 2023-01-27T19:20:43Z jordi fita mas <jordi@tandem.blog> # Add table for localization of country names country_i18n [schema_numerus country_code language country] 2023-01-27T19:20:43Z jordi fita mas <jordi@tandem.blog> # Add table for localization of country names
available_countries [schema_numerus country] 2023-01-27T18:49:28Z jordi fita mas <jordi@tandem.blog> # Add the list of available countries available_countries [schema_numerus country country_i18n] 2023-01-27T18:49:28Z jordi fita mas <jordi@tandem.blog> # Add the list of available countries
company [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-24T15:03:15Z jordi fita mas <jordi@tandem.blog> # Add the relation for companies company [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-24T15:03:15Z jordi fita mas <jordi@tandem.blog> # Add the relation for companies
company_user [schema_numerus user company] 2023-01-24T17:50:06Z jordi fita mas <jordi@tandem.blog> # Add the relation of companies and their users company_user [schema_numerus user company] 2023-01-24T17:50:06Z jordi fita mas <jordi@tandem.blog> # Add the relation of companies and their users
tax_rate [schema_numerus] 2023-01-28T11:33:39Z jordi fita mas <jordi@tandem.blog> # Add domain for tax rates tax_rate [schema_numerus] 2023-01-28T11:33:39Z jordi fita mas <jordi@tandem.blog> # Add domain for tax rates
tax [schema_numerus company tax_rate] 2023-01-28T11:45:47Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes tax [schema_numerus company tax_rate] 2023-01-28T11:45:47Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes
contact [schema_numerus company extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-29T12:59:18Z jordi fita mas <jordi@tandem.blog> # Add the relation for contacts contact [schema_numerus company extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-29T12:59:18Z jordi fita mas <jordi@tandem.blog> # Add the relation for contacts
product [schema_numerus company] 2023-02-04T09:17:24Z jordi fita mas <jordi@tandem.blog> # Add relation for products product [schema_numerus company tax] 2023-02-04T09:17:24Z jordi fita mas <jordi@tandem.blog> # Add relation for products
parse_price [schema_public] 2023-02-05T11:04:54Z jordi fita mas <jordi@tandem.blog> # Add function to convert from price to cents parse_price [schema_public] 2023-02-05T11:04:54Z jordi fita mas <jordi@tandem.blog> # Add function to convert from price to cents
to_price [schema_numerus] 2023-02-05T11:46:31Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices to_price [schema_numerus] 2023-02-05T11:46:31Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices
invoice_status [schema_numerus] 2023-02-07T14:50:26Z jordi fita mas <jordi@tandem.blog> # A relation of invoice status
invoice_status_i18n [schema_numerus invoice_status language] 2023-02-07T14:56:18Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice status translatable texts
available_invoice_status [schema_numerus invoice_status invoice_status_i18n] 2023-02-07T15:07:06Z jordi fita mas <jordi@tandem.blog> # Add the list of available invoice status

35
test/invoice_status.sql Normal file
View File

@ -0,0 +1,35 @@
-- Test invoice_status
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(15);
set search_path to numerus, public;
select has_table('invoice_status');
select has_pk('invoice_status' );
select table_privs_are('invoice_status', 'guest', array []::text[]);
select table_privs_are('invoice_status', 'invoicer', array ['SELECT']);
select table_privs_are('invoice_status', 'admin', array ['SELECT']);
select table_privs_are('invoice_status', 'authenticator', array []::text[]);
select has_column('invoice_status', 'invoice_status');
select col_is_pk('invoice_status', 'invoice_status');
select col_type_is('invoice_status', 'invoice_status', 'text');
select col_not_null('invoice_status', 'invoice_status');
select col_hasnt_default('invoice_status', 'invoice_status');
select has_column('invoice_status', 'name');
select col_type_is('invoice_status', 'name', 'text');
select col_not_null('invoice_status', 'name');
select col_hasnt_default('invoice_status', 'name');
select *
from finish();
rollback;

View File

@ -0,0 +1,44 @@
-- Test invoice_status_i18n
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, public;
select has_table('invoice_status_i18n');
select has_pk('invoice_status_i18n' );
select col_is_pk('invoice_status_i18n', array['invoice_status', 'lang_tag']);
select table_privs_are('invoice_status_i18n', 'guest', array []::text[]);
select table_privs_are('invoice_status_i18n', 'invoicer', array ['SELECT']);
select table_privs_are('invoice_status_i18n', 'admin', array ['SELECT']);
select table_privs_are('invoice_status_i18n', 'authenticator', array []::text[]);
select has_column('invoice_status_i18n', 'invoice_status');
select col_is_fk('invoice_status_i18n', 'invoice_status');
select fk_ok('invoice_status_i18n', 'invoice_status', 'invoice_status', 'invoice_status');
select col_type_is('invoice_status_i18n', 'invoice_status', 'text');
select col_not_null('invoice_status_i18n', 'invoice_status');
select col_hasnt_default('invoice_status_i18n', 'invoice_status');
select has_column('invoice_status_i18n', 'lang_tag');
select col_is_fk('invoice_status_i18n', 'lang_tag');
select fk_ok('invoice_status_i18n', 'lang_tag', 'language', 'lang_tag');
select col_type_is('invoice_status_i18n', 'lang_tag', 'text');
select col_not_null('invoice_status_i18n', 'lang_tag');
select col_hasnt_default('invoice_status_i18n', 'lang_tag');
select has_column('invoice_status_i18n', 'name');
select col_type_is('invoice_status_i18n', 'name', 'text');
select col_not_null('invoice_status_i18n', 'name');
select col_hasnt_default('invoice_status_i18n', 'name');
select *
from finish();
rollback;

View File

@ -0,0 +1,21 @@
-- Verify numerus:available_invoice_status on pg
begin;
set search_path to numerus;
select 1 / count(*) from invoice_status where invoice_status = 'created' and name ='Created';
select 1 / count(*) from invoice_status where invoice_status = 'sent' and name ='Sent';
select 1 / count(*) from invoice_status where invoice_status = 'paid' and name ='Paid';
select 1 / count(*) from invoice_status where invoice_status = 'unpaid' and name ='Unpaid';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' and name ='Creada' and lang_tag = 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' and name ='Creada' and lang_tag = 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'sent' and name ='Enviada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'sent' and name ='Enviada' and lang_tag= 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'unpaid' and name ='No cobrada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'unpaid' and name ='No cobrada' and lang_tag= 'es';
rollback;

10
verify/invoice_status.sql Normal file
View File

@ -0,0 +1,10 @@
-- Verify numerus:invoice_status on pg
begin;
select invoice_status
, name
from numerus.invoice_status
where false;
rollback;

View File

@ -0,0 +1,11 @@
-- Verify numerus:invoice_status_i18n on pg
begin;
select invoice_status
, lang_tag
, name
from numerus.invoice_status_i18n
where false;
rollback;

View File

@ -304,7 +304,7 @@ main {
margin-top: 2rem; margin-top: 2rem;
} }
input[type="text"], input[type="password"], input[type="email"], input[type="tel"], input[type="url"], input[type="number"], select { input[type="text"], input[type="password"], input[type="email"], input[type="tel"], input[type="url"], input[type="number"], select, textarea {
background-color: var(--numerus--background-color); background-color: var(--numerus--background-color);
border: 1px solid var(--numerus--color--black); border: 1px solid var(--numerus--color--black);
border-radius: 0; border-radius: 0;
@ -320,7 +320,7 @@ input.width-2x {
max-width: 30rem; max-width: 30rem;
} }
.input input::placeholder { .input input::placeholder, .input textarea::placeholder {
color: transparent; color: transparent;
} }
@ -330,6 +330,7 @@ input.width-2x {
pointer-events: none; pointer-events: none;
} }
.input textarea:not(:focus):placeholder-shown ~ label,
.input input:placeholder-shown ~ label { .input input:placeholder-shown ~ label {
font-size: 1em; font-size: 1em;
background-color: initial; background-color: initial;
@ -342,11 +343,13 @@ input.width-2x {
color: var(--numerus--color--red); color: var(--numerus--color--red);
} }
[lang="en"] textarea:not([required]) + label::after,
[lang="en"] input:not([required]) + label::after, [lang="en"] input:not([required]) + label::after,
[lang="en"] select:not([required]) + label::after { [lang="en"] select:not([required]) + label::after {
content: " (optional)" content: " (optional)"
} }
[lang="ca"] textarea:not([required]) + label::after, [lang="es"] textarea:not([required]) + label::after,
[lang="ca"] input:not([required]) + label::after, [lang="es"] input:not([required]) + label::after, [lang="ca"] input:not([required]) + label::after, [lang="es"] input:not([required]) + label::after,
[lang="ca"] select:not([required]) + label::after, [lang="es"] select:not([required]) + label::after { [lang="ca"] select:not([required]) + label::after, [lang="es"] select:not([required]) + label::after {
content: " (opcional)" content: " (opcional)"

View File

@ -1,8 +1,15 @@
{{ define "input-field" -}} {{ define "input-field" -}}
<div class="input {{ if .Errors }}has-errors{{ end }}"> <div class="input {{ if .Errors }}has-errors{{ end }}">
{{ if eq .Type "textarea" }}
<textarea name="{{ .Name }}" id="{{ .Name }}-field"
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
{{ if .Required }}required="required"{{ end }} placeholder="{{ .Label }}"
>{{ .Val }}</textarea>
{{ else }}
<input type="{{ .Type }}" name="{{ .Name }}" id="{{ .Name }}-field" <input type="{{ .Type }}" name="{{ .Name }}" id="{{ .Name }}-field"
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }} {{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
{{ if .Required }}required="required"{{ end }} value="{{ .Val }}" placeholder="{{ .Label }}"> {{ if .Required }}required="required"{{ end }} value="{{ .Val }}" placeholder="{{ .Label }}">
{{ end }}
<label for="{{ .Name }}-field">{{ .Label }}</label> <label for="{{ .Name }}-field">{{ .Label }}</label>
{{- if .Errors }} {{- if .Errors }}
<ul> <ul>