Split contact relation into tax_details, phone, web, and email

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.
This commit is contained in:
jordi fita mas 2023-06-30 21:32:48 +02:00
parent 30cd15ee89
commit 1c0f126c58
72 changed files with 2339 additions and 657 deletions

View File

@ -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 lHort, 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 lHort, 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 laigua règia.', '55.92', array[124], array['metall']);

View File

@ -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;

36
deploy/add_contact@v0.sql Normal file
View File

@ -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;

39
deploy/contact_email.sql Normal file
View File

@ -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;

38
deploy/contact_phone.sql Normal file
View File

@ -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;

View File

@ -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;

39
deploy/contact_web.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

20
deploy/tax_details.sql Normal file
View File

@ -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;

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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
}

View File

@ -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,

358
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-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 <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\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 dacceptació:"
@ -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 dusuari 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 ladreç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 dimpost"
#: pkg/company.go:436
#: pkg/company.go:572
msgid "Select a tax class"
msgstr "Escolliu una classe dimpost"
#: 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 limpost en blanc."
#: pkg/company.go:464
#: pkg/company.go:600
msgid "Selected tax class is not valid."
msgstr "Heu seleccionat una classe dimpost 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 dacceptació"
#: 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 "LID 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 "LID 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 "LID 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 ladreç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"

358
po/es.po
View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-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 <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\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"

View File

@ -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;

View File

@ -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;

23
revert/contact_email.sql Normal file
View File

@ -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;

23
revert/contact_phone.sql Normal file
View File

@ -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;

View File

@ -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;

23
revert/contact_web.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

7
revert/tax_details.sql Normal file
View File

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

View File

@ -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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to edit quotations
@v0 2023-06-12T14:05:34Z jordi fita mas <jordi@tandem.blog> # Tag version 0
contact_phone [roles schema_numerus extension_pg_libphonenumber] 2023-06-28T11:04:19Z jordi fita mas <jordi@tandem.blog> # Add relation to keep contacts phone numbers
contact_email [roles schema_numerus email contact] 2023-06-28T11:47:19Z jordi fita mas <jordi@tandem.blog> # Add relation to keep contacts emails
contact_web [roles schema_numerus extension_uri contact] 2023-06-28T12:01:07Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add relation of contacts tax details
tax_details [schema_numerus extension_vat country_code] 2023-06-29T10:57:57Z jordi fita mas <jordi@tandem.blog> # Add composite type for contacts tax details
add_contact [add_contact@v0 tax_details] 2023-06-29T11:10:15Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Update invoices contact_id foreign key to point to tax sales

View File

@ -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();

View File

@ -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')
;

View File

@ -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')
;

View File

@ -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')
;

View File

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

View File

@ -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'
);

119
test/contact_email.sql Normal file
View File

@ -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;

118
test/contact_phone.sql Normal file
View File

@ -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;

View File

@ -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;

118
test/contact_web.sql Normal file
View File

@ -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;

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

26
test/tax_details.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

13
verify/contact_email.sql Normal file
View File

@ -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;

13
verify/contact_phone.sql Normal file
View File

@ -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;

View File

@ -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;

13
verify/contact_web.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

7
verify/tax_details.sql Normal file
View File

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

View File

@ -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 {

View File

@ -24,23 +24,28 @@
{{ with .Form }}
<div class="contact-data">
{{ 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 }}
</div>
{{ template "check-field" .HasTaxDetails }}
<div class="contact-data contact-tax-details">
{{ 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 }}
</div>
{{ end }}
<fieldset>
<button class="primary" type="submit">{{( pgettext "Save" "action" )}}</button>
<button formnovalidate class="primary" type="submit">{{( pgettext "Save" "action" )}}</button>
</fieldset>
</form>
</section>

View File

@ -21,22 +21,27 @@
{{ csrfToken }}
<div class="contact-data">
{{ 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 }}
</div>
{{ template "check-field" .HasTaxDetails }}
<div class="contact-data contact-tax-details">
{{ 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 }}
</div>
<fieldset>
<button class="primary" type="submit">{{( pgettext "Save" "action" )}}</button>
<button formnovalidate class="primary" type="submit">{{( pgettext "Save" "action" )}}</button>
</fieldset>
</form>

View File

@ -142,6 +142,12 @@
</fieldset>
{{- end }}
{{ define "check-field" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.CheckField*/ -}}
<input id="{{ .Name }}-field" type="checkbox" name="{{.Name}}"{{ if .Checked}} checked="checked"{{ end }}{{ if .Required }} required="required"{{ end }}>
<label for="{{ .Name }}-field">{{.Label }}</label>
{{- end }}
{{ define "invoice-product-form" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}
<fieldset class="new-invoice-product"

View File

@ -51,18 +51,20 @@
<div>
{{ if .HasQuotee -}}
<address class="quotee">
{{ .Quotee.Name }}<br>
{{ .Quotee.VATIN }}<br>
{{ .Quotee.Address }}<br>
{{ .Quotee.City }} ({{ .Quotee.PostalCode}}), {{ .Quotee.Province }}<br>
</address>
<address class="quotee">
{{ .Quotee.Name }}<br>
{{ if .HasTaxDetails -}}
{{ .Quotee.VATIN }}<br>
{{ .Quotee.Address }}<br>
{{ .Quotee.City }} ({{ .Quotee.PostalCode}}), {{ .Quotee.Province }}<br>
{{- end }}
</address>
{{- end }}
{{ if .TermsAndConditions -}}
<p class="terms_and_conditions">{{(gettext "Terms and Conditions:")}} {{ .TermsAndConditions }}</p>
{{- end }}
{{- $columns := 5 | add (len .TaxClasses) | add (boolToInt .HasDiscounts) -}}
<table>
<thead>