From 1c0f126c58ffd14a54293d9add498250f55aa9a0 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Fri, 30 Jun 2023 21:32:48 +0200 Subject: [PATCH] Split contact relation into tax_details, phone, web, and email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We need to have contacts with just a name: we need to assign freelancer’s quote as expense linked the government, but of course we do not have a phone or email for that “contact”, much less a VATIN or other tax details. It is also interesting for other expenses-only contacts to not have to input all tax details, as we may not need to invoice then, thus are useless for us, but sometimes it might be interesting to have them, “just in case”. Of course, i did not want to make nullable any of the tax details required to generate an invoice, otherwise we could allow illegal invoices. Therefore, that data had to go in a different relation, and invoice’s foreign key update to point to that relation, not just customer, or we would again be able to create invalid invoices. We replaced the contact’s trade name with just name, because we do not need _three_ names for a contact, but we _do_ need two: the one we use to refer to them and the business name for tax purposes. The new contact_phone, contact_web, and contact_email relations could be simply a nullable field, but i did not see the point, since there are not that many instances where i need any of this data. Now company.taxDetailsForm is no longer “the same as contactForm with some extra fields”, because i have to add a check whether the user needs to invoice the contact, to check that the required values are there. I have an additional problem with the contact form when not using JavaScript: i must set the required field to all tax details fields to avoid the “(optional)” suffix, and because they _are_ required when that checkbox is enabled, but i can not set them optional when the check is unchecked. My solution for now is to ignore the form validation, and later i will add some JavaScript that adds the validation again, so it will work in all cases. --- demo/demo.sql | 12 +- deploy/add_contact.sql | 42 +++- deploy/add_contact@v0.sql | 36 +++ deploy/contact_email.sql | 39 ++++ deploy/contact_phone.sql | 38 +++ deploy/contact_tax_details.sql | 60 +++++ deploy/contact_web.sql | 39 ++++ deploy/edit_contact.sql | 79 +++++-- deploy/edit_contact@v0.sql | 53 +++++ deploy/invoice_contact_id_fkey.sql | 13 ++ deploy/tax_details.sql | 20 ++ pkg/company.go | 144 +++++++++++- pkg/contacts.go | 181 +++++++++------ pkg/expenses.go | 2 +- pkg/form.go | 38 +++ pkg/invoices.go | 46 +++- pkg/pgtypes.go | 58 +++++ pkg/quote.go | 12 +- po/ca.po | 358 +++++++++++++++-------------- po/es.po | 358 +++++++++++++++-------------- revert/add_contact.sql | 36 ++- revert/add_contact@v0.sql | 7 + revert/contact_email.sql | 23 ++ revert/contact_phone.sql | 23 ++ revert/contact_tax_details.sql | 51 ++++ revert/contact_web.sql | 23 ++ revert/edit_contact.sql | 52 ++++- revert/edit_contact@v0.sql | 7 + revert/invoice_contact_id_fkey.sql | 10 + revert/tax_details.sql | 7 + sqitch.plan | 9 + test/add_contact.sql | 76 ++++-- test/add_expense.sql | 10 +- test/add_invoice.sql | 18 +- test/add_quote.sql | 10 +- test/attach_to_expense.sql | 6 +- test/contact.sql | 80 ++----- test/contact_email.sql | 119 ++++++++++ test/contact_phone.sql | 118 ++++++++++ test/contact_tax_details.sql | 157 +++++++++++++ test/contact_web.sql | 118 ++++++++++ test/edit_contact.sql | 95 ++++++-- test/edit_expense.sql | 6 +- test/edit_invoice.sql | 12 +- test/edit_quote.sql | 6 +- test/expense.sql | 6 +- test/expense_attachment.sql | 6 +- test/expense_tax.sql | 6 +- test/expense_tax_amount.sql | 4 +- test/invoice.sql | 14 +- test/invoice_amount.sql | 9 +- test/invoice_product.sql | 12 +- test/invoice_product_amount.sql | 9 +- test/invoice_product_tax.sql | 12 +- test/invoice_tax_amount.sql | 9 +- test/quote_amount.sql | 4 +- test/tax_details.sql | 26 +++ verify/add_contact.sql | 2 +- verify/add_contact@v0.sql | 7 + verify/contact_email.sql | 13 ++ verify/contact_phone.sql | 13 ++ verify/contact_tax_details.sql | 19 ++ verify/contact_web.sql | 13 ++ verify/edit_contact.sql | 2 +- verify/edit_contact@v0.sql | 7 + verify/invoice_contact_id_fkey.sql | 13 ++ verify/tax_details.sql | 7 + web/static/numerus.css | 8 + web/template/contacts/edit.gohtml | 15 +- web/template/contacts/new.gohtml | 31 +-- web/template/form.gohtml | 6 + web/template/quotes/view.gohtml | 16 +- 72 files changed, 2339 insertions(+), 657 deletions(-) create mode 100644 deploy/add_contact@v0.sql create mode 100644 deploy/contact_email.sql create mode 100644 deploy/contact_phone.sql create mode 100644 deploy/contact_tax_details.sql create mode 100644 deploy/contact_web.sql create mode 100644 deploy/edit_contact@v0.sql create mode 100644 deploy/invoice_contact_id_fkey.sql create mode 100644 deploy/tax_details.sql create mode 100644 revert/add_contact@v0.sql create mode 100644 revert/contact_email.sql create mode 100644 revert/contact_phone.sql create mode 100644 revert/contact_tax_details.sql create mode 100644 revert/contact_web.sql create mode 100644 revert/edit_contact@v0.sql create mode 100644 revert/invoice_contact_id_fkey.sql create mode 100644 revert/tax_details.sql create mode 100644 test/contact_email.sql create mode 100644 test/contact_phone.sql create mode 100644 test/contact_tax_details.sql create mode 100644 test/contact_web.sql create mode 100644 test/tax_details.sql create mode 100644 verify/add_contact@v0.sql create mode 100644 verify/contact_email.sql create mode 100644 verify/contact_phone.sql create mode 100644 verify/contact_tax_details.sql create mode 100644 verify/contact_web.sql create mode 100644 verify/edit_contact@v0.sql create mode 100644 verify/invoice_contact_id_fkey.sql create mode 100644 verify/tax_details.sql diff --git a/demo/demo.sql b/demo/demo.sql index d78dd13..68040d8 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -42,12 +42,12 @@ values (123, 123, 'Retenció 15 %', -0.15) ; alter sequence contact_contact_id_seq restart with 123; -select add_contact (123, 'Melcior', '1', 'Rei Blanc', '0732621', 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR', array['pesebre', 'mag']); -select add_contact (123, 'Gaspar', '2', 'Rei Ros', '111', 'gaspar@reismags.cat', '', 'C/ Principal, 2', 'Nova Delhi', 'Delhi', '2', 'IN', array['pesebre', 'mag']); -select add_contact (123, 'Baltasar', '3', 'Rei Negre', '1-111-111', 'baltasar@reismags.cat', '', 'C/ Principal, 3', 'Sanaa', 'Sanaa', '3', 'YE', array['pesebre', 'mag']); -select add_contact (123, 'Caganera', '41414141L', '', '222 222 222', 'caganera@pesebre.cat', '', 'C/ De l’Hort, 4', 'Olot', 'Girona', '17800', 'ES', array['pesebre', 'persona']); -select add_contact (123, 'Bou', '41414142C', '', '333 333 333', 'bou@pesebre.cat', '', 'C/ De la Palla, 5', 'Sant Climent Sescebes', 'Girona', '17751', 'ES', array['pesebre', 'bestia']); -select add_contact (123, 'Rabadà', '41414143K', '', '444 444 444', 'rabada@pesebre.cat', '', 'C/ De les Ovelles, 6', 'Fornells de la Selva', 'Girona', '17458', 'ES', array['pesebre', 'persona']); +select add_contact (123, 'Melcior', '0732621', 'melcio@reismags.cat', '', '(Rei Blanc,1,"C/ Principal, 1",Shiraz,Fars,1,IR)', array['pesebre', 'mag']); +select add_contact (123, 'Gaspar', '111', 'gaspar@reismags.cat', '', '(Rei Ros,2,"C/ Principal, 2",Nova Delhi,Delhi,2,IN)', array['pesebre', 'mag']); +select add_contact (123, 'Baltasar', '1-111-111', 'baltasar@reismags.cat', '', '(Rei Negre,3,"C/ Principal, 3",Sanaa,Sanaa,3,YE)', array['pesebre', 'mag']); +select add_contact (123, 'Caganera', '222 222 222', 'caganera@pesebre.cat', '', '(Caganera,41414141L,"C/ De l’Hort, 4",Olot,Girona,17800,ES)', array['pesebre', 'persona']); +select add_contact (123, 'Bou', '333 333 333', 'bou@pesebre.cat', '', '(Bou,41414142C,"C/ De la Palla, 5",Sant Climent Sescebes,Girona,17751,ES)', array['pesebre', 'bestia']); +select add_contact (123, 'Rabadà', '444 444 444', 'rabada@pesebre.cat', '', '(Rabadà,41414143K,"C/ De les Ovelles, 6",Fornells de la Selva,Girona,17458,ES)', array['pesebre', 'persona']); alter sequence product_product_id_seq restart with 123; select add_product(123, '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 l’aigua règia.', '55.92', array[124], array['metall']); diff --git a/deploy/add_contact.sql b/deploy/add_contact.sql index 86d54fb..67f5057 100644 --- a/deploy/add_contact.sql +++ b/deploy/add_contact.sql @@ -7,21 +7,47 @@ -- requires: country_code -- requires: contact -- requires: tag_name +-- requires: tax_details begin; set search_path to numerus, public; -create or replace function add_contact(company_id integer, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as +create or replace function add_contact(company_id integer, name text, phone text, email email, web uri, tax_details tax_details, tags tag_name[]) returns uuid as $$ declare cid integer; cslug uuid; begin - insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, tags) - values (add_contact.company_id, add_contact.business_name, (add_contact.country_code || add_contact.vatin)::vatin, add_contact.trade_name, parse_packed_phone_number(add_contact.phone, add_contact.country_code), add_contact.email, add_contact.web, add_contact.address, add_contact.city, add_contact.province, add_contact.postal_code, add_contact.country_code, add_contact.tags) + insert into contact (company_id, name, tags) + values (add_contact.company_id, add_contact.name, add_contact.tags) returning contact_id, slug - into cid, cslug; + into cid, cslug + ; + + if tax_details is not null then + insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) + values (cid, tax_details.business_name, (tax_details.country_code || tax_details.vatin)::vatin, tax_details.address, tax_details.city, tax_details.province, tax_details.postal_code, tax_details.country_code) + ; + end if; + + if phone is not null and trim(phone) <> '' then + insert into contact_phone (contact_id, phone) + values (cid, parse_packed_phone_number(add_contact.phone, coalesce(tax_details.country_code, 'ES'))) + ; + end if; + + if email is not null and trim(email) <> '' then + insert into contact_email (contact_id, email) + values (cid, add_contact.email) + ; + end if; + + if web is not null and web <> '' then + insert into contact_web (contact_id, uri) + values (cid, add_contact.web) + ; + end if; return cslug; end @@ -29,8 +55,10 @@ $$ language plpgsql ; -revoke execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public; -grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer; -grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin; +revoke execute on function add_contact(integer, text, text, email, uri, tax_details, tag_name[]) from public; +grant execute on function add_contact(integer, text, text, email, uri, tax_details, tag_name[]) to invoicer; +grant execute on function add_contact(integer, text, text, email, uri, tax_details, tag_name[]) to admin; + +drop function if exists add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]); commit; diff --git a/deploy/add_contact@v0.sql b/deploy/add_contact@v0.sql new file mode 100644 index 0000000..86d54fb --- /dev/null +++ b/deploy/add_contact@v0.sql @@ -0,0 +1,36 @@ +-- Deploy numerus:add_contact to pg +-- requires: schema_numerus +-- requires: extension_vat +-- requires: email +-- requires: extension_pg_libphonenumber +-- requires: extension_uri +-- requires: country_code +-- requires: contact +-- requires: tag_name + +begin; + +set search_path to numerus, public; + +create or replace function add_contact(company_id integer, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as +$$ +declare + cid integer; + cslug uuid; +begin + insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, tags) + values (add_contact.company_id, add_contact.business_name, (add_contact.country_code || add_contact.vatin)::vatin, add_contact.trade_name, parse_packed_phone_number(add_contact.phone, add_contact.country_code), add_contact.email, add_contact.web, add_contact.address, add_contact.city, add_contact.province, add_contact.postal_code, add_contact.country_code, add_contact.tags) + returning contact_id, slug + into cid, cslug; + + return cslug; +end +$$ + language plpgsql +; + +revoke execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public; +grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer; +grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin; + +commit; diff --git a/deploy/contact_email.sql b/deploy/contact_email.sql new file mode 100644 index 0000000..39d54ee --- /dev/null +++ b/deploy/contact_email.sql @@ -0,0 +1,39 @@ +-- Deploy numerus:contact_email to pg +-- requires: roles +-- requires: schema_numerus +-- requires: email +-- requires: contact + +begin; + +set search_path to numerus, public; + +create table contact_email ( + contact_id integer primary key references contact, + email email not null +); + +grant select, insert, update, delete on table contact_email to invoicer; +grant select, insert, update, delete on table contact_email to admin; + +alter table contact_email enable row level security; + +create policy company_policy +on contact_email +using ( + exists( + select 1 + from contact + where contact.contact_id = contact_email.contact_id + ) +); + +insert into contact_email +select contact_id, email +from contact; + +alter table contact + drop column email +; + +commit; diff --git a/deploy/contact_phone.sql b/deploy/contact_phone.sql new file mode 100644 index 0000000..2439d63 --- /dev/null +++ b/deploy/contact_phone.sql @@ -0,0 +1,38 @@ +-- Deploy numerus:contact_phone to pg +-- requires: roles +-- requires: schema_numerus +-- requires: extension_pg_libphonenumber + +begin; + +set search_path to numerus, public; + +create table contact_phone ( + contact_id integer primary key references contact, + phone packed_phone_number not null +); + +grant select, insert, update, delete on table contact_phone to invoicer; +grant select, insert, update, delete on table contact_phone to admin; + +alter table contact_phone enable row level security; + +create policy company_policy +on contact_phone +using ( + exists( + select 1 + from contact + where contact.contact_id = contact_phone.contact_id + ) +); + +insert into contact_phone +select contact_id, phone +from contact; + +alter table contact + drop column phone +; + +commit; diff --git a/deploy/contact_tax_details.sql b/deploy/contact_tax_details.sql new file mode 100644 index 0000000..a994d57 --- /dev/null +++ b/deploy/contact_tax_details.sql @@ -0,0 +1,60 @@ +-- Deploy numerus:contact_tax_details to pg +-- requires: roles +-- requires: schema_numerus +-- requires: contact +-- requires: extension_vat +-- requires: country_code +-- requires: country + +begin; + +set search_path to numerus, public; + +create table contact_tax_details ( + contact_id integer primary key references contact, + business_name text not null constraint business_name_not_empty check(length(trim(business_name)) > 1), + vatin vatin not null, + address text not null, + city text not null, + province text not null, + postal_code text not null, + country_code country_code not null references country +); + +alter table contact_tax_details enable row level security; + +create policy company_policy +on contact_tax_details +using ( + exists( + select 1 + from contact + where contact.contact_id = contact_tax_details.contact_id + ) +); + +grant select, insert, update, delete on table contact_tax_details to invoicer; +grant select, insert, update, delete on table contact_tax_details to admin; + +insert into contact_tax_details +select contact_id, business_name, vatin, address, city, province, postal_code, country_code +from contact; + +update contact set trade_name = business_name where trade_name = ''; + +alter table contact + rename column trade_name to name +; + +alter table contact + drop column business_name +, drop column vatin +, drop column address +, drop column city +, drop column province +, drop column postal_code +, drop column country_code +, add constraint name_not_empty check(length(trim(name)) > 1) +; + +commit; diff --git a/deploy/contact_web.sql b/deploy/contact_web.sql new file mode 100644 index 0000000..e10332c --- /dev/null +++ b/deploy/contact_web.sql @@ -0,0 +1,39 @@ +-- Deploy numerus:contact_web to pg +-- requires: roles +-- requires: schema_numerus +-- requires: extension_uri +-- requires: contact + +begin; + +set search_path to numerus, public; + +create table contact_web ( + contact_id integer primary key references contact, + uri uri not null +); + +grant select, insert, update, delete on table contact_web to invoicer; +grant select, insert, update, delete on table contact_web to admin; + +alter table contact_web enable row level security; + +create policy company_policy +on contact_web +using ( + exists( + select 1 + from contact + where contact.contact_id = contact_web.contact_id + ) +); + +insert into contact_web +select contact_id, web +from contact; + +alter table contact + drop column web +; + +commit; diff --git a/deploy/edit_contact.sql b/deploy/edit_contact.sql index 90e3908..32bb75d 100644 --- a/deploy/edit_contact.sql +++ b/deploy/edit_contact.sql @@ -7,29 +7,20 @@ -- requires: contact -- requires: extension_vat -- requires: extension_pg_libphonenumber +-- requires: tax_details begin; set search_path to numerus, public; -create or replace function edit_contact(contact_slug uuid, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as +create or replace function edit_contact(contact_slug uuid, name text, phone text, email email, web uri, tax_details tax_details, tags tag_name[]) returns uuid as $$ declare cid integer; company integer; begin update contact - set business_name = edit_contact.business_name - , vatin = (edit_contact.country_code || edit_contact.vatin)::vatin - , trade_name = edit_contact.trade_name - , phone = parse_packed_phone_number( edit_contact.phone, edit_contact.country_code) - , email = edit_contact.email - , web = edit_contact.web - , address = edit_contact.address - , city = edit_contact.city - , province = edit_contact.province - , postal_code = edit_contact.postal_code - , country_code = edit_contact.country_code + set name = edit_contact.name , tags = edit_contact.tags where slug = contact_slug returning contact_id, company_id @@ -40,14 +31,72 @@ begin return null; end if; + if tax_details is null then + delete + from contact_tax_details + where contact_id = cid + ; + else + insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) + values (cid, tax_details.business_name, (tax_details.country_code || tax_details.vatin)::vatin, tax_details.address, tax_details.city, tax_details.province, tax_details.postal_code, tax_details.country_code) + on conflict (contact_id) do update + set business_name = excluded.business_name + , vatin = excluded.vatin + , address = excluded.address + , city = excluded.city + , province = excluded.province + , postal_code = excluded.postal_code + , country_code = excluded.country_code + ; + end if; + + if phone is null or trim(phone) = '' then + delete from contact_phone + where contact_id = cid + ; + else + insert into contact_phone (contact_id, phone) + values (cid, parse_packed_phone_number(edit_contact.phone, coalesce(tax_details.country_code, 'ES'))) + on conflict (contact_id) do update + set phone = excluded.phone + ; + end if; + + if email is null or trim(email) = '' then + delete from contact_email + where contact_id = cid + ; + else + insert into contact_email (contact_id, email) + values (cid, edit_contact.email) + on conflict (contact_id) do update + set email = excluded.email + ; + end if; + + if web is null or web = '' then + delete from contact_web + where contact_id = cid + ; + else + insert into contact_web (contact_id, uri) + values (cid, edit_contact.web) + on conflict (contact_id) do update + set uri = excluded.uri + ; + end if; + return contact_slug; end $$ language plpgsql ; -revoke execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public; -grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer; -grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin; +revoke execute on function edit_contact(uuid, text, text, email, uri, tax_details, tag_name[]) from public; +grant execute on function edit_contact(uuid, text, text, email, uri, tax_details, tag_name[]) to invoicer; +grant execute on function edit_contact(uuid, text, text, email, uri, tax_details, tag_name[]) to admin; + + +drop function if exists edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]); commit; diff --git a/deploy/edit_contact@v0.sql b/deploy/edit_contact@v0.sql new file mode 100644 index 0000000..90e3908 --- /dev/null +++ b/deploy/edit_contact@v0.sql @@ -0,0 +1,53 @@ +-- Deploy numerus:edit_contact to pg +-- requires: schema_numerus +-- requires: email +-- requires: extension_uri +-- requires: country_code +-- requires: tag_name +-- requires: contact +-- requires: extension_vat +-- requires: extension_pg_libphonenumber + +begin; + +set search_path to numerus, public; + +create or replace function edit_contact(contact_slug uuid, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as +$$ +declare + cid integer; + company integer; +begin + update contact + set business_name = edit_contact.business_name + , vatin = (edit_contact.country_code || edit_contact.vatin)::vatin + , trade_name = edit_contact.trade_name + , phone = parse_packed_phone_number( edit_contact.phone, edit_contact.country_code) + , email = edit_contact.email + , web = edit_contact.web + , address = edit_contact.address + , city = edit_contact.city + , province = edit_contact.province + , postal_code = edit_contact.postal_code + , country_code = edit_contact.country_code + , tags = edit_contact.tags + where slug = contact_slug + returning contact_id, company_id + into cid, company + ; + + if cid is null then + return null; + end if; + + return contact_slug; +end +$$ + language plpgsql +; + +revoke execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public; +grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer; +grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin; + +commit; diff --git a/deploy/invoice_contact_id_fkey.sql b/deploy/invoice_contact_id_fkey.sql new file mode 100644 index 0000000..529f4c9 --- /dev/null +++ b/deploy/invoice_contact_id_fkey.sql @@ -0,0 +1,13 @@ +-- Deploy numerus:invoice_contact_id_fkey to pg +-- requires: schema_numerus +-- requires: invoice +-- requires: contact_tax_details + +begin; + +alter table numerus.invoice + drop constraint invoice_contact_id_fkey +, add foreign key (contact_id) references numerus.contact_tax_details (contact_id) +; + +commit; diff --git a/deploy/tax_details.sql b/deploy/tax_details.sql new file mode 100644 index 0000000..9e8db75 --- /dev/null +++ b/deploy/tax_details.sql @@ -0,0 +1,20 @@ +-- Deploy numerus:tax_details to pg +-- requires: schema_numerus +-- requires: extension_vat +-- requires: country_code + +begin; + +set search_path to numerus, public; + +create type tax_details as ( + business_name text, + vatin text, + address text, + city text, + province text, + postal_code text, + country_code country_code +); + +commit; diff --git a/pkg/company.go b/pkg/company.go index 35aca21..8c154f6 100644 --- a/pkg/company.go +++ b/pkg/company.go @@ -85,7 +85,18 @@ type PaymentMethod struct { } type taxDetailsForm struct { - *contactForm + locale *Locale + TradeName *InputField + BusinessName *InputField + VATIN *InputField + Phone *InputField + Email *InputField + Web *InputField + Address *InputField + City *InputField + Province *InputField + PostalCode *InputField + Country *SelectField Currency *SelectField InvoiceNumberFormat *InputField NextInvoiceNumber *InputField @@ -96,7 +107,94 @@ type taxDetailsForm struct { func newTaxDetailsForm(ctx context.Context, conn *Conn, locale *Locale) *taxDetailsForm { return &taxDetailsForm{ - contactForm: newContactForm(ctx, conn, locale), + locale: locale, + TradeName: &InputField{ + Name: "trade_name", + Label: pgettext("input", "Trade name", locale), + Type: "text", + }, + Phone: &InputField{ + Name: "phone", + Label: pgettext("input", "Phone", locale), + Type: "tel", + Required: true, + Attributes: []template.HTMLAttr{ + `autocomplete="tel"`, + }, + }, + Email: &InputField{ + Name: "email", + Label: pgettext("input", "Email", locale), + Type: "email", + Required: true, + Attributes: []template.HTMLAttr{ + `autocomplete="email"`, + }, + }, + Web: &InputField{ + Name: "web", + Label: pgettext("input", "Web", locale), + Type: "url", + Attributes: []template.HTMLAttr{ + `autocomplete="url"`, + }, + }, + BusinessName: &InputField{ + Name: "business_name", + Label: pgettext("input", "Business name", locale), + Type: "text", + Required: true, + Attributes: []template.HTMLAttr{ + `autocomplete="organization"`, + `minlength="2"`, + }, + }, + VATIN: &InputField{ + Name: "vatin", + Label: pgettext("input", "VAT number", locale), + Type: "text", + Required: true, + }, + Address: &InputField{ + Name: "address", + Label: pgettext("input", "Address", locale), + Type: "text", + Required: true, + Attributes: []template.HTMLAttr{ + `autocomplete="address-line1"`, + }, + }, + City: &InputField{ + Name: "city", + Label: pgettext("input", "City", locale), + Type: "text", + Required: true, + }, + Province: &InputField{ + Name: "province", + Label: pgettext("input", "Province", locale), + Type: "text", + Required: true, + }, + PostalCode: &InputField{ + Name: "postal_code", + Label: pgettext("input", "Postal code", locale), + Type: "text", + Required: true, + Attributes: []template.HTMLAttr{ + `autocomplete="postal-code"`, + }, + }, + Country: &SelectField{ + Name: "country", + Label: pgettext("input", "Country", locale), + Options: mustGetCountryOptions(ctx, conn, locale), + Required: true, + Selected: []string{"ES"}, + Attributes: []template.HTMLAttr{ + `autocomplete="country"`, + }, + }, Currency: &SelectField{ Name: "currency", Label: pgettext("input", "Currency", locale), @@ -143,9 +241,20 @@ func newTaxDetailsForm(ctx context.Context, conn *Conn, locale *Locale) *taxDeta } func (form *taxDetailsForm) Parse(r *http.Request) error { - if err := form.contactForm.Parse(r); err != nil { + if err := r.ParseForm(); err != nil { return err } + form.TradeName.FillValue(r) + form.BusinessName.FillValue(r) + form.VATIN.FillValue(r) + form.Phone.FillValue(r) + form.Email.FillValue(r) + form.Web.FillValue(r) + form.Address.FillValue(r) + form.City.FillValue(r) + form.Province.FillValue(r) + form.PostalCode.FillValue(r) + form.Country.FillValue(r) form.Currency.FillValue(r) form.InvoiceNumberFormat.FillValue(r) form.NextInvoiceNumber.FillValue(r) @@ -157,12 +266,39 @@ func (form *taxDetailsForm) Parse(r *http.Request) error { func (form *taxDetailsForm) Validate(ctx context.Context, conn *Conn) bool { validator := newFormValidator() + + country := "" + if validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale)) { + country = form.Country.Selected[0] + } + + validator.CheckRequiredInput(form.BusinessName, gettext("Business name can not be empty.", form.locale)) + validator.CheckInputMinLength(form.BusinessName, 2, gettext("Business name must have at least two letters.", form.locale)) + if validator.CheckRequiredInput(form.VATIN, gettext("VAT number can not be empty.", form.locale)) { + validator.CheckValidVATINInput(form.VATIN, country, gettext("This value is not a valid VAT number.", form.locale)) + } + if validator.CheckRequiredInput(form.Phone, gettext("Phone can not be empty.", form.locale)) { + validator.CheckValidPhoneInput(form.Phone, country, gettext("This value is not a valid phone number.", form.locale)) + } + if validator.CheckRequiredInput(form.Email, gettext("Email can not be empty.", form.locale)) { + validator.CheckValidEmailInput(form.Email, gettext("This value is not a valid email. It should be like name@domain.com.", form.locale)) + } + if form.Web.Val != "" { + validator.CheckValidURL(form.Web, gettext("This value is not a valid web address. It should be like https://domain.com/.", form.locale)) + } + validator.CheckRequiredInput(form.Address, gettext("Address can not be empty.", form.locale)) + validator.CheckRequiredInput(form.City, gettext("City can not be empty.", form.locale)) + validator.CheckRequiredInput(form.Province, gettext("Province can not be empty.", form.locale)) + if validator.CheckRequiredInput(form.PostalCode, gettext("Postal code can not be empty.", form.locale)) { + validator.CheckValidPostalCode(ctx, conn, form.PostalCode, country, gettext("This value is not a valid postal code.", form.locale)) + } validator.CheckValidSelectOption(form.Currency, gettext("Selected currency is not valid.", form.locale)) validator.CheckRequiredInput(form.InvoiceNumberFormat, gettext("Invoice number format can not be empty.", form.locale)) validator.CheckValidInteger(form.NextInvoiceNumber, 1, math.MaxInt32, gettext("Next invoice number must be a number greater than zero.", form.locale)) validator.CheckRequiredInput(form.QuoteNumberFormat, gettext("Quotation number format can not be empty.", form.locale)) validator.CheckValidInteger(form.NextQuoteNumber, 1, math.MaxInt32, gettext("Next quotation number must be a number greater than zero.", form.locale)) - return form.contactForm.Validate(ctx, conn) && validator.AllOK() + + return validator.AllOK() } func (form *taxDetailsForm) mustFillFromDatabase(ctx context.Context, conn *Conn, company *Company) *taxDetailsForm { diff --git a/pkg/contacts.go b/pkg/contacts.go index 7b969e6..77e06a5 100644 --- a/pkg/contacts.go +++ b/pkg/contacts.go @@ -69,7 +69,7 @@ type editContactPage struct { func mustRenderEditContactForm(w http.ResponseWriter, r *http.Request, slug string, form *contactForm) { page := &editContactPage{ Slug: slug, - ContactName: form.BusinessName.Val, + ContactName: form.Name.String(), Form: form, } mustRenderMainTemplate(w, r, "contacts/edit.gohtml", page) @@ -95,7 +95,7 @@ func HandleAddContact(w http.ResponseWriter, r *http.Request, _ httprouter.Param return } company := mustGetCompany(r) - conn.MustExec(r.Context(), "select add_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", company.Id, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Tags) + conn.MustExec(r.Context(), "select add_contact($1, $2, $3, $4, $5, $6, $7)", company.Id, form.Name, form.Phone.ValueOrNil(), form.Email.ValueOrNil(), form.Web.ValueOrNil(), form.TaxDetails(), form.Tags) htmxRedirect(w, r, companyURI(company, "/contacts")) } @@ -115,7 +115,7 @@ func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprout mustRenderEditContactForm(w, r, params[0].Value, form) return } - slug := conn.MustGetText(r.Context(), "", "select edit_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", params[0].Value, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Tags) + slug := conn.MustGetText(r.Context(), "", "select edit_contact($1, $2, $3, $4, $5, $6, $7)", params[0].Value, form.Name, form.Phone.ValueOrNil(), form.Email.ValueOrNil(), form.Web.ValueOrNil(), form.TaxDetails(), form.Tags) if slug == "" { http.NotFound(w, r) } @@ -177,7 +177,7 @@ func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company if filters != nil { name := strings.TrimSpace(filters.Name.String()) if name != "" { - appendWhere("contact.business_name ilike $%d", "%"+name+"%") + appendWhere("contact.name ilike $%d", "%"+name+"%") } if len(filters.Tags.Tags) > 0 { if filters.TagsCondition.Selected == "and" { @@ -189,13 +189,15 @@ func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company } rows := conn.MustQuery(ctx, fmt.Sprintf(` select slug - , business_name - , email - , phone + , name + , coalesce(email::text, '') + , coalesce(phone::text, '') , tags from contact + left join contact_email using (contact_id) + left join contact_phone using (contact_id) where (%s) - order by business_name + order by name `, strings.Join(where, ") AND (")), args...) defer rows.Close() @@ -215,24 +217,59 @@ func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company } type contactForm struct { - locale *Locale - BusinessName *InputField - VATIN *InputField - TradeName *InputField - Phone *InputField - Email *InputField - Web *InputField - Address *InputField - City *InputField - Province *InputField - PostalCode *InputField - Country *SelectField - Tags *TagsField + locale *Locale + Name *InputField + HasTaxDetails *CheckField + BusinessName *InputField + VATIN *InputField + Phone *InputField + Email *InputField + Web *InputField + Address *InputField + City *InputField + Province *InputField + PostalCode *InputField + Country *SelectField + Tags *TagsField } func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactForm { return &contactForm{ locale: locale, + Name: &InputField{ + Name: "name", + Label: pgettext("input", "Name", locale), + Type: "text", + Required: true, + }, + Phone: &InputField{ + Name: "phone", + Label: pgettext("input", "Phone", locale), + Type: "tel", + Attributes: []template.HTMLAttr{ + `autocomplete="tel"`, + }, + }, + Email: &InputField{ + Name: "email", + Label: pgettext("input", "Email", locale), + Type: "email", + Attributes: []template.HTMLAttr{ + `autocomplete="email"`, + }, + }, + Web: &InputField{ + Name: "web", + Label: pgettext("input", "Web", locale), + Type: "url", + Attributes: []template.HTMLAttr{ + `autocomplete="url"`, + }, + }, + HasTaxDetails: &CheckField{ + Name: "has_tax_details", + Label: pgettext("input", "Need to input tax details", locale), + }, BusinessName: &InputField{ Name: "business_name", Label: pgettext("input", "Business name", locale), @@ -249,37 +286,6 @@ func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactFor Type: "text", Required: true, }, - TradeName: &InputField{ - Name: "trade_name", - Label: pgettext("input", "Trade name", locale), - Type: "text", - }, - Phone: &InputField{ - Name: "phone", - Label: pgettext("input", "Phone", locale), - Type: "tel", - Required: true, - Attributes: []template.HTMLAttr{ - `autocomplete="tel"`, - }, - }, - Email: &InputField{ - Name: "email", - Label: pgettext("input", "Email", locale), - Type: "email", - Required: true, - Attributes: []template.HTMLAttr{ - `autocomplete="email"`, - }, - }, - Web: &InputField{ - Name: "web", - Label: pgettext("input", "Web", locale), - Type: "url", - Attributes: []template.HTMLAttr{ - `autocomplete="url"`, - }, - }, Address: &InputField{ Name: "address", Label: pgettext("input", "Address", locale), @@ -331,9 +337,10 @@ func (form *contactForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } + form.Name.FillValue(r) + form.HasTaxDetails.FillValue(r) form.BusinessName.FillValue(r) form.VATIN.FillValue(r) - form.TradeName.FillValue(r) form.Phone.FillValue(r) form.Email.FillValue(r) form.Web.FillValue(r) @@ -349,42 +356,50 @@ func (form *contactForm) Parse(r *http.Request) error { func (form *contactForm) Validate(ctx context.Context, conn *Conn) bool { validator := newFormValidator() - country := "" - if validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale)) { - country = form.Country.Selected[0] + country := "ES" + if form.HasTaxDetails.Checked { + if validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale)) { + country = form.Country.Selected[0] + } + validator.CheckRequiredInput(form.BusinessName, gettext("Business name can not be empty.", form.locale)) + validator.CheckInputMinLength(form.BusinessName, 2, gettext("Business name must have at least two letters.", form.locale)) + if validator.CheckRequiredInput(form.VATIN, gettext("VAT number can not be empty.", form.locale)) { + validator.CheckValidVATINInput(form.VATIN, country, gettext("This value is not a valid VAT number.", form.locale)) + } + validator.CheckRequiredInput(form.Address, gettext("Address can not be empty.", form.locale)) + validator.CheckRequiredInput(form.City, gettext("City can not be empty.", form.locale)) + validator.CheckRequiredInput(form.Province, gettext("Province can not be empty.", form.locale)) + + if validator.CheckRequiredInput(form.PostalCode, gettext("Postal code can not be empty.", form.locale)) { + validator.CheckValidPostalCode(ctx, conn, form.PostalCode, country, gettext("This value is not a valid postal code.", form.locale)) + } } - validator.CheckRequiredInput(form.BusinessName, gettext("Business name can not be empty.", form.locale)) - validator.CheckInputMinLength(form.BusinessName, 2, gettext("Business name must have at least two letters.", form.locale)) - if validator.CheckRequiredInput(form.VATIN, gettext("VAT number can not be empty.", form.locale)) { - validator.CheckValidVATINInput(form.VATIN, country, gettext("This value is not a valid VAT number.", form.locale)) - } - if validator.CheckRequiredInput(form.Phone, gettext("Phone can not be empty.", form.locale)) { + validator.CheckRequiredInput(form.Name, gettext("Name can not be empty.", form.locale)) + validator.CheckInputMinLength(form.Name, 2, gettext("Name must have at least two letters.", form.locale)) + + if form.Phone.Val != "" { validator.CheckValidPhoneInput(form.Phone, country, gettext("This value is not a valid phone number.", form.locale)) } - if validator.CheckRequiredInput(form.Email, gettext("Email can not be empty.", form.locale)) { + if form.Email.Val != "" { validator.CheckValidEmailInput(form.Email, gettext("This value is not a valid email. It should be like name@domain.com.", form.locale)) } if form.Web.Val != "" { validator.CheckValidURL(form.Web, gettext("This value is not a valid web address. It should be like https://domain.com/.", form.locale)) } - validator.CheckRequiredInput(form.Address, gettext("Address can not be empty.", form.locale)) - validator.CheckRequiredInput(form.City, gettext("City can not be empty.", form.locale)) - validator.CheckRequiredInput(form.Province, gettext("Province can not be empty.", form.locale)) - if validator.CheckRequiredInput(form.PostalCode, gettext("Postal code can not be empty.", form.locale)) { - validator.CheckValidPostalCode(ctx, conn, form.PostalCode, country, gettext("This value is not a valid postal code.", form.locale)) - } + return validator.AllOK() } func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool { return !notFoundErrorOrPanic(conn.QueryRow(ctx, ` - select business_name + select name + , vatin is not null + , business_name , substr(vatin::text, 3) - , trade_name , phone , email - , web + , uri , address , city , province @@ -392,11 +407,16 @@ func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s , country_code , tags from contact + left join contact_email using (contact_id) + left join contact_phone using (contact_id) + left join contact_web using (contact_id) + left join contact_tax_details using (contact_id) where slug = $1 `, slug).Scan( + form.Name, + form.HasTaxDetails, form.BusinessName, form.VATIN, - form.TradeName, form.Phone, form.Email, form.Web, @@ -408,6 +428,21 @@ func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s form.Tags)) } +func (form *contactForm) TaxDetails() *CustomerTaxDetails { + if !form.HasTaxDetails.Checked { + return nil + } + return &CustomerTaxDetails{ + BusinessName: form.BusinessName.String(), + VATIN: form.VATIN.String(), + Address: form.Address.String(), + City: form.City.String(), + Province: form.Province.String(), + PostalCode: form.PostalCode.String(), + CountryCode: form.Country.String(), + } +} + func ServeEditContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { conn := getConn(r) locale := getLocale(r) diff --git a/pkg/expenses.go b/pkg/expenses.go index e821add..e882603 100644 --- a/pkg/expenses.go +++ b/pkg/expenses.go @@ -52,7 +52,7 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, company *Company , invoice_date , invoice_number , to_price(amount, decimal_digits) - , contact.business_name + , contact.name , coalesce(attachment.original_filename, '') , expense.tags from expense diff --git a/pkg/form.go b/pkg/form.go index 6b6b895..35c9719 100644 --- a/pkg/form.go +++ b/pkg/form.go @@ -59,6 +59,13 @@ func (field *InputField) Value() (driver.Value, error) { return field.Val, nil } +func (field *InputField) ValueOrNil() driver.Valuer { + if field.Val == "" { + return nil + } + return field +} + func (field *InputField) FillValue(r *http.Request) { field.Val = strings.TrimSpace(r.FormValue(field.Name)) } @@ -287,6 +294,37 @@ func (field *RadioField) isValidOption(selected string) bool { return field.FindOption(selected) != nil } +type CheckField struct { + Name string + Label string + Checked bool + Attributes []template.HTMLAttr + Required bool + Errors []error +} + +func (field *CheckField) FillValue(r *http.Request) { + field.Checked = len(r.Form[field.Name]) > 0 +} + +func (field *CheckField) Scan(value interface{}) error { + if value == nil { + field.Checked = false + return nil + } + switch v := value.(type) { + case bool: + field.Checked = v + default: + field.Checked, _ = strconv.ParseBool(fmt.Sprintf("%v", v)) + } + return nil +} + +func (field *CheckField) Value() (driver.Value, error) { + return field.Checked, nil +} + type FileField struct { Name string Label string diff --git a/pkg/invoices.go b/pkg/invoices.go index 2a672c6..a6120e2 100644 --- a/pkg/invoices.go +++ b/pkg/invoices.go @@ -64,7 +64,7 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, locale *Locale, select invoice.slug , invoice_date , invoice_number - , contact.business_name + , contact.name , invoice.tags , invoice.invoice_status , isi18n.name @@ -379,7 +379,41 @@ func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug stri } var invoiceId int var decimalDigits int - if notFoundErrorOrPanic(conn.QueryRow(ctx, "select invoice_id, decimal_digits, invoice_number, invoice_date, notes, instructions, business_name, vatin, phone, email, address, city, province, postal_code, to_price(subtotal, decimal_digits), to_price(total, decimal_digits) from invoice join payment_method using (payment_method_id) join contact using (contact_id) join invoice_amount using (invoice_id) join currency using (currency_code) where invoice.slug = $1", slug).Scan(&invoiceId, &decimalDigits, &inv.Number, &inv.Date, &inv.Notes, &inv.PaymentInstructions, &inv.Invoicee.Name, &inv.Invoicee.VATIN, &inv.Invoicee.Phone, &inv.Invoicee.Email, &inv.Invoicee.Address, &inv.Invoicee.City, &inv.Invoicee.Province, &inv.Invoicee.PostalCode, &inv.Subtotal, &inv.Total)) { + if notFoundErrorOrPanic(conn.QueryRow(ctx, ` + select invoice_id + , decimal_digits + , invoice_number + , invoice_date + , notes + , instructions + , business_name + , vatin + , address + , city + , province + , postal_code + , to_price(subtotal, decimal_digits) + , to_price(total, decimal_digits) + from invoice + join payment_method using (payment_method_id) + join contact_tax_details using (contact_id) + join invoice_amount using (invoice_id) + join currency using (currency_code) + where invoice.slug = $1`, slug).Scan( + &invoiceId, + &decimalDigits, + &inv.Number, + &inv.Date, + &inv.Notes, + &inv.PaymentInstructions, + &inv.Invoicee.Name, + &inv.Invoicee.VATIN, + &inv.Invoicee.Address, + &inv.Invoicee.City, + &inv.Invoicee.Province, + &inv.Invoicee.PostalCode, + &inv.Subtotal, + &inv.Total)) { return nil } if err := conn.QueryRow(ctx, "select business_name, vatin, phone, email, address, city, province, postal_code, legal_disclaimer from company where company_id = $1", company.Id).Scan(&inv.Invoicer.Name, &inv.Invoicer.VATIN, &inv.Invoicer.Phone, &inv.Invoicer.Email, &inv.Invoicer.Address, &inv.Invoicer.City, &inv.Invoicer.Province, &inv.Invoicer.PostalCode, &inv.LegalDisclaimer); err != nil { @@ -605,7 +639,7 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co Name: "customer", Label: pgettext("input", "Customer", locale), Required: true, - Options: mustGetContactOptions(ctx, conn, company), + Options: mustGetCustomerOptions(ctx, conn, company), }, Date: &InputField{ Name: "date", @@ -826,7 +860,11 @@ func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*Sel } func mustGetContactOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption { - return MustGetOptions(ctx, conn, "select contact_id::text, business_name from contact where company_id = $1 order by business_name", company.Id) + return MustGetOptions(ctx, conn, "select contact_id::text, name from contact where company_id = $1 order by name", company.Id) +} + +func mustGetCustomerOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption { + return MustGetOptions(ctx, conn, "select contact_id::text, name from contact join contact_tax_details using (contact_id) where company_id = $1 order by name", company.Id) } func mustGetDefaultPaymentMethod(ctx context.Context, conn *Conn, company *Company) string { diff --git a/pkg/pgtypes.go b/pkg/pgtypes.go index 422e948..25326a5 100644 --- a/pkg/pgtypes.go +++ b/pkg/pgtypes.go @@ -7,6 +7,38 @@ import ( "github.com/jackc/pgx/v4" ) +type CustomerTaxDetails struct { + BusinessName string + VATIN string + Address string + City string + Province string + PostalCode string + CountryCode string +} + +func (src CustomerTaxDetails) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) { + typeName := "tax_details" + dt, ok := ci.DataTypeForName(typeName) + if !ok { + return nil, fmt.Errorf("unable to find oid for type name %v", typeName) + } + values := []interface{}{ + src.BusinessName, + src.VATIN, + src.Address, + src.City, + src.Province, + src.PostalCode, + src.CountryCode, + } + ct := pgtype.NewValue(dt.Value).(pgtype.ValueTranscoder) + if err := ct.Set(values); err != nil { + return nil, err + } + return ct.EncodeBinary(ci, buf) +} + type NewInvoiceProductArray []*invoiceProductForm func (src NewInvoiceProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) { @@ -262,6 +294,32 @@ func registerPgTypes(ctx context.Context, conn *pgx.Conn) error { return err } + countryCodeOID, err := registerPgType(ctx, conn, &pgtype.Text{}, "country_code") + if err != nil { + return err + } + + taxDetailsType, err := pgtype.NewCompositeType( + "tax_details", + []pgtype.CompositeTypeField{ + {"business_name", pgtype.TextOID}, + {"vatin", pgtype.TextOID}, + {"address", pgtype.TextOID}, + {"city", pgtype.TextOID}, + {"province", pgtype.TextOID}, + {"postal_code", pgtype.TextOID}, + {"discount_rate", countryCodeOID}, + }, + conn.ConnInfo(), + ) + if err != nil { + return err + } + _, err = registerPgType(ctx, conn, taxDetailsType, taxDetailsType.TypeName()) + if err != nil { + return err + } + _, err = conn.Exec(ctx, "reset role") return err } diff --git a/pkg/quote.go b/pkg/quote.go index c36b629..e7fd893 100644 --- a/pkg/quote.go +++ b/pkg/quote.go @@ -62,7 +62,7 @@ func mustCollectQuoteEntries(ctx context.Context, conn *Conn, locale *Locale, fi select quote.slug , quote_date , quote_number - , coalesce(contact.business_name, '') + , coalesce(contact.name, '') , quote.tags , quote.quote_status , isi18n.name @@ -333,6 +333,7 @@ type quote struct { Date time.Time Quoter taxDetails HasQuotee bool + HasTaxDetails bool Quotee taxDetails TermsAndConditions string Notes string @@ -372,10 +373,9 @@ func mustGetQuote(ctx context.Context, conn *Conn, company *Company, slug string , notes , coalesce(instructions, '') , contact_id is not null - , coalesce(business_name, '') + , coalesce(business_name, contact.name, '') + , contact_tax_details.contact_id is not null , coalesce(vatin::text, '') - , coalesce(phone::text, '') - , coalesce(email, '') , coalesce(address, '') , coalesce(city, '') , coalesce(province, '') @@ -387,6 +387,7 @@ func mustGetQuote(ctx context.Context, conn *Conn, company *Company, slug string left join payment_method using (payment_method_id) left join quote_contact using (quote_id) left join contact using (contact_id) + left join contact_tax_details using (contact_id) join quote_amount using (quote_id) join currency using (currency_code) where quote.slug = $1`, slug).Scan( @@ -399,9 +400,8 @@ func mustGetQuote(ctx context.Context, conn *Conn, company *Company, slug string &quo.PaymentInstructions, &quo.HasQuotee, &quo.Quotee.Name, + &quo.HasTaxDetails, &quo.Quotee.VATIN, - &quo.Quotee.Phone, - &quo.Quotee.Email, &quo.Quotee.Address, &quo.Quotee.City, &quo.Quotee.Province, diff --git a/po/ca.po b/po/ca.po index a93e276..68340ea 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: numerus\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-06-20 11:35+0200\n" +"POT-Creation-Date: 2023-06-30 21:08+0200\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -65,7 +65,7 @@ msgstr "Nom" #: web/template/invoices/products.gohtml:50 #: web/template/invoices/view.gohtml:62 web/template/quotes/products.gohtml:50 -#: web/template/quotes/view.gohtml:71 web/template/products/index.gohtml:42 +#: web/template/quotes/view.gohtml:73 web/template/products/index.gohtml:42 msgctxt "title" msgid "Price" msgstr "Preu" @@ -95,15 +95,15 @@ msgstr "Desfes" #: web/template/invoices/new.gohtml:60 web/template/invoices/view.gohtml:67 #: web/template/invoices/edit.gohtml:61 web/template/quotes/new.gohtml:61 -#: web/template/quotes/view.gohtml:76 web/template/quotes/edit.gohtml:62 +#: web/template/quotes/view.gohtml:78 web/template/quotes/edit.gohtml:62 msgctxt "title" msgid "Subtotal" msgstr "Subtotal" #: web/template/invoices/new.gohtml:70 web/template/invoices/view.gohtml:71 #: web/template/invoices/view.gohtml:111 web/template/invoices/edit.gohtml:71 -#: web/template/quotes/new.gohtml:71 web/template/quotes/view.gohtml:80 -#: web/template/quotes/view.gohtml:120 web/template/quotes/edit.gohtml:72 +#: web/template/quotes/new.gohtml:71 web/template/quotes/view.gohtml:82 +#: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:72 msgctxt "title" msgid "Total" msgstr "Total" @@ -116,7 +116,7 @@ msgstr "Actualitza" #: web/template/invoices/new.gohtml:90 web/template/invoices/edit.gohtml:91 #: web/template/quotes/new.gohtml:91 web/template/quotes/edit.gohtml:92 -#: web/template/contacts/new.gohtml:39 web/template/contacts/edit.gohtml:43 +#: web/template/contacts/new.gohtml:44 web/template/contacts/edit.gohtml:48 #: web/template/expenses/new.gohtml:33 web/template/expenses/edit.gohtml:38 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 msgctxt "action" @@ -230,22 +230,22 @@ msgctxt "action" msgid "Download invoice" msgstr "Descarrega factura" -#: web/template/invoices/view.gohtml:61 web/template/quotes/view.gohtml:70 +#: web/template/invoices/view.gohtml:61 web/template/quotes/view.gohtml:72 msgctxt "title" msgid "Concept" msgstr "Concepte" -#: web/template/invoices/view.gohtml:64 web/template/quotes/view.gohtml:73 +#: web/template/invoices/view.gohtml:64 web/template/quotes/view.gohtml:75 msgctxt "title" msgid "Discount" msgstr "Descompte" -#: web/template/invoices/view.gohtml:66 web/template/quotes/view.gohtml:75 +#: web/template/invoices/view.gohtml:66 web/template/quotes/view.gohtml:77 msgctxt "title" msgid "Units" msgstr "Unitats" -#: web/template/invoices/view.gohtml:101 web/template/quotes/view.gohtml:110 +#: web/template/invoices/view.gohtml:101 web/template/quotes/view.gohtml:112 msgctxt "title" msgid "Tax Base" msgstr "Base imposable" @@ -280,7 +280,7 @@ msgctxt "input" msgid "(Max. %s)" msgstr "(Màx. %s)" -#: web/template/form.gohtml:194 +#: web/template/form.gohtml:200 msgctxt "action" msgid "Filters" msgstr "Filtra" @@ -382,7 +382,7 @@ msgctxt "action" msgid "Download quotation" msgstr "Descarrega pressupost" -#: web/template/quotes/view.gohtml:63 +#: web/template/quotes/view.gohtml:65 msgid "Terms and Conditions:" msgstr "Condicions d’acceptació:" @@ -648,7 +648,7 @@ msgctxt "title" msgid "Edit Product “%s”" msgstr "Edició del producte «%s»" -#: pkg/login.go:37 pkg/profile.go:40 pkg/contacts.go:268 +#: pkg/login.go:37 pkg/company.go:127 pkg/profile.go:40 pkg/contacts.go:255 msgctxt "input" msgid "Email" msgstr "Correu-e" @@ -658,11 +658,11 @@ msgctxt "input" msgid "Password" msgstr "Contrasenya" -#: pkg/login.go:70 pkg/profile.go:89 pkg/contacts.go:365 +#: pkg/login.go:70 pkg/company.go:283 pkg/profile.go:89 msgid "Email can not be empty." msgstr "No podeu deixar el correu-e en blanc." -#: pkg/login.go:71 pkg/profile.go:90 pkg/contacts.go:366 +#: pkg/login.go:71 pkg/company.go:284 pkg/profile.go:90 pkg/contacts.go:385 msgid "This value is not a valid email. It should be like name@domain.com." msgstr "Aquest valor no és un correu-e vàlid. Hauria de ser similar a nom@domini.cat." @@ -674,16 +674,16 @@ msgstr "No podeu deixar la contrasenya en blanc." msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." -#: pkg/products.go:164 pkg/products.go:263 pkg/quote.go:823 pkg/invoices.go:871 -#: pkg/contacts.go:135 +#: pkg/products.go:164 pkg/products.go:263 pkg/quote.go:823 pkg/invoices.go:909 +#: pkg/contacts.go:135 pkg/contacts.go:241 msgctxt "input" msgid "Name" msgstr "Nom" #: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:630 #: pkg/expenses.go:188 pkg/expenses.go:347 pkg/invoices.go:174 -#: pkg/invoices.go:623 pkg/invoices.go:1170 pkg/contacts.go:140 -#: pkg/contacts.go:325 +#: pkg/invoices.go:657 pkg/invoices.go:1208 pkg/contacts.go:140 +#: pkg/contacts.go:331 msgctxt "input" msgid "Tags" msgstr "Etiquetes" @@ -716,147 +716,250 @@ msgstr "Qualsevol" msgid "Invoices must have at least one of the specified labels." msgstr "Les factures han de tenir com a mínim una de les etiquetes." -#: pkg/products.go:269 pkg/quote.go:837 pkg/invoices.go:885 +#: pkg/products.go:269 pkg/quote.go:837 pkg/invoices.go:923 msgctxt "input" msgid "Description" msgstr "Descripció" -#: pkg/products.go:274 pkg/quote.go:841 pkg/invoices.go:889 +#: pkg/products.go:274 pkg/quote.go:841 pkg/invoices.go:927 msgctxt "input" msgid "Price" msgstr "Preu" -#: pkg/products.go:284 pkg/quote.go:870 pkg/expenses.go:167 pkg/invoices.go:918 +#: pkg/products.go:284 pkg/quote.go:870 pkg/expenses.go:167 pkg/invoices.go:956 msgctxt "input" msgid "Taxes" msgstr "Imposts" -#: pkg/products.go:309 pkg/quote.go:919 pkg/profile.go:92 pkg/invoices.go:967 +#: pkg/products.go:309 pkg/quote.go:919 pkg/profile.go:92 pkg/invoices.go:1005 +#: pkg/contacts.go:378 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." -#: pkg/products.go:310 pkg/quote.go:920 pkg/invoices.go:968 +#: pkg/products.go:310 pkg/quote.go:920 pkg/invoices.go:1006 msgid "Price can not be empty." msgstr "No podeu deixar el preu en blanc." -#: pkg/products.go:311 pkg/quote.go:921 pkg/invoices.go:969 +#: pkg/products.go:311 pkg/quote.go:921 pkg/invoices.go:1007 msgid "Price must be a number greater than zero." msgstr "El preu ha de ser un número major a zero." #: pkg/products.go:313 pkg/quote.go:929 pkg/expenses.go:213 pkg/expenses.go:218 -#: pkg/invoices.go:977 +#: pkg/invoices.go:1015 msgid "Selected tax is not valid." msgstr "Heu seleccionat un impost que no és vàlid." #: pkg/products.go:314 pkg/quote.go:930 pkg/expenses.go:214 pkg/expenses.go:219 -#: pkg/invoices.go:978 +#: pkg/invoices.go:1016 msgid "You can only select a tax of each class." msgstr "Només podeu seleccionar un impost de cada classe." -#: pkg/company.go:102 +#: pkg/company.go:113 +msgctxt "input" +msgid "Trade name" +msgstr "Nom comercial" + +#: pkg/company.go:118 pkg/contacts.go:247 +msgctxt "input" +msgid "Phone" +msgstr "Telèfon" + +#: pkg/company.go:136 pkg/contacts.go:263 +msgctxt "input" +msgid "Web" +msgstr "Web" + +#: pkg/company.go:144 pkg/contacts.go:275 +msgctxt "input" +msgid "Business name" +msgstr "Nom i cognoms" + +#: pkg/company.go:154 pkg/contacts.go:285 +msgctxt "input" +msgid "VAT number" +msgstr "DNI / NIF" + +#: pkg/company.go:160 pkg/contacts.go:291 +msgctxt "input" +msgid "Address" +msgstr "Adreça" + +#: pkg/company.go:169 pkg/contacts.go:300 +msgctxt "input" +msgid "City" +msgstr "Població" + +#: pkg/company.go:175 pkg/contacts.go:306 +msgctxt "input" +msgid "Province" +msgstr "Província" + +#: pkg/company.go:181 pkg/contacts.go:312 +msgctxt "input" +msgid "Postal code" +msgstr "Codi postal" + +#: pkg/company.go:190 pkg/contacts.go:321 +msgctxt "input" +msgid "Country" +msgstr "País" + +#: pkg/company.go:200 msgctxt "input" msgid "Currency" msgstr "Moneda" -#: pkg/company.go:109 +#: pkg/company.go:207 msgctxt "input" msgid "Invoice number format" msgstr "Format del número de factura" -#: pkg/company.go:115 +#: pkg/company.go:213 msgctxt "input" msgid "Next invoice number" msgstr "Següent número de factura" -#: pkg/company.go:124 +#: pkg/company.go:222 msgctxt "input" msgid "Quotation number format" msgstr "Format del número de pressupost" -#: pkg/company.go:130 +#: pkg/company.go:228 msgctxt "input" msgid "Next quotation number" msgstr "Següent número de pressupost" -#: pkg/company.go:139 +#: pkg/company.go:237 msgctxt "input" msgid "Legal disclaimer" msgstr "Nota legal" -#: pkg/company.go:160 +#: pkg/company.go:271 pkg/contacts.go:361 +msgid "Selected country is not valid." +msgstr "Heu seleccionat un país que no és vàlid." + +#: pkg/company.go:275 pkg/contacts.go:364 +msgid "Business name can not be empty." +msgstr "No podeu deixar el nom i els cognoms en blanc." + +#: pkg/company.go:276 pkg/contacts.go:365 +msgid "Business name must have at least two letters." +msgstr "Nom i cognoms han de tenir com a mínim dues lletres." + +#: pkg/company.go:277 pkg/contacts.go:366 +msgid "VAT number can not be empty." +msgstr "No podeu deixar el DNI o NIF en blanc." + +#: pkg/company.go:278 pkg/contacts.go:367 +msgid "This value is not a valid VAT number." +msgstr "Aquest valor no és un DNI o NIF vàlid." + +#: pkg/company.go:280 +msgid "Phone can not be empty." +msgstr "No podeu deixar el telèfon en blanc." + +#: pkg/company.go:281 pkg/contacts.go:382 +msgid "This value is not a valid phone number." +msgstr "Aquest valor no és un telèfon vàlid." + +#: pkg/company.go:287 pkg/contacts.go:388 +msgid "This value is not a valid web address. It should be like https://domain.com/." +msgstr "Aquest valor no és una adreça web vàlida. Hauria de ser similar a https://domini.cat/." + +#: pkg/company.go:289 pkg/contacts.go:369 +msgid "Address can not be empty." +msgstr "No podeu deixar l’adreça en blanc." + +#: pkg/company.go:290 pkg/contacts.go:370 +msgid "City can not be empty." +msgstr "No podeu deixar la població en blanc." + +#: pkg/company.go:291 pkg/contacts.go:371 +msgid "Province can not be empty." +msgstr "No podeu deixar la província en blanc." + +#: pkg/company.go:292 pkg/contacts.go:373 +msgid "Postal code can not be empty." +msgstr "No podeu deixar el codi postal en blanc." + +#: pkg/company.go:293 pkg/contacts.go:374 +msgid "This value is not a valid postal code." +msgstr "Aquest valor no és un codi postal vàlid." + +#: pkg/company.go:295 msgid "Selected currency is not valid." msgstr "Heu seleccionat una moneda que no és vàlida." -#: pkg/company.go:161 +#: pkg/company.go:296 msgid "Invoice number format can not be empty." msgstr "No podeu deixar el format del número de factura en blanc." -#: pkg/company.go:162 +#: pkg/company.go:297 msgid "Next invoice number must be a number greater than zero." msgstr "El següent número de factura ha de ser un número major a zero." -#: pkg/company.go:163 +#: pkg/company.go:298 msgid "Quotation number format can not be empty." msgstr "No podeu deixar el format del número de pressupost en blanc." -#: pkg/company.go:164 +#: pkg/company.go:299 msgid "Next quotation number must be a number greater than zero." msgstr "El següent número de pressupost ha de ser un número major a zero." -#: pkg/company.go:427 +#: pkg/company.go:563 msgctxt "input" msgid "Tax name" msgstr "Nom impost" -#: pkg/company.go:433 +#: pkg/company.go:569 msgctxt "input" msgid "Tax Class" msgstr "Classe d’impost" -#: pkg/company.go:436 +#: pkg/company.go:572 msgid "Select a tax class" msgstr "Escolliu una classe d’impost" -#: pkg/company.go:440 +#: pkg/company.go:576 msgctxt "input" msgid "Rate (%)" msgstr "Percentatge" -#: pkg/company.go:463 +#: pkg/company.go:599 msgid "Tax name can not be empty." msgstr "No podeu deixar el nom de l’impost en blanc." -#: pkg/company.go:464 +#: pkg/company.go:600 msgid "Selected tax class is not valid." msgstr "Heu seleccionat una classe d’impost que no és vàlida." -#: pkg/company.go:465 +#: pkg/company.go:601 msgid "Tax rate can not be empty." msgstr "No podeu deixar percentatge en blanc." -#: pkg/company.go:466 +#: pkg/company.go:602 msgid "Tax rate must be an integer between -99 and 99." msgstr "El percentatge ha de ser entre -99 i 99." -#: pkg/company.go:529 +#: pkg/company.go:665 msgctxt "input" msgid "Payment method name" msgstr "Nom del mètode de pagament" -#: pkg/company.go:535 +#: pkg/company.go:671 msgctxt "input" msgid "Instructions" msgstr "Instruccions" -#: pkg/company.go:553 +#: pkg/company.go:689 msgid "Payment method name can not be empty." msgstr "No podeu deixar el nom del mètode de pagament en blanc." -#: pkg/company.go:554 +#: pkg/company.go:690 msgid "Payment instructions can not be empty." msgstr "No podeu deixar les instruccions de pagament en blanc." -#: pkg/quote.go:147 pkg/quote.go:608 pkg/invoices.go:147 pkg/invoices.go:606 +#: pkg/quote.go:147 pkg/quote.go:608 pkg/invoices.go:147 pkg/invoices.go:640 msgctxt "input" msgid "Customer" msgstr "Client" @@ -901,8 +1004,8 @@ msgstr "Els pressuposts han de tenir com a mínim una de les etiquetes." msgid "quotations.zip" msgstr "pressuposts.zip" -#: pkg/quote.go:556 pkg/quote.go:1085 pkg/quote.go:1093 pkg/invoices.go:555 -#: pkg/invoices.go:1145 pkg/invoices.go:1153 +#: pkg/quote.go:556 pkg/quote.go:1085 pkg/quote.go:1093 pkg/invoices.go:589 +#: pkg/invoices.go:1183 pkg/invoices.go:1191 msgid "Invalid action" msgstr "Acció invàlida." @@ -920,12 +1023,12 @@ msgctxt "input" msgid "Terms and conditions" msgstr "Condicions d’acceptació" -#: pkg/quote.go:625 pkg/invoices.go:618 +#: pkg/quote.go:625 pkg/invoices.go:652 msgctxt "input" msgid "Notes" msgstr "Notes" -#: pkg/quote.go:634 pkg/invoices.go:628 +#: pkg/quote.go:634 pkg/invoices.go:662 msgctxt "input" msgid "Payment Method" msgstr "Mètode de pagament" @@ -938,7 +1041,7 @@ msgstr "Escolliu un mètode de pagament." msgid "Selected quotation status is not valid." msgstr "Heu seleccionat un estat de pressupost que no és vàlid." -#: pkg/quote.go:673 pkg/invoices.go:665 +#: pkg/quote.go:673 pkg/invoices.go:699 msgid "Selected customer is not valid." msgstr "Heu seleccionat un client que no és vàlid." @@ -950,21 +1053,21 @@ msgstr "No podeu deixar la data del pressupost en blanc." msgid "Quotation date must be a valid date." msgstr "La data del pressupost ha de ser vàlida." -#: pkg/quote.go:679 pkg/invoices.go:669 +#: pkg/quote.go:679 pkg/invoices.go:703 msgid "Selected payment method is not valid." msgstr "Heu seleccionat un mètode de pagament que no és vàlid." -#: pkg/quote.go:813 pkg/quote.go:818 pkg/invoices.go:861 pkg/invoices.go:866 +#: pkg/quote.go:813 pkg/quote.go:818 pkg/invoices.go:899 pkg/invoices.go:904 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/quote.go:851 pkg/invoices.go:899 +#: pkg/quote.go:851 pkg/invoices.go:937 msgctxt "input" msgid "Quantity" msgstr "Quantitat" -#: pkg/quote.go:860 pkg/invoices.go:908 +#: pkg/quote.go:860 pkg/invoices.go:946 msgctxt "input" msgid "Discount (%)" msgstr "Descompte (%)" @@ -973,23 +1076,23 @@ msgstr "Descompte (%)" msgid "Quotation product ID must be a number greater than zero." msgstr "L’ID del producte de pressupost ha de ser un número major a zero." -#: pkg/quote.go:917 pkg/invoices.go:965 +#: pkg/quote.go:917 pkg/invoices.go:1003 msgid "Product ID must be a positive number or zero." msgstr "L’ID del producte ha de ser un número positiu o zero." -#: pkg/quote.go:923 pkg/invoices.go:971 +#: pkg/quote.go:923 pkg/invoices.go:1009 msgid "Quantity can not be empty." msgstr "No podeu deixar la quantitat en blanc." -#: pkg/quote.go:924 pkg/invoices.go:972 +#: pkg/quote.go:924 pkg/invoices.go:1010 msgid "Quantity must be a number greater than zero." msgstr "La quantitat ha de ser un número major a zero." -#: pkg/quote.go:926 pkg/invoices.go:974 +#: pkg/quote.go:926 pkg/invoices.go:1012 msgid "Discount can not be empty." msgstr "No podeu deixar el descompte en blanc." -#: pkg/quote.go:927 pkg/invoices.go:975 +#: pkg/quote.go:927 pkg/invoices.go:1013 msgid "Discount must be a percentage between 0 and 100." msgstr "El descompte ha de ser un percentatge entre 0 i 100." @@ -1070,7 +1173,7 @@ msgctxt "input" msgid "Invoice number" msgstr "Número de factura" -#: pkg/expenses.go:161 pkg/invoices.go:612 +#: pkg/expenses.go:161 pkg/invoices.go:646 msgctxt "input" msgid "Invoice Date" msgstr "Data de factura" @@ -1089,7 +1192,7 @@ msgstr "Fitxer" msgid "Selected contact is not valid." msgstr "Heu seleccionat un contacte que no és vàlid." -#: pkg/expenses.go:212 pkg/invoices.go:667 +#: pkg/expenses.go:212 pkg/invoices.go:701 msgid "Invoice date must be a valid date." msgstr "La data de facturació ha de ser vàlida." @@ -1110,142 +1213,49 @@ msgctxt "input" msgid "Invoice Number" msgstr "Número de factura" -#: pkg/invoices.go:153 pkg/invoices.go:600 +#: pkg/invoices.go:153 pkg/invoices.go:634 msgctxt "input" msgid "Invoice Status" msgstr "Estat de la factura" -#: pkg/invoices.go:448 +#: pkg/invoices.go:482 msgid "Select a customer to bill." msgstr "Escolliu un client a facturar." -#: pkg/invoices.go:549 +#: pkg/invoices.go:583 msgid "invoices.zip" msgstr "factures.zip" -#: pkg/invoices.go:664 +#: pkg/invoices.go:698 msgid "Selected invoice status is not valid." msgstr "Heu seleccionat un estat de factura que no és vàlid." -#: pkg/invoices.go:666 +#: pkg/invoices.go:700 msgid "Invoice date can not be empty." msgstr "No podeu deixar la data de la factura en blanc." -#: pkg/invoices.go:802 +#: pkg/invoices.go:836 #, c-format msgid "Re: quotation #%s of %s" msgstr "Ref: pressupost núm. %s del %s" -#: pkg/invoices.go:803 +#: pkg/invoices.go:837 msgctxt "to_char" msgid "MM/DD/YYYY" msgstr "DD/MM/YYYY" -#: pkg/invoices.go:962 +#: pkg/invoices.go:1000 msgid "Invoice product ID must be a number greater than zero." msgstr "L’ID del producte de factura ha de ser un número major a zero." -#: pkg/contacts.go:238 +#: pkg/contacts.go:271 msgctxt "input" -msgid "Business name" -msgstr "Nom i cognoms" +msgid "Need to input tax details" +msgstr "Necessito poder facturar aquest contacte" -#: pkg/contacts.go:248 -msgctxt "input" -msgid "VAT number" -msgstr "DNI / NIF" - -#: pkg/contacts.go:254 -msgctxt "input" -msgid "Trade name" -msgstr "Nom comercial" - -#: pkg/contacts.go:259 -msgctxt "input" -msgid "Phone" -msgstr "Telèfon" - -#: pkg/contacts.go:277 -msgctxt "input" -msgid "Web" -msgstr "Web" - -#: pkg/contacts.go:285 -msgctxt "input" -msgid "Address" -msgstr "Adreça" - -#: pkg/contacts.go:294 -msgctxt "input" -msgid "City" -msgstr "Població" - -#: pkg/contacts.go:300 -msgctxt "input" -msgid "Province" -msgstr "Província" - -#: pkg/contacts.go:306 -msgctxt "input" -msgid "Postal code" -msgstr "Codi postal" - -#: pkg/contacts.go:315 -msgctxt "input" -msgid "Country" -msgstr "País" - -#: pkg/contacts.go:353 -msgid "Selected country is not valid." -msgstr "Heu seleccionat un país que no és vàlid." - -#: pkg/contacts.go:357 -msgid "Business name can not be empty." -msgstr "No podeu deixar el nom i els cognoms en blanc." - -#: pkg/contacts.go:358 -msgid "Business name must have at least two letters." -msgstr "Nom i cognoms han de tenir com a mínim dues lletres." - -#: pkg/contacts.go:359 -msgid "VAT number can not be empty." -msgstr "No podeu deixar el DNI o NIF en blanc." - -#: pkg/contacts.go:360 -msgid "This value is not a valid VAT number." -msgstr "Aquest valor no és un DNI o NIF vàlid." - -#: pkg/contacts.go:362 -msgid "Phone can not be empty." -msgstr "No podeu deixar el telèfon en blanc." - -#: pkg/contacts.go:363 -msgid "This value is not a valid phone number." -msgstr "Aquest valor no és un telèfon vàlid." - -#: pkg/contacts.go:369 -msgid "This value is not a valid web address. It should be like https://domain.com/." -msgstr "Aquest valor no és una adreça web vàlida. Hauria de ser similar a https://domini.cat/." - -#: pkg/contacts.go:371 -msgid "Address can not be empty." -msgstr "No podeu deixar l’adreça en blanc." - -#: pkg/contacts.go:372 -msgid "City can not be empty." -msgstr "No podeu deixar la població en blanc." - -#: pkg/contacts.go:373 -msgid "Province can not be empty." -msgstr "No podeu deixar la província en blanc." - -#: pkg/contacts.go:374 -msgid "Postal code can not be empty." -msgstr "No podeu deixar el codi postal en blanc." - -#: pkg/contacts.go:375 -msgid "This value is not a valid postal code." -msgstr "Aquest valor no és un codi postal vàlid." +#: pkg/contacts.go:379 +msgid "Name must have at least two letters." +msgstr "El nom ha de tenir com a mínim dues lletres." #~ msgctxt "action" #~ msgid "Update contact" diff --git a/po/es.po b/po/es.po index bf58736..561d0cc 100644 --- a/po/es.po +++ b/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: numerus\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-06-20 11:35+0200\n" +"POT-Creation-Date: 2023-06-30 21:08+0200\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -65,7 +65,7 @@ msgstr "Nombre" #: web/template/invoices/products.gohtml:50 #: web/template/invoices/view.gohtml:62 web/template/quotes/products.gohtml:50 -#: web/template/quotes/view.gohtml:71 web/template/products/index.gohtml:42 +#: web/template/quotes/view.gohtml:73 web/template/products/index.gohtml:42 msgctxt "title" msgid "Price" msgstr "Precio" @@ -95,15 +95,15 @@ msgstr "Deshacer" #: web/template/invoices/new.gohtml:60 web/template/invoices/view.gohtml:67 #: web/template/invoices/edit.gohtml:61 web/template/quotes/new.gohtml:61 -#: web/template/quotes/view.gohtml:76 web/template/quotes/edit.gohtml:62 +#: web/template/quotes/view.gohtml:78 web/template/quotes/edit.gohtml:62 msgctxt "title" msgid "Subtotal" msgstr "Subtotal" #: web/template/invoices/new.gohtml:70 web/template/invoices/view.gohtml:71 #: web/template/invoices/view.gohtml:111 web/template/invoices/edit.gohtml:71 -#: web/template/quotes/new.gohtml:71 web/template/quotes/view.gohtml:80 -#: web/template/quotes/view.gohtml:120 web/template/quotes/edit.gohtml:72 +#: web/template/quotes/new.gohtml:71 web/template/quotes/view.gohtml:82 +#: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:72 msgctxt "title" msgid "Total" msgstr "Total" @@ -116,7 +116,7 @@ msgstr "Actualizar" #: web/template/invoices/new.gohtml:90 web/template/invoices/edit.gohtml:91 #: web/template/quotes/new.gohtml:91 web/template/quotes/edit.gohtml:92 -#: web/template/contacts/new.gohtml:39 web/template/contacts/edit.gohtml:43 +#: web/template/contacts/new.gohtml:44 web/template/contacts/edit.gohtml:48 #: web/template/expenses/new.gohtml:33 web/template/expenses/edit.gohtml:38 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 msgctxt "action" @@ -230,22 +230,22 @@ msgctxt "action" msgid "Download invoice" msgstr "Descargar factura" -#: web/template/invoices/view.gohtml:61 web/template/quotes/view.gohtml:70 +#: web/template/invoices/view.gohtml:61 web/template/quotes/view.gohtml:72 msgctxt "title" msgid "Concept" msgstr "Concepto" -#: web/template/invoices/view.gohtml:64 web/template/quotes/view.gohtml:73 +#: web/template/invoices/view.gohtml:64 web/template/quotes/view.gohtml:75 msgctxt "title" msgid "Discount" msgstr "Descuento" -#: web/template/invoices/view.gohtml:66 web/template/quotes/view.gohtml:75 +#: web/template/invoices/view.gohtml:66 web/template/quotes/view.gohtml:77 msgctxt "title" msgid "Units" msgstr "Unidades" -#: web/template/invoices/view.gohtml:101 web/template/quotes/view.gohtml:110 +#: web/template/invoices/view.gohtml:101 web/template/quotes/view.gohtml:112 msgctxt "title" msgid "Tax Base" msgstr "Base imponible" @@ -280,7 +280,7 @@ msgctxt "input" msgid "(Max. %s)" msgstr "(Máx. %s)" -#: web/template/form.gohtml:194 +#: web/template/form.gohtml:200 msgctxt "action" msgid "Filters" msgstr "Filtrar" @@ -382,7 +382,7 @@ msgctxt "action" msgid "Download quotation" msgstr "Descargar presupuesto" -#: web/template/quotes/view.gohtml:63 +#: web/template/quotes/view.gohtml:65 msgid "Terms and Conditions:" msgstr "Condiciones de aceptación:" @@ -648,7 +648,7 @@ msgctxt "title" msgid "Edit Product “%s”" msgstr "Edición del producto «%s»" -#: pkg/login.go:37 pkg/profile.go:40 pkg/contacts.go:268 +#: pkg/login.go:37 pkg/company.go:127 pkg/profile.go:40 pkg/contacts.go:255 msgctxt "input" msgid "Email" msgstr "Correo-e" @@ -658,11 +658,11 @@ msgctxt "input" msgid "Password" msgstr "Contraseña" -#: pkg/login.go:70 pkg/profile.go:89 pkg/contacts.go:365 +#: pkg/login.go:70 pkg/company.go:283 pkg/profile.go:89 msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." -#: pkg/login.go:71 pkg/profile.go:90 pkg/contacts.go:366 +#: pkg/login.go:71 pkg/company.go:284 pkg/profile.go:90 pkg/contacts.go:385 msgid "This value is not a valid email. It should be like name@domain.com." msgstr "Este valor no es un correo-e válido. Tiene que ser parecido a nombre@dominio.es." @@ -674,16 +674,16 @@ 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:164 pkg/products.go:263 pkg/quote.go:823 pkg/invoices.go:871 -#: pkg/contacts.go:135 +#: pkg/products.go:164 pkg/products.go:263 pkg/quote.go:823 pkg/invoices.go:909 +#: pkg/contacts.go:135 pkg/contacts.go:241 msgctxt "input" msgid "Name" msgstr "Nombre" #: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:630 #: pkg/expenses.go:188 pkg/expenses.go:347 pkg/invoices.go:174 -#: pkg/invoices.go:623 pkg/invoices.go:1170 pkg/contacts.go:140 -#: pkg/contacts.go:325 +#: pkg/invoices.go:657 pkg/invoices.go:1208 pkg/contacts.go:140 +#: pkg/contacts.go:331 msgctxt "input" msgid "Tags" msgstr "Etiquetes" @@ -716,147 +716,250 @@ msgstr "Cualquiera" msgid "Invoices must have at least one of the specified labels." msgstr "Las facturas deben tener como mínimo una de las etiquetas." -#: pkg/products.go:269 pkg/quote.go:837 pkg/invoices.go:885 +#: pkg/products.go:269 pkg/quote.go:837 pkg/invoices.go:923 msgctxt "input" msgid "Description" msgstr "Descripción" -#: pkg/products.go:274 pkg/quote.go:841 pkg/invoices.go:889 +#: pkg/products.go:274 pkg/quote.go:841 pkg/invoices.go:927 msgctxt "input" msgid "Price" msgstr "Precio" -#: pkg/products.go:284 pkg/quote.go:870 pkg/expenses.go:167 pkg/invoices.go:918 +#: pkg/products.go:284 pkg/quote.go:870 pkg/expenses.go:167 pkg/invoices.go:956 msgctxt "input" msgid "Taxes" msgstr "Impuestos" -#: pkg/products.go:309 pkg/quote.go:919 pkg/profile.go:92 pkg/invoices.go:967 +#: pkg/products.go:309 pkg/quote.go:919 pkg/profile.go:92 pkg/invoices.go:1005 +#: pkg/contacts.go:378 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." -#: pkg/products.go:310 pkg/quote.go:920 pkg/invoices.go:968 +#: pkg/products.go:310 pkg/quote.go:920 pkg/invoices.go:1006 msgid "Price can not be empty." msgstr "No podéis dejar el precio en blanco." -#: pkg/products.go:311 pkg/quote.go:921 pkg/invoices.go:969 +#: pkg/products.go:311 pkg/quote.go:921 pkg/invoices.go:1007 msgid "Price must be a number greater than zero." msgstr "El precio tiene que ser un número mayor a cero." #: pkg/products.go:313 pkg/quote.go:929 pkg/expenses.go:213 pkg/expenses.go:218 -#: pkg/invoices.go:977 +#: pkg/invoices.go:1015 msgid "Selected tax is not valid." msgstr "Habéis escogido un impuesto que no es válido." #: pkg/products.go:314 pkg/quote.go:930 pkg/expenses.go:214 pkg/expenses.go:219 -#: pkg/invoices.go:978 +#: pkg/invoices.go:1016 msgid "You can only select a tax of each class." msgstr "Solo podéis escoger un impuesto de cada clase." -#: pkg/company.go:102 +#: pkg/company.go:113 +msgctxt "input" +msgid "Trade name" +msgstr "Nombre comercial" + +#: pkg/company.go:118 pkg/contacts.go:247 +msgctxt "input" +msgid "Phone" +msgstr "Teléfono" + +#: pkg/company.go:136 pkg/contacts.go:263 +msgctxt "input" +msgid "Web" +msgstr "Web" + +#: pkg/company.go:144 pkg/contacts.go:275 +msgctxt "input" +msgid "Business name" +msgstr "Nombre y apellidos" + +#: pkg/company.go:154 pkg/contacts.go:285 +msgctxt "input" +msgid "VAT number" +msgstr "DNI / NIF" + +#: pkg/company.go:160 pkg/contacts.go:291 +msgctxt "input" +msgid "Address" +msgstr "Dirección" + +#: pkg/company.go:169 pkg/contacts.go:300 +msgctxt "input" +msgid "City" +msgstr "Población" + +#: pkg/company.go:175 pkg/contacts.go:306 +msgctxt "input" +msgid "Province" +msgstr "Provincia" + +#: pkg/company.go:181 pkg/contacts.go:312 +msgctxt "input" +msgid "Postal code" +msgstr "Código postal" + +#: pkg/company.go:190 pkg/contacts.go:321 +msgctxt "input" +msgid "Country" +msgstr "País" + +#: pkg/company.go:200 msgctxt "input" msgid "Currency" msgstr "Moneda" -#: pkg/company.go:109 +#: pkg/company.go:207 msgctxt "input" msgid "Invoice number format" msgstr "Formato del número de factura" -#: pkg/company.go:115 +#: pkg/company.go:213 msgctxt "input" msgid "Next invoice number" msgstr "Siguiente número de factura" -#: pkg/company.go:124 +#: pkg/company.go:222 msgctxt "input" msgid "Quotation number format" msgstr "Formato del número de presupuesto" -#: pkg/company.go:130 +#: pkg/company.go:228 msgctxt "input" msgid "Next quotation number" msgstr "Siguiente número de presupuesto" -#: pkg/company.go:139 +#: pkg/company.go:237 msgctxt "input" msgid "Legal disclaimer" msgstr "Nota legal" -#: pkg/company.go:160 +#: pkg/company.go:271 pkg/contacts.go:361 +msgid "Selected country is not valid." +msgstr "Habéis escogido un país que no es válido." + +#: pkg/company.go:275 pkg/contacts.go:364 +msgid "Business name can not be empty." +msgstr "No podéis dejar el nombre y los apellidos en blanco." + +#: pkg/company.go:276 pkg/contacts.go:365 +msgid "Business name must have at least two letters." +msgstr "El nombre y los apellidos deben contener como mínimo dos letras." + +#: pkg/company.go:277 pkg/contacts.go:366 +msgid "VAT number can not be empty." +msgstr "No podéis dejar el DNI o NIF en blanco." + +#: pkg/company.go:278 pkg/contacts.go:367 +msgid "This value is not a valid VAT number." +msgstr "Este valor no es un DNI o NIF válido." + +#: pkg/company.go:280 +msgid "Phone can not be empty." +msgstr "No podéis dejar el teléfono en blanco." + +#: pkg/company.go:281 pkg/contacts.go:382 +msgid "This value is not a valid phone number." +msgstr "Este valor no es un teléfono válido." + +#: pkg/company.go:287 pkg/contacts.go:388 +msgid "This value is not a valid web address. It should be like https://domain.com/." +msgstr "Este valor no es una dirección web válida. Tiene que ser parecida a https://dominio.es/." + +#: pkg/company.go:289 pkg/contacts.go:369 +msgid "Address can not be empty." +msgstr "No podéis dejar la dirección en blanco." + +#: pkg/company.go:290 pkg/contacts.go:370 +msgid "City can not be empty." +msgstr "No podéis dejar la población en blanco." + +#: pkg/company.go:291 pkg/contacts.go:371 +msgid "Province can not be empty." +msgstr "No podéis dejar la provincia en blanco." + +#: pkg/company.go:292 pkg/contacts.go:373 +msgid "Postal code can not be empty." +msgstr "No podéis dejar el código postal en blanco." + +#: pkg/company.go:293 pkg/contacts.go:374 +msgid "This value is not a valid postal code." +msgstr "Este valor no es un código postal válido válido." + +#: pkg/company.go:295 msgid "Selected currency is not valid." msgstr "Habéis escogido una moneda que no es válida." -#: pkg/company.go:161 +#: pkg/company.go:296 msgid "Invoice number format can not be empty." msgstr "No podéis dejar el formato del número de factura en blanco." -#: pkg/company.go:162 +#: pkg/company.go:297 msgid "Next invoice number must be a number greater than zero." msgstr "El siguiente número de factura tiene que ser un número mayor a cero." -#: pkg/company.go:163 +#: pkg/company.go:298 msgid "Quotation number format can not be empty." msgstr "No podéis dejar el formato del número de presupuesto en blanco." -#: pkg/company.go:164 +#: pkg/company.go:299 msgid "Next quotation number must be a number greater than zero." msgstr "El siguiente número de presupuesto tiene que ser un número mayor a cero." -#: pkg/company.go:427 +#: pkg/company.go:563 msgctxt "input" msgid "Tax name" msgstr "Nombre impuesto" -#: pkg/company.go:433 +#: pkg/company.go:569 msgctxt "input" msgid "Tax Class" msgstr "Clase de impuesto" -#: pkg/company.go:436 +#: pkg/company.go:572 msgid "Select a tax class" msgstr "Escoged una clase de impuesto" -#: pkg/company.go:440 +#: pkg/company.go:576 msgctxt "input" msgid "Rate (%)" msgstr "Porcentaje" -#: pkg/company.go:463 +#: pkg/company.go:599 msgid "Tax name can not be empty." msgstr "No podéis dejar el nombre del impuesto en blanco." -#: pkg/company.go:464 +#: pkg/company.go:600 msgid "Selected tax class is not valid." msgstr "Habéis escogido una clase impuesto que no es válida." -#: pkg/company.go:465 +#: pkg/company.go:601 msgid "Tax rate can not be empty." msgstr "No podéis dejar el porcentaje en blanco." -#: pkg/company.go:466 +#: pkg/company.go:602 msgid "Tax rate must be an integer between -99 and 99." msgstr "El porcentaje tiene que estar entre -99 y 99." -#: pkg/company.go:529 +#: pkg/company.go:665 msgctxt "input" msgid "Payment method name" msgstr "Nombre del método de pago" -#: pkg/company.go:535 +#: pkg/company.go:671 msgctxt "input" msgid "Instructions" msgstr "Instrucciones" -#: pkg/company.go:553 +#: pkg/company.go:689 msgid "Payment method name can not be empty." msgstr "No podéis dejar el nombre del método de pago en blanco." -#: pkg/company.go:554 +#: pkg/company.go:690 msgid "Payment instructions can not be empty." msgstr "No podéis dejar las instrucciones de pago en blanco." -#: pkg/quote.go:147 pkg/quote.go:608 pkg/invoices.go:147 pkg/invoices.go:606 +#: pkg/quote.go:147 pkg/quote.go:608 pkg/invoices.go:147 pkg/invoices.go:640 msgctxt "input" msgid "Customer" msgstr "Cliente" @@ -901,8 +1004,8 @@ msgstr "Los presupuestos deben tener como mínimo una de las etiquetas." msgid "quotations.zip" msgstr "presupuestos.zip" -#: pkg/quote.go:556 pkg/quote.go:1085 pkg/quote.go:1093 pkg/invoices.go:555 -#: pkg/invoices.go:1145 pkg/invoices.go:1153 +#: pkg/quote.go:556 pkg/quote.go:1085 pkg/quote.go:1093 pkg/invoices.go:589 +#: pkg/invoices.go:1183 pkg/invoices.go:1191 msgid "Invalid action" msgstr "Acción inválida." @@ -920,12 +1023,12 @@ msgctxt "input" msgid "Terms and conditions" msgstr "Condiciones de aceptación" -#: pkg/quote.go:625 pkg/invoices.go:618 +#: pkg/quote.go:625 pkg/invoices.go:652 msgctxt "input" msgid "Notes" msgstr "Notas" -#: pkg/quote.go:634 pkg/invoices.go:628 +#: pkg/quote.go:634 pkg/invoices.go:662 msgctxt "input" msgid "Payment Method" msgstr "Método de pago" @@ -938,7 +1041,7 @@ msgstr "Escoged un método e pago." msgid "Selected quotation status is not valid." msgstr "Habéis escogido un estado de presupuesto que no es válido." -#: pkg/quote.go:673 pkg/invoices.go:665 +#: pkg/quote.go:673 pkg/invoices.go:699 msgid "Selected customer is not valid." msgstr "Habéis escogido un cliente que no es válido." @@ -950,21 +1053,21 @@ msgstr "No podéis dejar la fecha del presupuesto en blanco." msgid "Quotation date must be a valid date." msgstr "La fecha de presupuesto debe ser válida." -#: pkg/quote.go:679 pkg/invoices.go:669 +#: pkg/quote.go:679 pkg/invoices.go:703 msgid "Selected payment method is not valid." msgstr "Habéis escogido un método de pago que no es válido." -#: pkg/quote.go:813 pkg/quote.go:818 pkg/invoices.go:861 pkg/invoices.go:866 +#: pkg/quote.go:813 pkg/quote.go:818 pkg/invoices.go:899 pkg/invoices.go:904 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/quote.go:851 pkg/invoices.go:899 +#: pkg/quote.go:851 pkg/invoices.go:937 msgctxt "input" msgid "Quantity" msgstr "Cantidad" -#: pkg/quote.go:860 pkg/invoices.go:908 +#: pkg/quote.go:860 pkg/invoices.go:946 msgctxt "input" msgid "Discount (%)" msgstr "Descuento (%)" @@ -973,23 +1076,23 @@ msgstr "Descuento (%)" msgid "Quotation product ID must be a number greater than zero." msgstr "El ID de producto de presupuesto tiene que ser un número mayor a cero." -#: pkg/quote.go:917 pkg/invoices.go:965 +#: pkg/quote.go:917 pkg/invoices.go:1003 msgid "Product ID must be a positive number or zero." msgstr "El ID de producto tiene que ser un número positivo o cero." -#: pkg/quote.go:923 pkg/invoices.go:971 +#: pkg/quote.go:923 pkg/invoices.go:1009 msgid "Quantity can not be empty." msgstr "No podéis dejar la cantidad en blanco." -#: pkg/quote.go:924 pkg/invoices.go:972 +#: pkg/quote.go:924 pkg/invoices.go:1010 msgid "Quantity must be a number greater than zero." msgstr "La cantidad tiene que ser un número mayor a cero." -#: pkg/quote.go:926 pkg/invoices.go:974 +#: pkg/quote.go:926 pkg/invoices.go:1012 msgid "Discount can not be empty." msgstr "No podéis dejar el descuento en blanco." -#: pkg/quote.go:927 pkg/invoices.go:975 +#: pkg/quote.go:927 pkg/invoices.go:1013 msgid "Discount must be a percentage between 0 and 100." msgstr "El descuento tiene que ser un porcentaje entre 0 y 100." @@ -1070,7 +1173,7 @@ msgctxt "input" msgid "Invoice number" msgstr "Número de factura" -#: pkg/expenses.go:161 pkg/invoices.go:612 +#: pkg/expenses.go:161 pkg/invoices.go:646 msgctxt "input" msgid "Invoice Date" msgstr "Fecha de factura" @@ -1089,7 +1192,7 @@ msgstr "Archivo" msgid "Selected contact is not valid." msgstr "Habéis escogido un contacto que no es válido." -#: pkg/expenses.go:212 pkg/invoices.go:667 +#: pkg/expenses.go:212 pkg/invoices.go:701 msgid "Invoice date must be a valid date." msgstr "La fecha de factura debe ser válida." @@ -1110,142 +1213,49 @@ msgctxt "input" msgid "Invoice Number" msgstr "Número de factura" -#: pkg/invoices.go:153 pkg/invoices.go:600 +#: pkg/invoices.go:153 pkg/invoices.go:634 msgctxt "input" msgid "Invoice Status" msgstr "Estado de la factura" -#: pkg/invoices.go:448 +#: pkg/invoices.go:482 msgid "Select a customer to bill." msgstr "Escoged un cliente a facturar." -#: pkg/invoices.go:549 +#: pkg/invoices.go:583 msgid "invoices.zip" msgstr "facturas.zip" -#: pkg/invoices.go:664 +#: pkg/invoices.go:698 msgid "Selected invoice status is not valid." msgstr "Habéis escogido un estado de factura que no es válido." -#: pkg/invoices.go:666 +#: pkg/invoices.go:700 msgid "Invoice date can not be empty." msgstr "No podéis dejar la fecha de la factura en blanco." -#: pkg/invoices.go:802 +#: pkg/invoices.go:836 #, c-format msgid "Re: quotation #%s of %s" msgstr "Ref: presupuesto n.º %s del %s" -#: pkg/invoices.go:803 +#: pkg/invoices.go:837 msgctxt "to_char" msgid "MM/DD/YYYY" msgstr "DD/MM/YYYY" -#: pkg/invoices.go:962 +#: pkg/invoices.go:1000 msgid "Invoice product ID must be a number greater than zero." msgstr "El ID de producto de factura tiene que ser un número mayor a cero." -#: pkg/contacts.go:238 +#: pkg/contacts.go:271 msgctxt "input" -msgid "Business name" -msgstr "Nombre y apellidos" +msgid "Need to input tax details" +msgstr "Necesito facturar este contacto" -#: pkg/contacts.go:248 -msgctxt "input" -msgid "VAT number" -msgstr "DNI / NIF" - -#: pkg/contacts.go:254 -msgctxt "input" -msgid "Trade name" -msgstr "Nombre comercial" - -#: pkg/contacts.go:259 -msgctxt "input" -msgid "Phone" -msgstr "Teléfono" - -#: pkg/contacts.go:277 -msgctxt "input" -msgid "Web" -msgstr "Web" - -#: pkg/contacts.go:285 -msgctxt "input" -msgid "Address" -msgstr "Dirección" - -#: pkg/contacts.go:294 -msgctxt "input" -msgid "City" -msgstr "Población" - -#: pkg/contacts.go:300 -msgctxt "input" -msgid "Province" -msgstr "Provincia" - -#: pkg/contacts.go:306 -msgctxt "input" -msgid "Postal code" -msgstr "Código postal" - -#: pkg/contacts.go:315 -msgctxt "input" -msgid "Country" -msgstr "País" - -#: pkg/contacts.go:353 -msgid "Selected country is not valid." -msgstr "Habéis escogido un país que no es válido." - -#: pkg/contacts.go:357 -msgid "Business name can not be empty." -msgstr "No podéis dejar el nombre y los apellidos en blanco." - -#: pkg/contacts.go:358 -msgid "Business name must have at least two letters." -msgstr "El nombre y los apellidos deben contener como mínimo dos letras." - -#: pkg/contacts.go:359 -msgid "VAT number can not be empty." -msgstr "No podéis dejar el DNI o NIF en blanco." - -#: pkg/contacts.go:360 -msgid "This value is not a valid VAT number." -msgstr "Este valor no es un DNI o NIF válido." - -#: pkg/contacts.go:362 -msgid "Phone can not be empty." -msgstr "No podéis dejar el teléfono en blanco." - -#: pkg/contacts.go:363 -msgid "This value is not a valid phone number." -msgstr "Este valor no es un teléfono válido." - -#: pkg/contacts.go:369 -msgid "This value is not a valid web address. It should be like https://domain.com/." -msgstr "Este valor no es una dirección web válida. Tiene que ser parecida a https://dominio.es/." - -#: pkg/contacts.go:371 -msgid "Address can not be empty." -msgstr "No podéis dejar la dirección en blanco." - -#: pkg/contacts.go:372 -msgid "City can not be empty." -msgstr "No podéis dejar la población en blanco." - -#: pkg/contacts.go:373 -msgid "Province can not be empty." -msgstr "No podéis dejar la provincia en blanco." - -#: pkg/contacts.go:374 -msgid "Postal code can not be empty." -msgstr "No podéis dejar el código postal en blanco." - -#: pkg/contacts.go:375 -msgid "This value is not a valid postal code." -msgstr "Este valor no es un código postal válido válido." +#: pkg/contacts.go:379 +msgid "Name must have at least two letters." +msgstr "El nombre debe contener como mínimo dos letras." #~ msgctxt "action" #~ msgid "Update contact" diff --git a/revert/add_contact.sql b/revert/add_contact.sql index 9437949..6a48fca 100644 --- a/revert/add_contact.sql +++ b/revert/add_contact.sql @@ -1,7 +1,39 @@ --- Revert numerus:add_contact from pg +-- Deploy numerus:add_contact to pg +-- requires: schema_numerus +-- requires: extension_vat +-- requires: email +-- requires: extension_pg_libphonenumber +-- requires: extension_uri +-- requires: country_code +-- requires: contact +-- requires: tag_name begin; -drop function if exists numerus.add_contact(integer, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[]); +set search_path to numerus, public; + +create or replace function add_contact(company_id integer, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as +$$ +declare + cid integer; + cslug uuid; +begin + insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, tags) + values (add_contact.company_id, add_contact.business_name, (add_contact.country_code || add_contact.vatin)::vatin, add_contact.trade_name, parse_packed_phone_number(add_contact.phone, add_contact.country_code), add_contact.email, add_contact.web, add_contact.address, add_contact.city, add_contact.province, add_contact.postal_code, add_contact.country_code, add_contact.tags) + returning contact_id, slug + into cid, cslug; + + return cslug; +end +$$ + language plpgsql +; + +revoke execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public; +grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer; +grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin; + + +drop function if exists add_contact(integer, text, text, email, uri, tax_details, tag_name[]); commit; diff --git a/revert/add_contact@v0.sql b/revert/add_contact@v0.sql new file mode 100644 index 0000000..9437949 --- /dev/null +++ b/revert/add_contact@v0.sql @@ -0,0 +1,7 @@ +-- Revert numerus:add_contact from pg + +begin; + +drop function if exists numerus.add_contact(integer, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[]); + +commit; diff --git a/revert/contact_email.sql b/revert/contact_email.sql new file mode 100644 index 0000000..9fa2a1a --- /dev/null +++ b/revert/contact_email.sql @@ -0,0 +1,23 @@ +-- Revert numerus:contact_email from pg + +begin; + +set search_path to numerus, public; + +alter table contact + add column email email +; + +update contact +set email = email.email +from contact_email as email +where email.contact_id = email.contact_id +; + +alter table contact + alter column email set not null +; + +drop table if exists contact_email; + +commit; diff --git a/revert/contact_phone.sql b/revert/contact_phone.sql new file mode 100644 index 0000000..59205de --- /dev/null +++ b/revert/contact_phone.sql @@ -0,0 +1,23 @@ +-- Revert numerus:contact_phone from pg + +begin; + +set search_path to numerus, public; + +alter table contact + add column phone packed_phone_number +; + +update contact +set phone = phone.phone +from contact_phone as phone +where phone.contact_id = contact.contact_id +; + +alter table contact + alter column phone set not null +; + +drop table if exists contact_phone; + +commit; diff --git a/revert/contact_tax_details.sql b/revert/contact_tax_details.sql new file mode 100644 index 0000000..30e3adf --- /dev/null +++ b/revert/contact_tax_details.sql @@ -0,0 +1,51 @@ +-- Revert numerus:contact_tax_details from pg + +begin; + +set search_path to numerus, public; + +alter table contact + drop constraint name_not_empty +, add column country_code country_code +, add column postal_code text +, add column province text +, add column city text +, add column address text +, add column vatin vatin +, add column business_name text constraint business_name_not_empty check(length(trim(business_name)) > 1) +; + +alter table contact + rename column name to trade_name +; + +update contact +set business_name = tax.business_name + , vatin = tax.vatin + , address = tax.address + , city = tax.city + , province = tax.province + , postal_code = tax.postal_code + , country_code = tax.country_code +from contact_tax_details as tax +where tax.contact_id = contact.contact_id +; + +alter table contact + alter column business_name set not null +, alter column vatin set not null +, alter column address set not null +, alter column city set not null +, alter column province set not null +, alter column postal_code set not null +, alter column country_code set not null +; + +update contact +set trade_name = '' +where trade_name = business_name +; + +drop table if exists contact_tax_details; + +commit; diff --git a/revert/contact_web.sql b/revert/contact_web.sql new file mode 100644 index 0000000..3a63784 --- /dev/null +++ b/revert/contact_web.sql @@ -0,0 +1,23 @@ +-- Revert numerus:contact_web from pg + +begin; + +set search_path to numerus, public; + +alter table contact + add column web uri +; + +update contact +set web = web.uri +from contact_web as web +where web.contact_id = contact.contact_id +; + +alter table contact + alter column web set not null +; + +drop table if exists numerus.contact_web; + +commit; diff --git a/revert/edit_contact.sql b/revert/edit_contact.sql index e17b325..6b6b9dc 100644 --- a/revert/edit_contact.sql +++ b/revert/edit_contact.sql @@ -1,7 +1,55 @@ --- Revert numerus:edit_contact from pg +-- Deploy numerus:edit_contact to pg +-- requires: schema_numerus +-- requires: email +-- requires: extension_uri +-- requires: country_code +-- requires: tag_name +-- requires: contact +-- requires: extension_vat +-- requires: extension_pg_libphonenumber begin; -drop function if exists numerus.edit_contact(uuid, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[]); +set search_path to numerus, public; + +create or replace function edit_contact(contact_slug uuid, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as +$$ +declare + cid integer; + company integer; +begin + update contact + set business_name = edit_contact.business_name + , vatin = (edit_contact.country_code || edit_contact.vatin)::vatin + , trade_name = edit_contact.trade_name + , phone = parse_packed_phone_number( edit_contact.phone, edit_contact.country_code) + , email = edit_contact.email + , web = edit_contact.web + , address = edit_contact.address + , city = edit_contact.city + , province = edit_contact.province + , postal_code = edit_contact.postal_code + , country_code = edit_contact.country_code + , tags = edit_contact.tags + where slug = contact_slug + returning contact_id, company_id + into cid, company + ; + + if cid is null then + return null; + end if; + + return contact_slug; +end +$$ + language plpgsql +; + +revoke execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public; +grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer; +grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin; + +drop function if exists edit_contact(uuid, text, text, email, uri, tax_details, tag_name[]); commit; diff --git a/revert/edit_contact@v0.sql b/revert/edit_contact@v0.sql new file mode 100644 index 0000000..e17b325 --- /dev/null +++ b/revert/edit_contact@v0.sql @@ -0,0 +1,7 @@ +-- Revert numerus:edit_contact from pg + +begin; + +drop function if exists numerus.edit_contact(uuid, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[]); + +commit; diff --git a/revert/invoice_contact_id_fkey.sql b/revert/invoice_contact_id_fkey.sql new file mode 100644 index 0000000..e3c4425 --- /dev/null +++ b/revert/invoice_contact_id_fkey.sql @@ -0,0 +1,10 @@ +-- Revert numerus:invoice_contact_id_fkey from pg + +begin; + +alter table numerus.invoice + drop constraint invoice_contact_id_fkey +, add foreign key (contact_id) references numerus.contact (contact_id) +; + +commit; diff --git a/revert/tax_details.sql b/revert/tax_details.sql new file mode 100644 index 0000000..221f086 --- /dev/null +++ b/revert/tax_details.sql @@ -0,0 +1,7 @@ +-- Revert numerus:tax_details from pg + +begin; + +drop type if exists numerus.tax_details; + +commit; diff --git a/sqitch.plan b/sqitch.plan index b876de2..1b31987 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -98,3 +98,12 @@ compute_new_quote_amount [roles schema_numerus company tax new_quote_product new edited_quote_product [schema_numerus discount_rate] 2023-06-07T13:03:23Z jordi fita mas # Add type for passing products to edit quotations edit_quote [roles schema_numerus quote currency parse_price edited_quote_product tax quote_contact quote_payment_method quote_product quote_product_tax quote_product_product tag_name] 2023-06-07T13:08:10Z jordi fita mas # Add function to edit quotations @v0 2023-06-12T14:05:34Z jordi fita mas # Tag version 0 + +contact_phone [roles schema_numerus extension_pg_libphonenumber] 2023-06-28T11:04:19Z jordi fita mas # Add relation to keep contacts’ phone numbers +contact_email [roles schema_numerus email contact] 2023-06-28T11:47:19Z jordi fita mas # Add relation to keep contacts’ emails +contact_web [roles schema_numerus extension_uri contact] 2023-06-28T12:01:07Z jordi fita mas # Add relation to keep contacts’ websites +contact_tax_details [roles schema_numerus contact extension_vat country_code country] 2023-06-23T09:14:03Z jordi fita mas # Add relation of contact’s tax details +tax_details [schema_numerus extension_vat country_code] 2023-06-29T10:57:57Z jordi fita mas # Add composite type for contacts’ tax details +add_contact [add_contact@v0 tax_details] 2023-06-29T11:10:15Z jordi fita mas # Change add contact to accept a tax_detail parameter and use the new relations +edit_contact [edit_contact@v0 tax_details] 2023-06-29T11:50:41Z jordi fita mas # Change edit_contact to require tax_details parameter and to use new relations for web, email, and phone +invoice_contact_id_fkey [schema_numerus invoice contact_tax_details] 2023-06-30T16:50:45Z jordi fita mas # Update invoice’s contact_id foreign key to point to tax sales diff --git a/test/add_contact.sql b/test/add_contact.sql index 056bd85..88a9fa3 100644 --- a/test/add_contact.sql +++ b/test/add_contact.sql @@ -5,19 +5,19 @@ reset client_min_messages; begin; -select plan(14); +select plan(18); set search_path to auth, numerus, public; -select has_function('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']); -select function_lang_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'plpgsql'); -select function_returns('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'uuid'); -select isnt_definer('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']); -select volatility_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'volatile'); -select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'guest', array []::text[]); -select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'invoicer', array ['EXECUTE']); -select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'admin', array ['EXECUTE']); -select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'authenticator', array []::text[]); +select has_function('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]']); +select function_lang_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'plpgsql'); +select function_returns('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'uuid'); +select isnt_definer('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]']); +select volatility_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'volatile'); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'guest', array []::text[]); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'authenticator', array []::text[]); set client_min_messages to warning; @@ -41,35 +41,67 @@ values (111, 1, 'cash', 'cash') set constraints "company_default_payment_method_id_fkey" immediate; select lives_ok( - $$ select add_contact(1, 'Contact 2.1', '40404040D', 'Trade Contact 2.1', '777-777-777', 'c@c', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '17486', 'ES', '{tag1,tag2}') $$, - 'Should be able to insert a contact for the first company with two tags' + $$ select add_contact(1, 'Contact 2.1', '777-777-777', null, 'https://c', null, '{tag1,tag2}') $$, + 'Should be able to insert a contact for the first company with two tags, no email, and no tax details' ); select lives_ok( - $$ select add_contact(1, 'Contact 2.2', '41414141L', 'Trade Contact 2.2', '888-888-888', 'd@d', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17487', 'ES', '{}') $$, - 'Should be able to insert a second contact for the first company with no tag' + $$ select add_contact(1, 'Contact 2.2', null, 'd@d', null, '(Contact 2.2 Ltd,41414141L,"Fake St., 123",City 2.2,Province 2.2,17487,ES)', '{}') $$, + 'Should be able to insert a second contact for the first company with no tag, no phone, and not website' ); select lives_ok( - $$ select add_contact(2, 'Contact 4.1', '42424242Y', '', '999-999-999', 'e@e', '', 'Yet Another Fake St., 123', 'City 4.1', 'Province 4.1', '17488', 'ES', '{tag2}') $$, - 'Should be able to insert a contact for the second company with a tag' + $$ select add_contact(2, 'Contact 4.1', '999-999-999', 'e@e', 'http://e', '(Contact 4.1 Ltd,42424242Y,"Another Fake St., 123",City 4.1,Province 4.1,17488,ES)', '{tag2}') $$, + 'Should be able to insert a contact for the second company with a tag and everything else' ); select lives_ok( - $$ select add_contact(1, 'Contact 2.3', '43434343Q', '', '000-000-000', 'f@f', '', 'The Last Fake St., 123', '', '', '', 'ES', '{tag2}') $$, + $$ select add_contact(1, 'Contact 2.3', null, null, null, null, '{tag2}') $$, 'Should be able to insert another contact with a repeated tag' ); select bag_eq( - $$ select company_id, business_name, vatin::text, trade_name, phone::text, email::text, web::text, address, city, province, postal_code, country_code::text, tags, created_at from contact $$, - $$ values (1, 'Contact 2.1', 'ES40404040D', 'Trade Contact 2.1', '+34 777 77 77 77', 'c@c', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '17486', 'ES', '{tag1,tag2}'::tag_name[], CURRENT_TIMESTAMP) - , (1, 'Contact 2.2', 'ES41414141L', 'Trade Contact 2.2', '+34 888 88 88 88', 'd@d', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17487', 'ES', '{}'::tag_name[], CURRENT_TIMESTAMP) - , (2, 'Contact 4.1', 'ES42424242Y', '', '+34 999 99 99 99', 'e@e', '', 'Yet Another Fake St., 123', 'City 4.1', 'Province 4.1', '17488', 'ES', '{tag2}'::tag_name[], CURRENT_TIMESTAMP) - , (1, 'Contact 2.3', 'ES43434343Q', '', '+34 000000000', 'f@f', '', 'The Last Fake St., 123', '', '', '', 'ES', '{tag2}'::tag_name[], CURRENT_TIMESTAMP) + $$ select company_id, name, tags, created_at from contact $$, + $$ values (1, 'Contact 2.1', '{tag1,tag2}'::tag_name[], CURRENT_TIMESTAMP) + , (1, 'Contact 2.2', '{}'::tag_name[], CURRENT_TIMESTAMP) + , (2, 'Contact 4.1', '{tag2}'::tag_name[], CURRENT_TIMESTAMP) + , (1, 'Contact 2.3', '{tag2}'::tag_name[], CURRENT_TIMESTAMP) $$, 'Should have created all contacts' ); +select bag_eq( + $$ select name, business_name, vatin::text, address, city, province, postal_code, country_code::text from contact join contact_tax_details using (contact_id) $$, + $$ values ('Contact 2.2', 'Contact 2.2 Ltd', 'ES41414141L', 'Fake St., 123', 'City 2.2', 'Province 2.2', '17487', 'ES') + , ('Contact 4.1', 'Contact 4.1 Ltd', 'ES42424242Y', 'Another Fake St., 123', 'City 4.1', 'Province 4.1', '17488', 'ES') + $$, + 'Should have created all contacts’ tax details' +); + +select bag_eq( + $$ select name, phone::text from contact join contact_phone using (contact_id) $$, + $$ values ('Contact 2.1', '+34 777 77 77 77') + , ('Contact 4.1', '+34 999 99 99 99') + $$, + 'Should have created all contacts’ phone' +); + +select bag_eq( + $$ select name, email::text from contact join contact_email using (contact_id) $$, + $$ values ('Contact 2.2', 'd@d') + , ('Contact 4.1', 'e@e') + $$, + 'Should have created all contacts’ email' +); + +select bag_eq( + $$ select name, uri::text from contact join contact_web using (contact_id) $$, + $$ values ('Contact 2.1', 'https://c') + , ('Contact 4.1', 'http://e') + $$, + 'Should have created all contacts’ web' +); + select * from finish(); diff --git a/test/add_expense.sql b/test/add_expense.sql index 9235cd4..da75fb0 100644 --- a/test/add_expense.sql +++ b/test/add_expense.sql @@ -55,11 +55,11 @@ values (3, 1, 11, 'IRPF -15 %', -0.15) , (6, 2, 22, 'IVA 10 %', 0.10) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') - , (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') - , (14, 2, 'Contact 4.1', 'XX777', '', '999-999-999', 'e@e', '', '', '', '', '', 'ES') - , (15, 2, 'Contact 4.2', 'XX888', '', '000-000-000', 'f@f', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (12, 1, 'Contact 2.1') + , (13, 1, 'Contact 2.2') + , (14, 2, 'Contact 4.1') + , (15, 2, 'Contact 4.2') ; diff --git a/test/add_invoice.sql b/test/add_invoice.sql index 0cdd983..eae5fd6 100644 --- a/test/add_invoice.sql +++ b/test/add_invoice.sql @@ -25,6 +25,7 @@ truncate invoice_number_counter cascade; truncate invoice_product_tax cascade; truncate invoice_product cascade; truncate invoice cascade; +truncate contact_tax_details cascade; truncate contact cascade; truncate product cascade; truncate tax cascade; @@ -72,11 +73,18 @@ values ( 7, 1, 'Product 2.1', 1212) , (11, 2, 'Product 4.3', 1010) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') - , (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') - , (14, 2, 'Contact 4.1', 'XX777', '', '999-999-999', 'e@e', '', '', '', '', '', 'ES') - , (15, 2, 'Contact 4.2', 'XX888', '', '000-000-000', 'f@f', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (12, 1, 'Contact 2.1') + , (13, 1, 'Contact 2.2') + , (14, 2, 'Contact 4.1') + , (15, 2, 'Contact 4.2') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values (12, 'Contact 2.1', 'XX555', '', '', '', '', 'ES') + , (13, 'Contact 2.2', 'XX666', '', '', '', '', 'ES') + , (14, 'Contact 4.1', 'XX777', '', '', '', '', 'ES') + , (15, 'Contact 4.2', 'XX888', '', '', '', '', 'ES') ; diff --git a/test/add_quote.sql b/test/add_quote.sql index a2576a9..45500ba 100644 --- a/test/add_quote.sql +++ b/test/add_quote.sql @@ -74,11 +74,11 @@ values ( 7, 1, 'Product 2.1', 1212) , (11, 2, 'Product 4.3', 1010) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') - , (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') - , (14, 2, 'Contact 4.1', 'XX777', '', '999-999-999', 'e@e', '', '', '', '', '', 'ES') - , (15, 2, 'Contact 4.2', 'XX888', '', '000-000-000', 'f@f', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (12, 1, 'Contact 2.1') + , (13, 1, 'Contact 2.2') + , (14, 2, 'Contact 4.1') + , (15, 2, 'Contact 4.2') ; diff --git a/test/attach_to_expense.sql b/test/attach_to_expense.sql index 8873f61..53ca5a7 100644 --- a/test/attach_to_expense.sql +++ b/test/attach_to_expense.sql @@ -53,9 +53,9 @@ values (3, 1, 11, 'IRPF -15 %', -0.15) , (4, 1, 11, 'IVA 21 %', 0.21) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') - , (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (12, 1, 'Contact 2.1') + , (13, 1, 'Contact 2.2') ; insert into expense (expense_id, company_id, slug, invoice_number, invoice_date, contact_id, amount, currency_code, tags) diff --git a/test/contact.sql b/test/contact.sql index d8cada8..719e89d 100644 --- a/test/contact.sql +++ b/test/contact.sql @@ -5,7 +5,7 @@ reset client_min_messages; begin; -select plan(90); +select plan(48); set search_path to numerus, auth, public; @@ -43,62 +43,10 @@ select col_not_null('contact', 'slug'); select col_has_default('contact', 'slug'); select col_default_is('contact', 'slug', 'gen_random_uuid()'); -select has_column('contact', 'business_name'); -select col_type_is('contact', 'business_name', 'text'); -select col_not_null('contact', 'business_name'); -select col_hasnt_default('contact', 'business_name'); - -select has_column('contact', 'vatin'); -select col_type_is('contact', 'vatin', 'vatin'); -select col_not_null('contact', 'vatin'); -select col_hasnt_default('contact', 'vatin'); - -select has_column('contact', 'trade_name'); -select col_type_is('contact', 'trade_name', 'text'); -select col_not_null('contact', 'trade_name'); -select col_hasnt_default('contact', 'trade_name'); - -select has_column('contact', 'phone'); -select col_type_is('contact', 'phone', 'packed_phone_number'); -select col_not_null('contact', 'phone'); -select col_hasnt_default('contact', 'phone'); - -select has_column('contact', 'email'); -select col_type_is('contact', 'email', 'email'); -select col_not_null('contact', 'email'); -select col_hasnt_default('contact', 'email'); - -select has_column('contact', 'web'); -select col_type_is('contact', 'web', 'uri'); -select col_not_null('contact', 'web'); -select col_hasnt_default('contact', 'web'); - -select has_column('contact', 'address'); -select col_type_is('contact', 'address', 'text'); -select col_not_null('contact', 'address'); -select col_hasnt_default('contact', 'address'); - -select has_column('contact', 'city'); -select col_type_is('contact', 'city', 'text'); -select col_not_null('contact', 'city'); -select col_hasnt_default('contact', 'city'); - -select has_column('contact', 'province'); -select col_type_is('contact', 'province', 'text'); -select col_not_null('contact', 'province'); -select col_hasnt_default('contact', 'province'); - -select has_column('contact', 'postal_code'); -select col_type_is('contact', 'postal_code', 'text'); -select col_not_null('contact', 'postal_code'); -select col_hasnt_default('contact', 'postal_code'); - -select has_column('contact', 'country_code'); -select col_type_is('contact', 'country_code', 'country_code'); -select col_is_fk('contact', 'country_code'); -select col_type_is('contact', 'country_code', 'country_code'); -select col_not_null('contact', 'country_code'); -select col_hasnt_default('contact', 'country_code'); +select has_column('contact', 'name'); +select col_type_is('contact', 'name', 'text'); +select col_not_null('contact', 'name'); +select col_hasnt_default('contact', 'name'); select has_column('contact', 'tags'); select col_type_is('contact', 'tags', 'tag_name[]'); @@ -144,15 +92,15 @@ values (2, 1) , (4, 5) ; -insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (2, 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') - , (4, 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') +insert into contact (company_id, name) +values (2, 'Contact 1') + , (4, 'Contact 2') ; prepare contact_data as -select company_id, business_name +select company_id, name from contact -order by company_id, business_name; +order by company_id, name; set role invoicer; select is_empty('contact_data', 'Should show no data when cookie is not set yet'); @@ -185,11 +133,11 @@ select throws_ok( reset role; select throws_ok( $$ - insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) - values (2, ' ', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES') + insert into contact (company_id, name) + values (2, ' ') $$, - '23514', 'new row for relation "contact" violates check constraint "business_name_not_empty"', - 'Should not allow contacts with blank business name' + '23514', 'new row for relation "contact" violates check constraint "name_not_empty"', + 'Should not allow contacts with blank trade name' ); diff --git a/test/contact_email.sql b/test/contact_email.sql new file mode 100644 index 0000000..04034ed --- /dev/null +++ b/test/contact_email.sql @@ -0,0 +1,119 @@ +-- Test contact_email +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(21); + +set search_path to numerus, auth, public; + +select has_table('contact_email'); +select has_pk('contact_email' ); +select table_privs_are('contact_email', 'guest', array []::text[]); +select table_privs_are('contact_email', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_email', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_email', 'authenticator', array []::text[]); + +select has_column('contact_email', 'contact_id'); +select col_is_pk('contact_email', 'contact_id'); +select col_is_fk('contact_email', 'contact_id'); +select fk_ok('contact_email', 'contact_id', 'contact', 'contact_id'); +select col_type_is('contact_email', 'contact_id', 'integer'); +select col_not_null('contact_email', 'contact_id'); +select col_hasnt_default('contact_email', 'contact_id'); + +select has_column('contact_email', 'email'); +select col_type_is('contact_email', 'email', 'email'); +select col_not_null('contact_email', 'email'); +select col_hasnt_default('contact_email', 'email'); + + + +set client_min_messages to warning; +truncate contact_email cascade; +truncate contact cascade; +truncate company_user cascade; +truncate payment_method cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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') +; + +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, name) +values (6, 2, 'C1') + , (8, 4, 'C2') + , (9, 4, 'C3') +; + +insert into contact_email (contact_id, email) +values (6, 'c@c') + , (8, 'd@d') +; + +prepare contact_data as +select company_id, email +from contact +join contact_email using (contact_id) +order by company_id, email; + +set role invoicer; +select is_empty('contact_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (2, 'c@c') + $$, + 'Should only list contacts of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (4, 'd@d') + $$, + 'Should only list contacts of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'contact_data', + '42501', 'permission denied for table contact', + 'Should not allow select to guest users' +); +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/contact_phone.sql b/test/contact_phone.sql new file mode 100644 index 0000000..82765b6 --- /dev/null +++ b/test/contact_phone.sql @@ -0,0 +1,118 @@ +-- Test contact_phone +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(21); + +set search_path to numerus, auth, public; + +select has_table('contact_phone'); +select has_pk('contact_phone' ); +select table_privs_are('contact_phone', 'guest', array []::text[]); +select table_privs_are('contact_phone', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_phone', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_phone', 'authenticator', array []::text[]); + +select has_column('contact_phone', 'contact_id'); +select col_is_pk('contact_phone', 'contact_id'); +select col_is_fk('contact_phone', 'contact_id'); +select fk_ok('contact_phone', 'contact_id', 'contact', 'contact_id'); +select col_type_is('contact_phone', 'contact_id', 'integer'); +select col_not_null('contact_phone', 'contact_id'); +select col_hasnt_default('contact_phone', 'contact_id'); + +select has_column('contact_phone', 'phone'); +select col_type_is('contact_phone', 'phone', 'packed_phone_number'); +select col_not_null('contact_phone', 'phone'); +select col_hasnt_default('contact_phone', 'phone'); + + +set client_min_messages to warning; +truncate contact_phone cascade; +truncate contact cascade; +truncate company_user cascade; +truncate payment_method cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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') +; + +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, name) +values (6, 2, 'C1') + , (8, 4, 'C2') + , (9, 4, 'C3') +; + +insert into contact_phone (contact_id, phone) +values (6, '777-777-777') + , (8, '888-888-888') +; + +prepare contact_data as +select company_id, phone +from contact +join contact_phone using (contact_id) +order by company_id, phone; + +set role invoicer; +select is_empty('contact_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (2, '777-777-777'::packed_phone_number) + $$, + 'Should only list contacts of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (4, '888-888-888'::packed_phone_number) + $$, + 'Should only list contacts of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'contact_data', + '42501', 'permission denied for table contact', + 'Should not allow select to guest users' +); +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/contact_tax_details.sql b/test/contact_tax_details.sql new file mode 100644 index 0000000..5bc2143 --- /dev/null +++ b/test/contact_tax_details.sql @@ -0,0 +1,157 @@ +-- Test contact_tax_details +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(48); + +set search_path to numerus, auth, public; + +select has_table('contact_tax_details'); +select has_pk('contact_tax_details' ); +select table_privs_are('contact_tax_details', 'guest', array []::text[]); +select table_privs_are('contact_tax_details', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_tax_details', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_tax_details', 'authenticator', array []::text[]); + +select has_column('contact_tax_details', 'contact_id'); +select col_is_pk('contact_tax_details', 'contact_id'); +select col_is_fk('contact_tax_details', 'contact_id'); +select fk_ok('contact_tax_details', 'contact_id', 'contact', 'contact_id'); +select col_type_is('contact_tax_details', 'contact_id', 'integer'); +select col_not_null('contact_tax_details', 'contact_id'); +select col_hasnt_default('contact_tax_details', 'contact_id'); + +select has_column('contact_tax_details', 'business_name'); +select col_type_is('contact_tax_details', 'business_name', 'text'); +select col_not_null('contact_tax_details', 'business_name'); +select col_hasnt_default('contact_tax_details', 'business_name'); + +select has_column('contact_tax_details', 'vatin'); +select col_type_is('contact_tax_details', 'vatin', 'vatin'); +select col_not_null('contact_tax_details', 'vatin'); +select col_hasnt_default('contact_tax_details', 'vatin'); + +select has_column('contact_tax_details', 'address'); +select col_type_is('contact_tax_details', 'address', 'text'); +select col_not_null('contact_tax_details', 'address'); +select col_hasnt_default('contact_tax_details', 'address'); + +select has_column('contact_tax_details', 'city'); +select col_type_is('contact_tax_details', 'city', 'text'); +select col_not_null('contact_tax_details', 'city'); +select col_hasnt_default('contact_tax_details', 'city'); + +select has_column('contact_tax_details', 'province'); +select col_type_is('contact_tax_details', 'province', 'text'); +select col_not_null('contact_tax_details', 'province'); +select col_hasnt_default('contact_tax_details', 'province'); + +select has_column('contact_tax_details', 'postal_code'); +select col_type_is('contact_tax_details', 'postal_code', 'text'); +select col_not_null('contact_tax_details', 'postal_code'); +select col_hasnt_default('contact_tax_details', 'postal_code'); + +select has_column('contact_tax_details', 'country_code'); +select col_is_fk('contact_tax_details', 'country_code'); +select col_type_is('contact_tax_details', 'country_code', 'country_code'); +select col_type_is('contact_tax_details', 'country_code', 'country_code'); +select col_not_null('contact_tax_details', 'country_code'); +select col_hasnt_default('contact_tax_details', 'country_code'); + + +set client_min_messages to warning; +truncate contact_tax_details cascade; +truncate contact cascade; +truncate company_user cascade; +truncate payment_method cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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') +; + +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, name) +values (6, 2, 'C1') + , (8, 4, 'C2') + , (9, 4, 'C3') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values (6, 'Contact 1', 'XX555', '', '', '', '', 'ES') + , (8, 'Contact 2', 'XX666', '', '', '', '', 'ES') +; + +prepare contact_data as +select company_id, business_name +from contact +join contact_tax_details using (contact_id) +order by company_id, business_name; + +set role invoicer; +select is_empty('contact_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (2, 'Contact 1') + $$, + 'Should only list contacts of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (4, 'Contact 2') + $$, + 'Should only list contacts of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'contact_data', + '42501', 'permission denied for table contact', + 'Should not allow select to guest users' +); +reset role; + +select throws_ok( $$ + insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) + values (9, ' ', 'XX123', '', '', '', '', 'ES') + $$, + '23514', 'new row for relation "contact_tax_details" violates check constraint "business_name_not_empty"', + 'Should not allow contacts with blank business name' +); + +select * +from finish(); + +rollback; + diff --git a/test/contact_web.sql b/test/contact_web.sql new file mode 100644 index 0000000..00aa735 --- /dev/null +++ b/test/contact_web.sql @@ -0,0 +1,118 @@ +-- Test contact_web +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(21); + +set search_path to numerus, auth, public; + +select has_table('contact_web'); +select has_pk('contact_web' ); +select table_privs_are('contact_web', 'guest', array []::text[]); +select table_privs_are('contact_web', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_web', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_web', 'authenticator', array []::text[]); + +select has_column('contact_web', 'contact_id'); +select col_is_pk('contact_web', 'contact_id'); +select col_is_fk('contact_web', 'contact_id'); +select fk_ok('contact_web', 'contact_id', 'contact', 'contact_id'); +select col_type_is('contact_web', 'contact_id', 'integer'); +select col_not_null('contact_web', 'contact_id'); +select col_hasnt_default('contact_web', 'contact_id'); + +select has_column('contact_web', 'uri'); +select col_type_is('contact_web', 'uri', 'uri'); +select col_not_null('contact_web', 'uri'); +select col_hasnt_default('contact_web', 'uri'); + + +set client_min_messages to warning; +truncate contact_web cascade; +truncate contact cascade; +truncate company_user cascade; +truncate payment_method cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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') +; + +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, name) +values (6, 2, 'C1') + , (8, 4, 'C2') + , (9, 4, 'C3') +; + +insert into contact_web (contact_id, uri) +values (6, 'http://rainforest.com/') + , (8, 'https://kiwi.com/') +; + +prepare contact_data as +select company_id, uri +from contact +join contact_web using (contact_id) +order by company_id, uri; + +set role invoicer; +select is_empty('contact_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (2, 'http://rainforest.com/'::uri) + $$, + 'Should only list contacts of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (4, 'https://kiwi.com/'::uri) + $$, + 'Should only list contacts of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'contact_data', + '42501', 'permission denied for table contact', + 'Should not allow select to guest users' +); +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/edit_contact.sql b/test/edit_contact.sql index bc86b38..73d61cd 100644 --- a/test/edit_contact.sql +++ b/test/edit_contact.sql @@ -5,19 +5,19 @@ reset client_min_messages; begin; -select plan(12); +select plan(17); set search_path to auth, numerus, public; -select has_function('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']); -select function_lang_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'plpgsql'); -select function_returns('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'uuid'); -select isnt_definer('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']); -select volatility_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'volatile'); -select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'guest', array []::text[]); -select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'invoicer', array ['EXECUTE']); -select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'admin', array ['EXECUTE']); -select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'authenticator', array []::text[]); +select has_function('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]']); +select function_lang_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'plpgsql'); +select function_returns('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'uuid'); +select isnt_definer('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]']); +select volatility_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'volatile'); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'guest', array []::text[]); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'authenticator', array []::text[]); set client_min_messages to warning; @@ -40,30 +40,89 @@ values (111, 1, 'cash', 'cash') set constraints "company_default_payment_method_id_fkey" immediate; -insert into contact (contact_id, company_id, slug, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, tags) -values (12, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES', '{tag1}') - , (13, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES', '{tag2}') +insert into contact (contact_id, company_id, slug, name, tags) +values (12, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 1', '{tag1}') + , (13, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2', '{tag2}') + , (14, 1, '12fd031b-8f4d-4ac1-9dde-7df336dc6d52', 'Contact 3', '{tag3}') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values (12, 'Contact 1 Ltd', 'XX555', '', '', '', '', 'ES') + , (13, 'Contact 2 Ltd', 'XX666', '', '', '', '', 'ES') +; + +insert into contact_phone (contact_id, phone) +values (12, '777-777-777') + , (13, '888-888-888') +; + +insert into contact_email (contact_id, email) +values (12, 'c@c') + , (13, 'd@d') +; + +insert into contact_web (contact_id, uri) +values (12, 'https://1/') + , (13, 'https://2/') ; select lives_ok( - $$ select edit_contact('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 2.1', '40404040D', 'Trade Contact 2.1', '999-999-999', 'c1@c1', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '19486', 'ES', array['tag1']) $$, + $$ select edit_contact('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 2.1', '999-999-999', 'c1@c1', 'https://c', '(Contact 2.1 Ltd,40404040D,"Fake St., 123",City 2.1,Province 2.1,19486,ES)', array['tag1']) $$, 'Should be able to edit the first contact' ); select lives_ok( - $$ select edit_contact('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2.2', '41414141L', 'Trade Contact 2.2', '111-111-111', 'd2@d2', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17417', 'ES', array['tag1', 'tag3']) $$, + $$ select edit_contact('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2.2', null, null, null, null, array['tag1', 'tag3']) $$, 'Should be able to edit the second contact' ); +select lives_ok( + $$ select edit_contact('12fd031b-8f4d-4ac1-9dde-7df336dc6d52', 'Contact 2.3', '111-111-111', 'd2@d2', 'https://d', '(Contact 2.3 Ltd,41414141L,"Another Fake St., 123",City 2.2,Province 2.2,17417,ES)', array['tag2']) $$, + 'Should be able to edit the third contact' +); + select bag_eq( - $$ select company_id, business_name, vatin::text, trade_name, phone::text, email::text, web::text, address, city, province, postal_code, country_code::text, tags, created_at from contact $$, - $$ values (1, 'Contact 2.1', 'ES40404040D', 'Trade Contact 2.1', '+34 999 99 99 99', 'c1@c1', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '19486', 'ES', '{tag1}'::tag_name[], CURRENT_TIMESTAMP) - , (1, 'Contact 2.2', 'ES41414141L', 'Trade Contact 2.2', '+34 111111111', 'd2@d2', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17417', 'ES', '{tag1,tag3}'::tag_name[], CURRENT_TIMESTAMP) + $$ select company_id, name, tags, created_at from contact $$, + $$ values (1, 'Contact 2.1', '{tag1}'::tag_name[], CURRENT_TIMESTAMP) + , (1, 'Contact 2.2', '{tag1,tag3}'::tag_name[], CURRENT_TIMESTAMP) + , (1, 'Contact 2.3', '{tag2}'::tag_name[], CURRENT_TIMESTAMP) $$, 'Should have updated all contacts' ); +select bag_eq( + $$ select name, business_name, vatin::text, address, city, province, postal_code, country_code::text from contact join contact_tax_details using (contact_id) $$, + $$ values ('Contact 2.1', 'Contact 2.1 Ltd', 'ES40404040D', 'Fake St., 123', 'City 2.1', 'Province 2.1', '19486', 'ES') + , ('Contact 2.3', 'Contact 2.3 Ltd', 'ES41414141L', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17417', 'ES') + $$, + 'Should have updated all contacts’ tax details' +); + +select bag_eq( + $$ select name, phone::text from contact join contact_phone using (contact_id) $$, + $$ values ('Contact 2.1', '+34 999 99 99 99') + , ('Contact 2.3', '+34 111111111') + $$, + 'Should have updated all contacts’ phone' +); + +select bag_eq( + $$ select name, email::text from contact join contact_email using (contact_id) $$, + $$ values ('Contact 2.1', 'c1@c1') + , ('Contact 2.3', 'd2@d2') + $$, + 'Should have updated all contacts’ email' +); + +select bag_eq( + $$ select name, uri::text from contact join contact_web using (contact_id) $$, + $$ values ('Contact 2.1', 'https://c') + , ('Contact 2.3', 'https://d') + $$, + 'Should have updated all contacts’ web' +); + select * from finish(); diff --git a/test/edit_expense.sql b/test/edit_expense.sql index 9dd02ed..8d33e1e 100644 --- a/test/edit_expense.sql +++ b/test/edit_expense.sql @@ -53,9 +53,9 @@ values (3, 1, 11, 'IRPF -15 %', -0.15) , (4, 1, 11, 'IVA 21 %', 0.21) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') - , (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (12, 1, 'Contact 2.1') + , (13, 1, 'Contact 2.2') ; insert into expense (expense_id, company_id, slug, invoice_number, invoice_date, contact_id, amount, currency_code, tags) diff --git a/test/edit_invoice.sql b/test/edit_invoice.sql index 538ada9..821bc75 100644 --- a/test/edit_invoice.sql +++ b/test/edit_invoice.sql @@ -24,6 +24,7 @@ set client_min_messages to warning; truncate invoice_product_tax cascade; truncate invoice_product cascade; truncate invoice cascade; +truncate contact_tax_details cascade; truncate contact cascade; truncate product cascade; truncate tax cascade; @@ -61,9 +62,14 @@ values ( 7, 1, 'Product 1.1', 1212) , ( 9, 1, 'Product 3.3', 3636) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') - , (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (12, 1, 'Contact 2.1') + , (13, 1, 'Contact 2.2') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values (12, 'Contact 2.1', 'XX555', '', '', '', '', 'ES') + , (13, 'Contact 2.2', 'XX666', '', '', '', '', 'ES') ; insert into invoice (invoice_id, company_id, slug, invoice_number, invoice_date, contact_id, payment_method_id, currency_code, tags) diff --git a/test/edit_quote.sql b/test/edit_quote.sql index 497b408..d004416 100644 --- a/test/edit_quote.sql +++ b/test/edit_quote.sql @@ -63,9 +63,9 @@ values ( 7, 1, 'Product 1.1', 1212) , ( 9, 1, 'Product 3.3', 3636) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') - , (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (12, 1, 'Contact 2.1') + , (13, 1, 'Contact 2.2') ; insert into quote (quote_id, company_id, slug, quote_number, quote_date, currency_code, tags) diff --git a/test/expense.sql b/test/expense.sql index e5434f1..f0e5768 100644 --- a/test/expense.sql +++ b/test/expense.sql @@ -118,9 +118,9 @@ 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 contact (contact_id, company_id, name) +values (6, 2, 'Contact 1') + , (8, 4, 'Contact 2') ; insert into expense (company_id, invoice_number, contact_id, invoice_date, amount, currency_code) diff --git a/test/expense_attachment.sql b/test/expense_attachment.sql index 8fc2fe3..7e9804b 100644 --- a/test/expense_attachment.sql +++ b/test/expense_attachment.sql @@ -87,9 +87,9 @@ values (3, 2, 22, 'IVA 21 %', 0.21) , (6, 4, 44, 'IVA 10 %', 0.10) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values ( 9, 2, 'Customer 1', 'XX555', '', '777-777-777', 'c1@e', '', '', '', '', '', 'ES') - , (10, 4, 'Customer 2', 'XX666', '', '888-888-888', 'c2@e', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values ( 9, 2, 'Customer 1') + , (10, 4, 'Customer 2') ; insert into expense (expense_id, company_id, invoice_number, contact_id, invoice_date, amount, currency_code) diff --git a/test/expense_tax.sql b/test/expense_tax.sql index 188fdf6..49346ac 100644 --- a/test/expense_tax.sql +++ b/test/expense_tax.sql @@ -84,9 +84,9 @@ values (3, 2, 22, 'IVA 21 %', 0.21) , (6, 4, 44, 'IVA 10 %', 0.10) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values ( 9, 2, 'Customer 1', 'XX555', '', '777-777-777', 'c1@e', '', '', '', '', '', 'ES') - , (10, 4, 'Customer 2', 'XX666', '', '888-888-888', 'c2@e', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values ( 9, 2, 'Customer 1') + , (10, 4, 'Customer 2') ; insert into expense (expense_id, company_id, invoice_number, contact_id, invoice_date, amount, currency_code) diff --git a/test/expense_tax_amount.sql b/test/expense_tax_amount.sql index 8c715a5..234e92f 100644 --- a/test/expense_tax_amount.sql +++ b/test/expense_tax_amount.sql @@ -58,8 +58,8 @@ values (2, 1, 11, 'IRPF -15 %', -0.15) , (5, 1, 11, 'IVA 21 %', 0.21) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (7, 1, 'Contact') ; insert into expense (expense_id, company_id, invoice_number, invoice_date, contact_id, amount, currency_code) diff --git a/test/invoice.sql b/test/invoice.sql index 5471884..5c118d6 100644 --- a/test/invoice.sql +++ b/test/invoice.sql @@ -56,7 +56,7 @@ select col_default_is('invoice', 'invoice_date', 'CURRENT_DATE'); select has_column('invoice', 'contact_id'); select col_is_fk('invoice', 'contact_id'); -select fk_ok('invoice', 'contact_id', 'contact', 'contact_id'); +select fk_ok('invoice', 'contact_id', 'contact_tax_details', 'contact_id'); select col_type_is('invoice', 'contact_id', 'integer'); select col_not_null('invoice', 'contact_id'); select col_hasnt_default('invoice', 'contact_id'); @@ -104,6 +104,7 @@ select col_default_is('invoice', 'created_at', 'CURRENT_TIMESTAMP'); set client_min_messages to warning; truncate invoice cascade; +truncate contact_tax_details cascade; truncate contact cascade; truncate company_user cascade; truncate company cascade; @@ -135,9 +136,14 @@ 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 contact (contact_id, company_id, name) +values (6, 2, 'Contact 1') + , (8, 4, 'Contact 2') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values (6, 'Contact 1', 'XX555', '', '', '', '', 'ES') + , (8, 'Contact 2', 'XX666', '', '', '', '', 'ES') ; insert into invoice (company_id, invoice_number, contact_id, currency_code, payment_method_id) diff --git a/test/invoice_amount.sql b/test/invoice_amount.sql index 3c12009..09bec1c 100644 --- a/test/invoice_amount.sql +++ b/test/invoice_amount.sql @@ -29,6 +29,7 @@ set client_min_messages to warning; truncate invoice_product_tax cascade; truncate invoice_product cascade; truncate invoice cascade; +truncate contact_tax_details cascade; truncate contact cascade; truncate tax cascade; truncate tax_class cascade; @@ -59,8 +60,12 @@ values (2, 1, 11, 'IRPF -15 %', -0.15) , (5, 1, 11, 'IVA 21 %', 0.21) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (7, 1, 'Contact') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values (7, 'Contact', 'XX555', '', '', '', '', 'ES') ; insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code, payment_method_id) diff --git a/test/invoice_product.sql b/test/invoice_product.sql index 2d1ba90..0e14d24 100644 --- a/test/invoice_product.sql +++ b/test/invoice_product.sql @@ -68,6 +68,7 @@ select col_default_is('invoice_product', 'discount_rate', '0.0'); set client_min_messages to warning; truncate invoice_product cascade; truncate invoice cascade; +truncate contact_tax_details cascade; truncate contact cascade; truncate company_user cascade; truncate payment_method cascade; @@ -99,9 +100,14 @@ 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 contact (contact_id, company_id, name) +values (6, 2, 'Contact 1') + , (8, 4, 'Contact 2') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values (6, 'Contact 1', 'XX555', '', '', '', '', 'ES') + , (8, 'Contact 2', 'XX666', '', '', '', '', 'ES') ; insert into invoice (invoice_id, company_id, invoice_number, contact_id, currency_code, payment_method_id) diff --git a/test/invoice_product_amount.sql b/test/invoice_product_amount.sql index 75d5e91..12a97bd 100644 --- a/test/invoice_product_amount.sql +++ b/test/invoice_product_amount.sql @@ -29,6 +29,7 @@ set client_min_messages to warning; truncate invoice_product_tax cascade; truncate invoice_product cascade; truncate invoice cascade; +truncate contact_tax_details cascade; truncate contact cascade; truncate tax cascade; truncate tax_class cascade; @@ -59,8 +60,12 @@ values (2, 1, 11, 'IRPF -15 %', -0.15) , (5, 1, 11, 'IVA 21 %', 0.21) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (7, 1, 'Contact') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values (7, 'Contact', 'XX555', '', '', '', '', 'ES') ; insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code, payment_method_id) diff --git a/test/invoice_product_tax.sql b/test/invoice_product_tax.sql index c5018a0..0fb34f5 100644 --- a/test/invoice_product_tax.sql +++ b/test/invoice_product_tax.sql @@ -43,6 +43,7 @@ truncate invoice_product cascade; truncate invoice cascade; truncate tax cascade; truncate tax_class cascade; +truncate contact_tax_details cascade; truncate contact cascade; truncate company_user cascade; truncate payment_method cascade; @@ -84,9 +85,14 @@ values (3, 2, 22, 'IVA 21 %', 0.21) , (6, 4, 44, 'IVA 10 %', 0.10) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values ( 9, 2, 'Customer 1', 'XX555', '', '777-777-777', 'c1@e', '', '', '', '', '', 'ES') - , (10, 4, 'Customer 2', 'XX666', '', '888-888-888', 'c2@e', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values ( 9, 2, 'Customer 1') + , (10, 4, 'Customer 2') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values ( 9, 'Customer 1', 'XX555', '', '', '', '', 'ES') + , (10, 'Customer 2', 'XX666', '', '', '', '', 'ES') ; insert into invoice (invoice_id, company_id, invoice_number, contact_id, currency_code, payment_method_id) diff --git a/test/invoice_tax_amount.sql b/test/invoice_tax_amount.sql index 3dd6d6c..7c385c6 100644 --- a/test/invoice_tax_amount.sql +++ b/test/invoice_tax_amount.sql @@ -29,6 +29,7 @@ set client_min_messages to warning; truncate invoice_product_tax cascade; truncate invoice_product cascade; truncate invoice cascade; +truncate contact_tax_details cascade; truncate contact cascade; truncate tax cascade; truncate tax_class cascade; @@ -59,8 +60,12 @@ values (2, 1, 11, 'IRPF -15 %', -0.15) , (5, 1, 11, 'IVA 21 %', 0.21) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (7, 1, 'Contact') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values (7, 'Contact', 'XX555', '', '', '', '', 'ES') ; insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code, payment_method_id) diff --git a/test/quote_amount.sql b/test/quote_amount.sql index 81624cd..a75e448 100644 --- a/test/quote_amount.sql +++ b/test/quote_amount.sql @@ -59,8 +59,8 @@ values (2, 1, 11, 'IRPF -15 %', -0.15) , (5, 1, 11, 'IVA 21 %', 0.21) ; -insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') +insert into contact (contact_id, company_id, name) +values (7, 1, 'Contact') ; insert into quote (quote_id, company_id, quote_number, quote_date, currency_code) diff --git a/test/tax_details.sql b/test/tax_details.sql new file mode 100644 index 0000000..d7246a2 --- /dev/null +++ b/test/tax_details.sql @@ -0,0 +1,26 @@ +-- Test tax_details +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(9); + +set search_path to numerus, public; + +select has_composite('numerus', 'tax_details', 'Composite type numerus.tax_details should exist'); +select columns_are('numerus', 'tax_details', array['business_name', 'vatin', 'address', 'city', 'province', 'postal_code', 'country_code']); +select col_type_is('numerus'::name, 'tax_details'::name, 'business_name'::name, 'text'); +select col_type_is('numerus'::name, 'tax_details'::name, 'vatin'::name, 'text'); +select col_type_is('numerus'::name, 'tax_details'::name, 'address'::name, 'text'); +select col_type_is('numerus'::name, 'tax_details'::name, 'city'::name, 'text'); +select col_type_is('numerus'::name, 'tax_details'::name, 'province'::name, 'text'); +select col_type_is('numerus'::name, 'tax_details'::name, 'postal_code'::name, 'text'); +select col_type_is('numerus'::name, 'tax_details'::name, 'country_code'::name, 'country_code'); + + +select * +from finish(); + +rollback; diff --git a/verify/add_contact.sql b/verify/add_contact.sql index 2e71a67..55c87af 100644 --- a/verify/add_contact.sql +++ b/verify/add_contact.sql @@ -2,6 +2,6 @@ begin; -select has_function_privilege('numerus.add_contact(integer, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[])', 'execute'); +select has_function_privilege('numerus.add_contact(integer, text, text, numerus.email, uri, numerus.tax_details, numerus.tag_name[])', 'execute'); rollback; diff --git a/verify/add_contact@v0.sql b/verify/add_contact@v0.sql new file mode 100644 index 0000000..2e71a67 --- /dev/null +++ b/verify/add_contact@v0.sql @@ -0,0 +1,7 @@ +-- Verify numerus:add_contact on pg + +begin; + +select has_function_privilege('numerus.add_contact(integer, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[])', 'execute'); + +rollback; diff --git a/verify/contact_email.sql b/verify/contact_email.sql new file mode 100644 index 0000000..5bdacc4 --- /dev/null +++ b/verify/contact_email.sql @@ -0,0 +1,13 @@ +-- Verify numerus:contact_email on pg + +begin; + +select contact_id + , email +from numerus.contact_email +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.contact_email'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.contact_email'::regclass; + +rollback; diff --git a/verify/contact_phone.sql b/verify/contact_phone.sql new file mode 100644 index 0000000..574e718 --- /dev/null +++ b/verify/contact_phone.sql @@ -0,0 +1,13 @@ +-- Verify numerus:contact_phone on pg + +begin; + +select contact_id + , phone +from numerus.contact_phone +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.contact_phone'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.contact_phone'::regclass; + +rollback; diff --git a/verify/contact_tax_details.sql b/verify/contact_tax_details.sql new file mode 100644 index 0000000..6a053e5 --- /dev/null +++ b/verify/contact_tax_details.sql @@ -0,0 +1,19 @@ +-- Verify numerus:contact_tax_details on pg + +begin; + +select contact_id + , business_name + , vatin + , address + , city + , province + , postal_code + , country_code +from numerus.contact_tax_details +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.contact_tax_details'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.contact_tax_details'::regclass; + +rollback; diff --git a/verify/contact_web.sql b/verify/contact_web.sql new file mode 100644 index 0000000..bb177f9 --- /dev/null +++ b/verify/contact_web.sql @@ -0,0 +1,13 @@ +-- Verify numerus:contact_web on pg + +begin; + +select contact_id + , uri +from numerus.contact_web +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.contact_web'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.contact_web'::regclass; + +rollback; diff --git a/verify/edit_contact.sql b/verify/edit_contact.sql index e145ad3..1fa9360 100644 --- a/verify/edit_contact.sql +++ b/verify/edit_contact.sql @@ -2,6 +2,6 @@ begin; -select has_function_privilege('numerus.edit_contact(uuid, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[])', 'execute'); +select has_function_privilege('numerus.edit_contact(uuid, text, text, numerus.email, uri, numerus.tax_details, numerus.tag_name[])', 'execute'); rollback; diff --git a/verify/edit_contact@v0.sql b/verify/edit_contact@v0.sql new file mode 100644 index 0000000..e145ad3 --- /dev/null +++ b/verify/edit_contact@v0.sql @@ -0,0 +1,7 @@ +-- Verify numerus:edit_contact on pg + +begin; + +select has_function_privilege('numerus.edit_contact(uuid, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[])', 'execute'); + +rollback; diff --git a/verify/invoice_contact_id_fkey.sql b/verify/invoice_contact_id_fkey.sql new file mode 100644 index 0000000..32a70c2 --- /dev/null +++ b/verify/invoice_contact_id_fkey.sql @@ -0,0 +1,13 @@ +-- Verify numerus:invoice_contact_id_fkey.sql on pg + +begin; + +select 1/count(*) +from pg_catalog.pg_constraint +where conrelid = 'numerus.invoice'::regclass +and contype = 'f' +and conname = 'invoice_contact_id_fkey' +and pg_catalog.pg_get_constraintdef(oid, true) = 'FOREIGN KEY (contact_id) REFERENCES numerus.contact_tax_details(contact_id)' +; + +rollback; diff --git a/verify/tax_details.sql b/verify/tax_details.sql new file mode 100644 index 0000000..b55b99d --- /dev/null +++ b/verify/tax_details.sql @@ -0,0 +1,7 @@ +-- Verify numerus:tax_details on pg + +begin; + +select pg_catalog.has_type_privilege('numerus.tax_details', 'usage'); + +rollback; diff --git a/web/static/numerus.css b/web/static/numerus.css index 4f1bdb6..b1264f4 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -717,6 +717,14 @@ main > nav { grid-template-columns: repeat(3, 1fr); } +.contact-tax-details { + display: none; +} + +input:checked ~ .contact-tax-details { + display: grid; +} + /* Multiselect, tags */ .tag { diff --git a/web/template/contacts/edit.gohtml b/web/template/contacts/edit.gohtml index f0b2ef2..0f4a7ed 100644 --- a/web/template/contacts/edit.gohtml +++ b/web/template/contacts/edit.gohtml @@ -24,23 +24,28 @@ {{ with .Form }}
- {{ template "input-field" .BusinessName }} - {{ template "input-field" .VATIN }} - {{ template "input-field" .TradeName }} + {{ template "input-field" .Name }} {{ template "input-field" .Phone }} {{ template "input-field" .Email }} {{ template "input-field" .Web }} + {{ template "tags-field" .Tags }} +
+ + {{ template "check-field" .HasTaxDetails }} + +
+ {{ template "input-field" .BusinessName }} + {{ template "input-field" .VATIN }} {{ template "input-field" .Address }} {{ template "input-field" .City }} {{ template "input-field" .Province }} {{ template "input-field" .PostalCode }} {{ template "select-field" .Country }} - {{ template "tags-field" .Tags }}
{{ end }}
- +
diff --git a/web/template/contacts/new.gohtml b/web/template/contacts/new.gohtml index 1d273f9..0bdf05d 100644 --- a/web/template/contacts/new.gohtml +++ b/web/template/contacts/new.gohtml @@ -21,22 +21,27 @@ {{ csrfToken }}
- {{ template "input-field" .BusinessName | addInputAttr "autofocus" }} - {{ template "input-field" .VATIN }} - {{ template "input-field" .TradeName }} - {{ template "input-field" .Phone }} - {{ template "input-field" .Email }} - {{ template "input-field" .Web }} - {{ template "input-field" .Address }} - {{ template "input-field" .City }} - {{ template "input-field" .Province }} - {{ template "input-field" .PostalCode }} - {{ template "select-field" .Country }} - {{ template "tags-field" .Tags }} + {{ template "input-field" .Name | addInputAttr "autofocus" }} + {{ template "input-field" .Phone }} + {{ template "input-field" .Email }} + {{ template "input-field" .Web }} + {{ template "tags-field" .Tags }} +
+ + {{ template "check-field" .HasTaxDetails }} + +
+ {{ template "input-field" .BusinessName }} + {{ template "input-field" .VATIN }} + {{ template "input-field" .Address }} + {{ template "input-field" .City }} + {{ template "input-field" .Province }} + {{ template "input-field" .PostalCode }} + {{ template "select-field" .Country }}
- +
diff --git a/web/template/form.gohtml b/web/template/form.gohtml index 0461c0f..7e6a5b7 100644 --- a/web/template/form.gohtml +++ b/web/template/form.gohtml @@ -142,6 +142,12 @@ {{- end }} +{{ define "check-field" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.CheckField*/ -}} + + +{{- end }} + {{ define "invoice-product-form" -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}
{{ if .HasQuotee -}} -
- {{ .Quotee.Name }}
- {{ .Quotee.VATIN }}
- {{ .Quotee.Address }}
- {{ .Quotee.City }} ({{ .Quotee.PostalCode}}), {{ .Quotee.Province }}
-
+
+ {{ .Quotee.Name }}
+ {{ if .HasTaxDetails -}} + {{ .Quotee.VATIN }}
+ {{ .Quotee.Address }}
+ {{ .Quotee.City }} ({{ .Quotee.PostalCode}}), {{ .Quotee.Province }}
+ {{- end }} +
{{- end }} {{ if .TermsAndConditions -}}

{{(gettext "Terms and Conditions:")}} {{ .TermsAndConditions }}

{{- end }} - + {{- $columns := 5 | add (len .TaxClasses) | add (boolToInt .HasDiscounts) -}}