Add customer and invoices sections

Copied as much as i could from Numerus, and made as few modifications as
i could to adapt to this code base; it is, quite frankly, a piece of
shit.

We need to be able to create invoices from scratch “just in case”,
apparently, but it is not yet possible to create an invoice from a
booking.
This commit is contained in:
jordi fita mas 2024-04-28 20:28:45 +02:00
parent 3559ff311b
commit 17f7520876
157 changed files with 9244 additions and 399 deletions

1
debian/control vendored
View File

@ -10,6 +10,7 @@ Build-Depends:
golang-any, golang-any,
golang-github-jackc-pgx-v4-dev, golang-github-jackc-pgx-v4-dev,
golang-github-leonelquinteros-gotext-dev, golang-github-leonelquinteros-gotext-dev,
golang-github-rainycape-unidecode-dev,
golang-golang-x-text-dev, golang-golang-x-text-dev,
postgresql-all (>= 217~), postgresql-all (>= 217~),
sqitch, sqitch,

View File

@ -24,6 +24,21 @@ values (52, 42, 'employee')
, (52, 43, 'admin') , (52, 43, 'admin')
; ;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (1, 52, 'Pagament', '')
;
insert into tax_class (tax_class_id, company_id, name)
values (1, 52, 'VAT')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (1, 52, 1, 'General VAT (21 %)', 0.21)
, (2, 52, 1, 'Reduced VAT (10 %)', 0.10)
, (3, 52, 1, 'Super-reduced VAT (4 %)', 0.04)
, (4, 52, 1, 'VAT free (0 %)', 0.00)
;
select setup_redsys(52, '361716962', '1', 'test', 'redirect', 'sq7HjrUOBfKmC576ILgskD5srU870gJ7'); select setup_redsys(52, '361716962', '1', 'test', 'redirect', 'sq7HjrUOBfKmC576ILgskD5srU870gJ7');
select setup_location(52, '<div><h3>On som</h3><p>Ctra. de Sadernes, km 2, 17855 MONTAGUT i OIX</p></div>', '<iframe src="https://www.google.com/maps/embed?pb=!1m14!1m8!1m3!1d44661.89614700166!2d2.57381383167473!3d42.24000148364468!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x12bab79ff3b0f007%3A0x65b7563a5d1548e6!2sCamping%20Montagut!5e0!3m2!1sca!2sus!4v1703225042845!5m2!1sca!2sus" width="100%" height="600" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>', '<div><p><strong>Càmping i Safari tents:</strong><br />de 08/04 a 09/10</p><p><strong>Cabanes i Bungalows:</strong><br />de 08/04 a 11/12</p><p><strong>ACSI</strong>:<br />de 08/04 a 11/12</p></div>'); select setup_location(52, '<div><h3>On som</h3><p>Ctra. de Sadernes, km 2, 17855 MONTAGUT i OIX</p></div>', '<iframe src="https://www.google.com/maps/embed?pb=!1m14!1m8!1m3!1d44661.89614700166!2d2.57381383167473!3d42.24000148364468!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x12bab79ff3b0f007%3A0x65b7563a5d1548e6!2sCamping%20Montagut!5e0!3m2!1sca!2sus!4v1703225042845!5m2!1sca!2sus" width="100%" height="600" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>', '<div><p><strong>Càmping i Safari tents:</strong><br />de 08/04 a 09/10</p><p><strong>Cabanes i Bungalows:</strong><br />de 08/04 a 11/12</p><p><strong>ACSI</strong>:<br />de 08/04 a 11/12</p></div>');

49
deploy/add_contact.sql Normal file
View File

@ -0,0 +1,49 @@
-- Deploy camper:add_contact to pg
-- requires: roles
-- requires: schema_camper
-- requires: email
-- requires: extension_pg_libphonenumber
-- requires: country_code
-- requires: contact
-- requires: contact_phone
-- requires: contact_email
begin;
set search_path to camper, public;
create or replace function add_contact(company_id integer, name text, id_document_type_id text, id_document_number text, phone text, email text, address text, city text, province text, postal_code text, country_code country_code) returns uuid as
$$
declare
cid integer;
cslug uuid;
begin
insert into contact (company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
returning contact_id, slug
into cid, cslug
;
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(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;
return cslug;
end
$$
language plpgsql
;
revoke execute on function add_contact(integer, text, text, text, text, text, text, text, text, text, country_code) from public;
grant execute on function add_contact(integer, text, text, text, text, text, text, text, text, text, country_code) to employee;
grant execute on function add_contact(integer, text, text, text, text, text, text, text, text, text, country_code) to admin;
commit;

75
deploy/add_invoice.sql Normal file
View File

@ -0,0 +1,75 @@
-- Deploy camper:add_invoice to pg
-- requires: roles
-- requires: schema_camper
-- requires: invoice
-- requires: company
-- requires: currency
-- requires: parse_price
-- requires: new_invoice_product
-- requires: tax
-- requires: invoice_product
-- requires: invoice_product_product
-- requires: invoice_product_tax
-- requires: next_invoice_number
begin;
set search_path to camper, public;
create or replace function add_invoice(company integer, invoice_date date, contact_id integer, notes text, payment_method_id integer, products new_invoice_product[]) returns uuid as
$$
declare
iid integer;
pslug uuid;
product new_invoice_product;
ccode text;
ipid integer;
begin
insert into invoice (company_id, invoice_number, invoice_date, contact_id, notes, currency_code, payment_method_id)
select company_id
, next_invoice_number(add_invoice.company, invoice_date)
, invoice_date
, contact_id
, notes
, currency_code
, add_invoice.payment_method_id
from company
where company.company_id = add_invoice.company
returning invoice_id, slug, currency_code
into iid, pslug, ccode;
foreach product in array products
loop
insert into invoice_product (invoice_id, name, description, price, quantity, discount_rate)
select iid
, product.name
, coalesce(product.description, '')
, parse_price(product.price, currency.decimal_digits)
, product.quantity
, product.discount_rate
from currency
where currency_code = ccode
returning invoice_product_id
into ipid;
if product.product_id is not null then
insert into invoice_product_product (invoice_product_id, product_id)
values (ipid, product.product_id);
end if;
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
select ipid, tax_id, tax.rate
from tax
join unnest(product.tax) as ptax(tax_id) using (tax_id);
end loop;
return pslug;
end;
$$
language plpgsql;
revoke execute on function add_invoice(integer, date, integer, text, integer, new_invoice_product[]) from public;
grant execute on function add_invoice(integer, date, integer, text, integer, new_invoice_product[]) to employee;
grant execute on function add_invoice(integer, date, integer, text, integer, new_invoice_product[]) to admin;
commit;

View File

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

View File

@ -0,0 +1,64 @@
-- Deploy camper:compute_new_invoice_amount to pg
-- requires: schema_camper
-- requires: company
-- requires: currency
-- requires: tax
-- requires: new_invoice_product
-- requires: new_invoice_amount
begin;
set search_path to camper, public;
create or replace function compute_new_invoice_amount(company_id integer, products new_invoice_product[]) returns new_invoice_amount as
$$
declare
result new_invoice_amount;
begin
if array_length(products, 1) is null then
select to_price(0, decimal_digits), array[]::text[][], to_price(0, decimal_digits)
from company
join currency using (currency_code)
where company.company_id = compute_new_invoice_amount.company_id
into result.subtotal, result.taxes, result.total;
else
with product as (
select round(parse_price(price, currency.decimal_digits) * quantity * (1 - discount_rate))::integer as subtotal
, tax
, decimal_digits
from unnest(products)
join company on company.company_id = compute_new_invoice_amount.company_id
join currency using (currency_code)
)
, tax_amount as (
select tax_id
, sum(round(subtotal * tax.rate)::integer)::integer as amount
, decimal_digits
from product, unnest(product.tax) as product_tax(tax_id)
join tax using (tax_id)
group by tax_id, decimal_digits
)
, tax_total as (
select sum(amount)::integer as amount, array_agg(array[name, to_price(amount, decimal_digits)]) as taxes
from tax_amount
join tax using (tax_id)
)
select to_price(sum(subtotal)::integer, decimal_digits)
, coalesce(taxes, array[]::text[][])
, to_price(sum(subtotal)::integer + coalesce(tax_total.amount, 0), decimal_digits) as total
from product, tax_total
group by tax_total.amount, taxes, decimal_digits
into result.subtotal, result.taxes, result.total;
end if;
return result;
end
$$
language plpgsql
stable;
revoke execute on function compute_new_invoice_amount(integer, new_invoice_product[]) from public;
grant execute on function compute_new_invoice_amount(integer, new_invoice_product[]) to employee;
grant execute on function compute_new_invoice_amount(integer, new_invoice_product[]) to admin;
commit;

45
deploy/contact.sql Normal file
View File

@ -0,0 +1,45 @@
-- Deploy camper:contact to pg
-- requires: roles
-- requires: schema_camper
-- requires: user_profile
-- requires: company
-- requires: id_document_type
-- requires: country_code
-- requires: country
begin;
set search_path to camper, public;
create table contact (
contact_id integer generated by default as identity primary key,
company_id integer not null references company,
slug uuid not null unique default gen_random_uuid(),
name text not null constraint name_not_empty check(length(trim(name)) > 1),
id_document_type_id varchar(1) not null references id_document_type,
id_document_number text 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,
created_at timestamptz not null default current_timestamp
);
grant select, insert, update, delete on table contact to employee;
grant select, insert, update, delete on table contact to admin;
alter table contact enable row level security;
create policy company_policy
on contact
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = contact.company_id
)
);
commit;

31
deploy/contact_email.sql Normal file
View File

@ -0,0 +1,31 @@
-- Deploy camper:contact_email to pg
-- requires: roles
-- requires: schema_camper
-- requires: email
-- requires: contact
begin;
set search_path to camper, 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 employee;
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
)
);
commit;

30
deploy/contact_phone.sql Normal file
View File

@ -0,0 +1,30 @@
-- Deploy camper:contact_phone to pg
-- requires: roles
-- requires: schema_camper
-- requires: extension_pg_libphonenumber
begin;
set search_path to camper, 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 employee;
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
)
);
commit;

14
deploy/discount_rate.sql Normal file
View File

@ -0,0 +1,14 @@
-- Deploy camper:discount_rate to pg
-- requires: schema_camper
begin;
set search_path to camper, public;
create domain discount_rate as numeric
check (VALUE >= 0 and VALUE <= 1);
comment on domain discount_rate is
'A rate for discount in the range [0, 1]';
commit;

72
deploy/edit_contact.sql Normal file
View File

@ -0,0 +1,72 @@
-- Deploy camper:edit_contact to pg
-- requires: roles
-- requires: schema_camper
-- requires: email
-- requires: country_code
-- requires: contact
-- requires: extension_pg_libphonenumber
-- requires: contact_phone
-- requires: contact_email
begin;
set search_path to camper, public;
create or replace function edit_contact(contact_slug uuid, name text, id_document_type_id text, id_document_number text, phone text, email text, address text, city text, province text, postal_code text, country_code country_code) returns uuid as
$$
declare
cid integer;
begin
update contact
set name = edit_contact.name
, id_document_type_id = edit_contact.id_document_type_id
, id_document_number = edit_contact.id_document_number
, address = edit_contact.address
, city = edit_contact.city
, province = edit_contact.province
, postal_code = edit_contact.postal_code
, country_code = edit_contact.country_code
where slug = contact_slug
returning contact_id
into cid
;
if cid is null then
return null;
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(phone, coalesce(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, email)
on conflict (contact_id) do update
set email = excluded.email
;
end if;
return contact_slug;
end
$$
language plpgsql
;
revoke execute on function edit_contact(uuid, text, text, text, text, text, text, text, text, text, country_code) from public;
grant execute on function edit_contact(uuid, text, text, text, text, text, text, text, text, text, country_code) to employee;
grant execute on function edit_contact(uuid, text, text, text, text, text, text, text, text, text, country_code) to admin;
commit;

110
deploy/edit_invoice.sql Normal file
View File

@ -0,0 +1,110 @@
-- Deploy camper:edit_invoice to pg
-- requires: roles
-- requires: schema_camper
-- requires: invoice
-- requires: currency
-- requires: parse_price
-- requires: edited_invoice_product
-- requires: tax
-- requires: invoice_product
-- requires: invoice_product_product
-- requires: invoice_product_tax
begin;
set search_path to camper, public;
create or replace function edit_invoice(invoice_slug uuid, invoice_status text, contact_id integer, notes text, payment_method_id integer, products edited_invoice_product[]) returns uuid as
$$
declare
iid integer;
products_to_keep integer[];
products_to_delete integer[];
company integer;
ccode text;
product edited_invoice_product;
ipid integer;
begin
update invoice
set contact_id = edit_invoice.contact_id
, invoice_status = edit_invoice.invoice_status
, notes = edit_invoice.notes
, payment_method_id = edit_invoice.payment_method_id
where slug = invoice_slug
returning invoice_id, company_id, currency_code
into iid, company, ccode
;
if iid is null then
return null;
end if;
foreach product in array products
loop
if product.invoice_product_id is null then
insert into invoice_product (invoice_id, name, description, price, quantity, discount_rate)
select iid
, product.name
, coalesce(product.description, '')
, parse_price(product.price, currency.decimal_digits)
, product.quantity
, product.discount_rate
from currency
where currency_code = ccode
returning invoice_product_id
into ipid;
else
ipid := product.invoice_product_id;
update invoice_product
set name = product.name
, description = coalesce(product.description, '')
, price = parse_price(product.price, currency.decimal_digits)
, quantity = product.quantity
, discount_rate = product.discount_rate
from currency
where invoice_product_id = ipid
and currency_code = ccode;
end if;
products_to_keep := array_append(products_to_keep, ipid);
if product.product_id is null then
delete from invoice_product_product where invoice_product_id = ipid;
else
insert into invoice_product_product (invoice_product_id, product_id)
values (ipid, product.product_id)
on conflict (invoice_product_id) do update
set product_id = product.product_id;
end if;
delete from invoice_product_tax where invoice_product_id = ipid;
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
select ipid, tax_id, tax.rate
from tax
join unnest(product.tax) as ptax(tax_id) using (tax_id);
end loop;
select array_agg(invoice_product_id)
into products_to_delete
from invoice_product
where invoice_id = iid
and not (invoice_product_id = any(products_to_keep));
if array_length(products_to_delete, 1) > 0 then
delete from invoice_product_tax where invoice_product_id = any(products_to_delete);
delete from invoice_product_product where invoice_product_id = any(products_to_delete);
delete from invoice_product where invoice_product_id = any(products_to_delete);
end if;
return invoice_slug;
end;
$$
language plpgsql;
revoke execute on function edit_invoice(uuid, text, integer, text, integer, edited_invoice_product[]) from public;
grant execute on function edit_invoice(uuid, text, integer, text, integer, edited_invoice_product[]) to employee;
grant execute on function edit_invoice(uuid, text, integer, text, integer, edited_invoice_product[]) to admin;
commit;

View File

@ -0,0 +1,20 @@
-- Deploy camper:edited_invoice_product to pg
-- requires: schema_camper
-- requires: discount_rate
begin;
set search_path to camper, public;
create type edited_invoice_product as
( invoice_product_id integer
, product_id integer
, name text
, description text
, price text
, quantity integer
, discount_rate discount_rate
, tax integer[]
);
commit;

44
deploy/invoice.sql Normal file
View File

@ -0,0 +1,44 @@
-- Deploy camper:invoice to pg
-- requires: roles
-- requires: schema_camper
-- requires: user_profile
-- requires: company
-- requires: contact
-- requires: invoice_status
-- requires: currency
begin;
set search_path to camper, public;
create table invoice (
invoice_id integer generated by default as identity primary key,
company_id integer not null references company,
slug uuid not null unique default gen_random_uuid(),
invoice_number text not null constraint invoice_number_not_empty check(length(trim(invoice_number)) > 1),
invoice_date date not null default current_date,
contact_id integer not null references contact,
invoice_status text not null default 'created' references invoice_status,
notes text not null default '',
payment_method_id integer not null references payment_method,
currency_code text not null references currency,
created_at timestamptz not null default current_timestamp
);
grant select, insert, update, delete on table invoice to employee;
grant select, insert, update, delete on table invoice to admin;
alter table invoice enable row level security;
create policy company_policy
on invoice
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = invoice.company_id
)
);
commit;

22
deploy/invoice_amount.sql Normal file
View File

@ -0,0 +1,22 @@
-- Deploy camper:invoice_amount to pg
-- requires: schema_camper
-- requires: invoice_product
-- requires: invoice_product_amount
begin;
set search_path to camper, public;
create or replace view invoice_amount as
select invoice_id
, sum(subtotal)::integer as subtotal
, sum(total)::integer as total
from invoice_product
join invoice_product_amount using (invoice_product_id)
group by invoice_id
;
grant select on table invoice_amount to employee;
grant select on table invoice_amount to admin;
commit;

View File

@ -0,0 +1,32 @@
-- Deploy camper:invoice_number_counter to pg
-- requires: schema_camper
-- requires: company
begin;
set search_path to camper, public;
create table invoice_number_counter (
company_id integer not null references company,
year integer not null constraint year_always_positive check(year > 0),
currval integer not null constraint counter_zero_or_positive check(currval >= 0),
primary key (company_id, year)
);
grant select, insert, update on table invoice_number_counter to employee;
grant select, insert, update on table invoice_number_counter to admin;
alter table invoice_number_counter enable row level security;
create policy company_policy
on invoice_number_counter
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = invoice_number_counter.company_id
)
);
commit;

View File

@ -0,0 +1,38 @@
-- Deploy camper:invoice_product to pg
-- requires: schema_camper
-- requires: invoice
-- requires: discount_rate
begin;
set search_path to camper, public;
create table invoice_product (
invoice_product_id integer generated by default as identity primary key,
invoice_id integer not null references invoice,
name text not null constraint name_not_empty check(length(trim(name)) > 0),
description text not null default '',
price integer not null,
quantity integer not null default 1,
discount_rate discount_rate not null default 0.0
);
grant select, insert, update, delete on table invoice_product to employee;
grant select, insert, update, delete on table invoice_product to admin;
grant usage on sequence invoice_product_invoice_product_id_seq to employee;
grant usage on sequence invoice_product_invoice_product_id_seq to admin;
alter table invoice_product enable row level security;
create policy company_policy
on invoice_product
using (
exists(
select 1
from invoice
where invoice.invoice_id = invoice_product.invoice_id
)
);
commit;

View File

@ -0,0 +1,22 @@
-- Deploy camper:invoice_product_amount to pg
-- requires: schema_camper
-- requires: invoice_product
-- requires: invoice_product_tax
begin;
set search_path to camper, public;
create or replace view invoice_product_amount as
select invoice_product_id
, round(price * quantity * (1 - discount_rate))::integer as subtotal
, max(round(price * quantity * (1 - discount_rate))::integer) + coalesce(sum(round(round(price * quantity * (1 - discount_rate))::integer * tax_rate)::integer)::integer, 0) as total
from invoice_product
left join invoice_product_tax using (invoice_product_id)
group by invoice_product_id, price, quantity, discount_rate
;
grant select on table invoice_product_amount to employee;
grant select on table invoice_product_amount to admin;
commit;

View File

@ -0,0 +1,18 @@
-- Deploy camper:invoice_product_product to pg
-- requires: schema_camper
-- requires: invoice_product
-- requires: product
begin;
set search_path to camper;
create table invoice_product_product (
invoice_product_id integer primary key references invoice_product,
product_id integer not null references product
);
grant select, insert, update, delete on table invoice_product_product to employee;
grant select, insert, update, delete on table invoice_product_product to admin;
commit;

View File

@ -0,0 +1,33 @@
-- Deploy camper:invoice_product_tax to pg
-- requires: schema_camper
-- requires: invoice_product
-- requires: tax
-- requires: tax_rate
begin;
set search_path to camper, public;
create table invoice_product_tax (
invoice_product_id integer not null references invoice_product,
tax_id integer not null references tax,
tax_rate tax_rate not null,
primary key (invoice_product_id, tax_id)
);
grant select, insert, update, delete on table invoice_product_tax to employee;
grant select, insert, update, delete on table invoice_product_tax to admin;
alter table invoice_product_tax enable row level security;
create policy company_policy
on invoice_product_tax
using (
exists(
select 1
from invoice_product
where invoice_product.invoice_product_id = invoice_product_tax.invoice_product_id
)
);
commit;

16
deploy/invoice_status.sql Normal file
View File

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

View File

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

View File

@ -0,0 +1,23 @@
-- Deploy camper:invoice_tax_amount to pg
-- requires: schema_camper
-- requires: invoice_product
-- requires: invoice_product_tax
begin;
set search_path to camper, public;
create or replace view invoice_tax_amount as
select invoice_id
, tax_id
, sum(round(round(price * quantity * (1 - discount_rate))::integer * tax_rate)::integer)::integer as amount
from invoice_product
join invoice_product_tax using (invoice_product_id)
group by invoice_id
, tax_id
;
grant select on table invoice_tax_amount to employee;
grant select on table invoice_tax_amount to admin;
commit;

View File

@ -0,0 +1,14 @@
-- Deploy camper:new_invoice_amount to pg
-- requires: schema_camper
begin;
set search_path to camper, public;
create type new_invoice_amount as (
subtotal text,
taxes text[][],
total text
);
commit;

View File

@ -0,0 +1,19 @@
-- Deploy camper:new_invoice_product to pg
-- requires: schema_camper
-- requires: discount_rate
begin;
set search_path to camper, public;
create type new_invoice_product as (
product_id integer,
name text,
description text,
price text,
quantity integer,
discount_rate discount_rate,
tax integer[]
);
commit;

View File

@ -0,0 +1,38 @@
-- Deploy camper:next_invoice_number to pg
-- requires: schema_camper
-- requires: invoice_number_counter
begin;
set search_path to camper, public;
create or replace function next_invoice_number(company integer, invoice_date date) returns text
as
$$
declare
num integer;
invoice_number text;
begin
insert into invoice_number_counter (company_id, year, currval)
values (next_invoice_number.company, date_part('year', invoice_date), 1)
on conflict (company_id, year) do
update
set currval = invoice_number_counter.currval + 1
returning currval
into num;
select to_char(invoice_date, to_char(num, 'FM' || replace(invoice_number_format, '"', '\""')))
into invoice_number
from company
where company_id = next_invoice_number.company;
return invoice_number;
end;
$$
language plpgsql;
revoke execute on function next_invoice_number(integer, date) from public;
grant execute on function next_invoice_number(integer, date) to employee;
grant execute on function next_invoice_number(integer, date) to admin;
commit;

34
deploy/payment_method.sql Normal file
View File

@ -0,0 +1,34 @@
-- Deploy camper:payment_method to pg
-- requires: roles
-- requires: schema_camper
-- requires: user_profile
-- requires: company
begin;
set search_path to camper, public;
create table payment_method (
payment_method_id integer generated by default as identity primary key,
company_id integer not null references company,
name text not null constraint name_not_empty check(length(trim(name)) > 0),
instructions text not null
);
grant select, insert, update, delete on table payment_method to employee;
grant select, insert, update, delete on table payment_method to admin;
alter table payment_method enable row level security;
create policy company_policy
on payment_method
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = payment_method.company_id
)
);
commit;

40
deploy/product.sql Normal file
View File

@ -0,0 +1,40 @@
-- Deploy camper:product to pg
-- requires: roles
-- requires: schema_camper
-- requires: user_profile
-- requires: company
begin;
set search_path to camper, public;
create table product (
product_id integer generated by default as identity primary key,
company_id integer not null references company,
slug uuid not null default gen_random_uuid(),
name text not null constraint name_not_empty check(length(trim(name)) > 0),
description text not null default '',
price integer not null,
created_at timestamptz not null default current_timestamp
);
comment on column product.price is
'Price is stored in cents.';
grant select, insert, update, delete on table product to employee;
grant select, insert, update, delete on table product to admin;
alter table product enable row level security;
create policy company_policy
on product
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = product.company_id
)
);
commit;

31
deploy/product_tax.sql Normal file
View File

@ -0,0 +1,31 @@
-- Deploy camper:product_tax to pg
-- requires: schema_camper
-- requires: product
-- requires: tax
begin;
set search_path to camper, public;
create table product_tax (
product_id integer not null references product,
tax_id integer not null references tax,
primary key (product_id, tax_id)
);
grant select, insert, update, delete on table product_tax to employee;
grant select, insert, update, delete on table product_tax to admin;
alter table product_tax enable row level security;
create policy company_policy
on product_tax
using (
exists(
select 1
from product
where product.product_id = product_tax.product_id
)
);
commit;

37
deploy/tax.sql Normal file
View File

@ -0,0 +1,37 @@
-- Deploy camper:tax to pg
-- requires: roles
-- requires: schema_camper
-- requires: user_profile
-- requires: company
-- requires: tax_rate
-- requires: tax_class
begin;
set search_path to camper, public;
create table tax (
tax_id integer generated by default as identity primary key,
company_id integer not null references company,
tax_class_id integer not null references tax_class,
name text not null constraint name_not_empty check(length(trim(name)) > 0),
rate tax_rate not null
);
grant select, insert, update, delete on table tax to employee;
grant select, insert, update, delete on table tax to admin;
alter table tax enable row level security;
create policy company_policy
on tax
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = tax.company_id
)
);
commit;

33
deploy/tax_class.sql Normal file
View File

@ -0,0 +1,33 @@
-- Deploy camper:tax_class to pg
-- requires: roles
-- requires: schema_camper
-- requires: user_profile
-- requires: company
begin;
set search_path to camper, public;
create table tax_class (
tax_class_id integer generated by default as identity not null primary key,
company_id integer not null references company,
name text not null constraint name_not_empty check(length(trim(name)) > 0)
);
grant select, insert, update, delete on table tax_class to employee;
grant select, insert, update, delete on table tax_class to admin;
alter table tax_class enable row level security;
create policy company_policy
on tax_class
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = tax_class.company_id
)
);
commit;

14
deploy/tax_rate.sql Normal file
View File

@ -0,0 +1,14 @@
-- Deploy camper:tax_rate to pg
-- requires: schema_camper
begin;
set search_path to camper, public;
create domain tax_rate as numeric
check (value > -1 and value < 1);
comment on domain tax_rate is
'A rate for taxes in the range (-1, 1)';
commit;

3
go.mod
View File

@ -4,15 +4,16 @@ go 1.19
require ( require (
github.com/jackc/pgconn v1.11.0 github.com/jackc/pgconn v1.11.0
github.com/jackc/pgio v1.0.0
github.com/jackc/pgtype v1.10.0 github.com/jackc/pgtype v1.10.0
github.com/jackc/pgx/v4 v4.15.0 github.com/jackc/pgx/v4 v4.15.0
github.com/leonelquinteros/gotext v1.5.0 github.com/leonelquinteros/gotext v1.5.0
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8
golang.org/x/text v0.7.0 golang.org/x/text v0.7.0
) )
require ( require (
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect

2
go.sum
View File

@ -88,6 +88,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8 h1:iZTHFqK/oFrjyFDkiw5U/RjQxkMlkpq6tHQIO407i+s=
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=

View File

@ -13,9 +13,11 @@ import (
"dev.tandem.ws/tandem/camper/pkg/booking" "dev.tandem.ws/tandem/camper/pkg/booking"
"dev.tandem.ws/tandem/camper/pkg/campsite" "dev.tandem.ws/tandem/camper/pkg/campsite"
"dev.tandem.ws/tandem/camper/pkg/company" "dev.tandem.ws/tandem/camper/pkg/company"
"dev.tandem.ws/tandem/camper/pkg/customer"
"dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/home" "dev.tandem.ws/tandem/camper/pkg/home"
httplib "dev.tandem.ws/tandem/camper/pkg/http" httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/invoice"
"dev.tandem.ws/tandem/camper/pkg/legal" "dev.tandem.ws/tandem/camper/pkg/legal"
"dev.tandem.ws/tandem/camper/pkg/location" "dev.tandem.ws/tandem/camper/pkg/location"
"dev.tandem.ws/tandem/camper/pkg/media" "dev.tandem.ws/tandem/camper/pkg/media"
@ -32,7 +34,9 @@ type adminHandler struct {
booking *booking.AdminHandler booking *booking.AdminHandler
campsite *campsite.AdminHandler campsite *campsite.AdminHandler
company *company.AdminHandler company *company.AdminHandler
customer *customer.AdminHandler
home *home.AdminHandler home *home.AdminHandler
invoice *invoice.AdminHandler
legal *legal.AdminHandler legal *legal.AdminHandler
location *location.AdminHandler location *location.AdminHandler
media *media.AdminHandler media *media.AdminHandler
@ -49,7 +53,9 @@ func newAdminHandler(mediaDir string) *adminHandler {
booking: booking.NewAdminHandler(), booking: booking.NewAdminHandler(),
campsite: campsite.NewAdminHandler(), campsite: campsite.NewAdminHandler(),
company: company.NewAdminHandler(), company: company.NewAdminHandler(),
customer: customer.NewAdminHandler(),
home: home.NewAdminHandler(), home: home.NewAdminHandler(),
invoice: invoice.NewAdminHandler(),
legal: legal.NewAdminHandler(), legal: legal.NewAdminHandler(),
location: location.NewAdminHandler(), location: location.NewAdminHandler(),
media: media.NewAdminHandler(mediaDir), media: media.NewAdminHandler(mediaDir),
@ -85,10 +91,14 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data
h.campsite.Handler(user, company, conn).ServeHTTP(w, r) h.campsite.Handler(user, company, conn).ServeHTTP(w, r)
case "company": case "company":
h.company.Handler(user, company, conn).ServeHTTP(w, r) h.company.Handler(user, company, conn).ServeHTTP(w, r)
case "customers":
h.customer.Handler(user, company, conn).ServeHTTP(w, r)
case "home": case "home":
h.home.Handler(user, company, conn).ServeHTTP(w, r) h.home.Handler(user, company, conn).ServeHTTP(w, r)
case "legal": case "legal":
h.legal.Handler(user, company, conn).ServeHTTP(w, r) h.legal.Handler(user, company, conn).ServeHTTP(w, r)
case "invoices":
h.invoice.Handler(user, company, conn).ServeHTTP(w, r)
case "location": case "location":
h.location.Handler(user, company, conn).ServeHTTP(w, r) h.location.Handler(user, company, conn).ServeHTTP(w, r)
case "media": case "media":

View File

@ -7,6 +7,7 @@ package booking
import ( import (
"context" "context"
"dev.tandem.ws/tandem/camper/pkg/ods"
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
@ -197,16 +198,16 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user
"Holder Name", "Holder Name",
"Status", "Status",
} }
ods, err := writeTableOds(page, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error { table, err := ods.WriteTable(page, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error {
if err := writeCellString(sb, entry.Reference); err != nil { if err := ods.WriteCellString(sb, entry.Reference); err != nil {
return err return err
} }
writeCellDate(sb, entry.ArrivalDate) ods.WriteCellDate(sb, entry.ArrivalDate)
writeCellDate(sb, entry.DepartureDate) ods.WriteCellDate(sb, entry.DepartureDate)
if err := writeCellString(sb, entry.HolderName); err != nil { if err := ods.WriteCellString(sb, entry.HolderName); err != nil {
return err return err
} }
if err := writeCellString(sb, entry.StatusLabel); err != nil { if err := ods.WriteCellString(sb, entry.StatusLabel); err != nil {
return err return err
} }
return nil return nil
@ -214,7 +215,7 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user
if err != nil { if err != nil {
panic(err) panic(err)
} }
mustWriteOdsResponse(w, ods, user.Locale.Pgettext("bookings.ods", "filename")) ods.MustWriteResponse(w, table, user.Locale.Pgettext("bookings.ods", "filename"))
default: default:
template.MustRenderAdmin(w, r, user, company, "booking/index.gohtml", page) template.MustRenderAdmin(w, r, user, company, "booking/index.gohtml", page)
} }

View File

@ -71,9 +71,9 @@ func newCheckinForm(slug string) *checkInForm {
} }
func (f *checkInForm) FillFromDatabase(ctx context.Context, conn *database.Conn, l *locale.Locale) error { func (f *checkInForm) FillFromDatabase(ctx context.Context, conn *database.Conn, l *locale.Locale) error {
documentTypes := mustGetDocumentTypeOptions(ctx, conn, l) documentTypes := form.MustGetDocumentTypeOptions(ctx, conn, l)
sexes := mustGetSexOptions(ctx, conn, l) sexes := mustGetSexOptions(ctx, conn, l)
countries := mustGetCountryOptions(ctx, conn, l) countries := form.MustGetCountryOptions(ctx, conn, l)
rows, err := conn.Query(ctx, ` rows, err := conn.Query(ctx, `
select array[id_document_type_id] select array[id_document_type_id]
@ -118,10 +118,6 @@ func (f *checkInForm) FillFromDatabase(ctx context.Context, conn *database.Conn,
return nil return nil
} }
func mustGetDocumentTypeOptions(ctx context.Context, conn *database.Conn, l *locale.Locale) []*form.Option {
return form.MustGetOptions(ctx, conn, "select idt.id_document_type_id::text, coalesce(i18n.name, idt.name) as l10n_name from id_document_type as idt left join id_document_type_i18n as i18n on idt.id_document_type_id = i18n.id_document_type_id and i18n.lang_tag = $1 order by l10n_name", l.Language)
}
func mustGetSexOptions(ctx context.Context, conn *database.Conn, l *locale.Locale) []*form.Option { func mustGetSexOptions(ctx context.Context, conn *database.Conn, l *locale.Locale) []*form.Option {
return form.MustGetOptions(ctx, conn, "select sex.sex_id::text, coalesce(i18n.name, sex.name) as l10n_name from sex left join sex_i18n as i18n on sex.sex_id = i18n.sex_id and i18n.lang_tag = $1 order by l10n_name", l.Language) return form.MustGetOptions(ctx, conn, "select sex.sex_id::text, coalesce(i18n.name, sex.name) as l10n_name from sex left join sex_i18n as i18n on sex.sex_id = i18n.sex_id and i18n.lang_tag = $1 order by l10n_name", l.Language)
} }
@ -131,9 +127,9 @@ func (f *checkInForm) Parse(r *http.Request, user *auth.User, conn *database.Con
return err return err
} }
documentTypes := mustGetDocumentTypeOptions(r.Context(), conn, user.Locale) documentTypes := form.MustGetDocumentTypeOptions(r.Context(), conn, user.Locale)
sexes := mustGetSexOptions(r.Context(), conn, user.Locale) sexes := mustGetSexOptions(r.Context(), conn, user.Locale)
countries := mustGetCountryOptions(r.Context(), conn, user.Locale) countries := form.MustGetCountryOptions(r.Context(), conn, user.Locale)
guest := newGuestFormWithOptions(documentTypes, sexes, countries, nil) guest := newGuestFormWithOptions(documentTypes, sexes, countries, nil)
count := guest.count(r) count := guest.count(r)
@ -180,9 +176,9 @@ type guestForm struct {
} }
func newGuestForm(ctx context.Context, conn *database.Conn, l *locale.Locale, slug string) (*guestForm, error) { func newGuestForm(ctx context.Context, conn *database.Conn, l *locale.Locale, slug string) (*guestForm, error) {
documentTypes := mustGetDocumentTypeOptions(ctx, conn, l) documentTypes := form.MustGetDocumentTypeOptions(ctx, conn, l)
sexes := mustGetSexOptions(ctx, conn, l) sexes := mustGetSexOptions(ctx, conn, l)
countries := mustGetCountryOptions(ctx, conn, l) countries := form.MustGetCountryOptions(ctx, conn, l)
var country []string var country []string
row := conn.QueryRow(ctx, "select array[coalesce(country_code, '')] from booking where slug = $1", slug) row := conn.QueryRow(ctx, "select array[coalesce(country_code, '')] from booking where slug = $1", slug)

View File

@ -547,7 +547,7 @@ func newBookingCustomerFields(ctx context.Context, conn *database.Conn, l *local
}, },
Country: &form.Select{ Country: &form.Select{
Name: "country", Name: "country",
Options: mustGetCountryOptions(ctx, conn, l), Options: form.MustGetCountryOptions(ctx, conn, l),
}, },
Email: &form.Input{ Email: &form.Input{
Name: "email", Name: "email",
@ -561,10 +561,6 @@ func newBookingCustomerFields(ctx context.Context, conn *database.Conn, l *local
} }
} }
func mustGetCountryOptions(ctx context.Context, conn *database.Conn, l *locale.Locale) []*form.Option {
return form.MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", l.Language)
}
func (f *bookingCustomerFields) FillValues(r *http.Request) { func (f *bookingCustomerFields) FillValues(r *http.Request) {
f.FullName.FillValue(r) f.FullName.FillValue(r)
f.Address.FillValue(r) f.Address.FillValue(r)

324
pkg/customer/admin.go Normal file
View File

@ -0,0 +1,324 @@
package customer
import (
"context"
"net/http"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/form"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
type AdminHandler struct {
}
func NewAdminHandler() *AdminHandler {
return &AdminHandler{}
}
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
switch r.Method {
case http.MethodGet:
serveCustomerIndex(w, r, user, company, conn)
case http.MethodPost:
addCustomer(w, r, user, company, conn)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
}
case "new":
switch r.Method {
case http.MethodGet:
f := newCustomerForm(r.Context(), conn, user.Locale)
f.MustRender(w, r, user, company)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
default:
f := newCustomerForm(r.Context(), conn, user.Locale)
if err := f.FillFromDatabase(r.Context(), conn, head); err != nil {
if database.ErrorIsNotFound(err) {
http.NotFound(w, r)
return
}
panic(err)
}
h.customerHandler(user, company, conn, f).ServeHTTP(w, r)
}
})
}
func (h *AdminHandler) customerHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *customerForm) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
switch r.Method {
case http.MethodGet:
f.MustRender(w, r, user, company)
case http.MethodPut:
editCustomer(w, r, user, company, conn, f)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
default:
http.NotFound(w, r)
}
})
}
func serveCustomerIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
customers, err := collectCustomerEntries(r.Context(), conn, company)
if err != nil {
panic(err)
}
page := &customerIndex{
Customers: customers,
}
page.MustRender(w, r, user, company)
}
func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *auth.Company) ([]*customerEntry, error) {
rows, err := conn.Query(ctx, `
select '/admin/customers/' || slug
, name
, coalesce(email::text, '')
, coalesce(phone::text, '')
from contact
left join contact_email using (contact_id)
left join contact_phone using (contact_id)
where company_id = $1
order by name
`, company.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var customers []*customerEntry
for rows.Next() {
customer := &customerEntry{}
if err = rows.Scan(&customer.URL, &customer.Name, &customer.Email, &customer.Phone); err != nil {
return nil, err
}
customers = append(customers, customer)
}
return customers, nil
}
type customerEntry struct {
URL string
Name string
Email string
Phone string
}
type customerIndex struct {
Customers []*customerEntry
}
func (page *customerIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "customer/index.gohtml", page)
}
func addCustomer(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
f := newCustomerForm(r.Context(), conn, user.Locale)
processCustomerForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
var err error
f.Slug, err = tx.AddContact(ctx, company.ID, f.FullName.Val, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Phone.Val, f.Email.Val, f.Address.Val, f.City.Val, f.Province.Val, f.PostalCode.Val, f.Country.String())
return err
})
}
func editCustomer(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *customerForm) {
processCustomerForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
_, err := tx.EditContact(ctx, f.Slug, f.FullName.Val, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Phone.Val, f.Email.Val, f.Address.Val, f.City.Val, f.Province.Val, f.PostalCode.Val, f.Country.String())
return err
})
}
func processCustomerForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *customerForm, act func(ctx context.Context, tx *database.Tx) error) {
if err := f.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil {
panic(err)
} else if !ok {
if !httplib.IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
f.MustRender(w, r, user, company)
return
}
tx := conn.MustBegin(r.Context())
if err := act(r.Context(), tx); err == nil {
if err := tx.Commit(r.Context()); err != nil {
panic(err)
}
} else {
if err := tx.Rollback(r.Context()); err != nil {
panic(err)
}
panic(err)
}
httplib.Redirect(w, r, "/admin/customers", http.StatusSeeOther)
}
type customerForm struct {
URL string
Slug string
FullName *form.Input
IDDocumentType *form.Select
IDDocumentNumber *form.Input
Address *form.Input
City *form.Input
Province *form.Input
PostalCode *form.Input
Country *form.Select
Email *form.Input
Phone *form.Input
}
func newCustomerForm(ctx context.Context, conn *database.Conn, l *locale.Locale) *customerForm {
return &customerForm{
FullName: &form.Input{
Name: "full_name",
},
IDDocumentType: &form.Select{
Name: "id_document_type",
Options: form.MustGetDocumentTypeOptions(ctx, conn, l),
},
IDDocumentNumber: &form.Input{
Name: "id_document_number",
},
Address: &form.Input{
Name: "address",
},
City: &form.Input{
Name: "city",
},
Province: &form.Input{
Name: "province",
},
PostalCode: &form.Input{
Name: "postal_code",
},
Country: &form.Select{
Name: "country",
Options: form.MustGetCountryOptions(ctx, conn, l),
},
Email: &form.Input{
Name: "email",
},
Phone: &form.Input{
Name: "phone",
},
}
}
func (f *customerForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error {
row := conn.QueryRow(ctx, `
select '/admin/customers/' || slug
, slug
, name
, array[id_document_type_id::text]
, id_document_number
, address
, city
, province
, postal_code
, array[country_code::text]
, coalesce(email::text, '')
, coalesce(phone::text, '')
from contact as text
left join contact_email using (contact_id)
left join contact_phone using (contact_id)
where slug = $1
`, slug)
return row.Scan(
&f.URL,
&f.Slug,
&f.FullName.Val,
&f.IDDocumentType.Selected,
&f.IDDocumentNumber.Val,
&f.Address.Val,
&f.City.Val,
&f.Province.Val,
&f.PostalCode.Val,
&f.Country.Selected,
&f.Email.Val,
&f.Phone.Val,
)
}
func (f *customerForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
f.FullName.FillValue(r)
f.IDDocumentType.FillValue(r)
f.IDDocumentNumber.FillValue(r)
f.Address.FillValue(r)
f.City.FillValue(r)
f.Province.FillValue(r)
f.PostalCode.FillValue(r)
f.Country.FillValue(r)
f.Email.FillValue(r)
f.Phone.FillValue(r)
return nil
}
func (f *customerForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
v := form.NewValidator(l)
var country string
if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) {
country = f.Country.Selected[0]
}
v.CheckSelectedOptions(f.IDDocumentType, l.GettextNoop("Selected ID document type is not valid."))
v.CheckRequired(f.IDDocumentNumber, l.GettextNoop("ID document number can not be empty."))
if v.CheckRequired(f.FullName, l.GettextNoop("Full name can not be empty.")) {
v.CheckMinLength(f.FullName, 1, l.GettextNoop("Full name must have at least one letter."))
}
v.CheckRequired(f.Address, l.GettextNoop("Address can not be empty."))
v.CheckRequired(f.City, l.GettextNoop("Town or village can not be empty."))
if v.CheckRequired(f.PostalCode, l.GettextNoop("Postcode can not be empty.")) && country != "" {
if _, err := v.CheckValidPostalCode(ctx, conn, f.PostalCode, country, l.GettextNoop("This postcode is not valid.")); err != nil {
return false, err
}
}
if f.Email.Val != "" {
v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com."))
}
if f.Phone.Val != "" && country != "" {
if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil {
return false, err
}
}
return v.AllOK, nil
}
func (f *customerForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "customer/form.gohtml", f)
}

View File

@ -0,0 +1,76 @@
package database
import (
"fmt"
"github.com/jackc/pgio"
"github.com/jackc/pgtype"
)
const EditedInvoiceProductTypeName = "edited_invoice_product"
type EditedInvoiceProduct struct {
*NewInvoiceProduct
InvoiceProductId int
}
func (src EditedInvoiceProduct) EncodeBinary(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) {
typeName := EditedInvoiceProductTypeName
dt, ok := ci.DataTypeForName(typeName)
if !ok {
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
}
var invoiceProductId interface{}
if src.InvoiceProductId > 0 {
invoiceProductId = src.InvoiceProductId
}
var productId interface{}
if src.ProductId > 0 {
productId = src.ProductId
}
values := []interface{}{
invoiceProductId,
productId,
src.Name,
src.Description,
src.Price,
src.Quantity,
src.Discount,
src.Taxes,
}
ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType)
if err := ct.Set(values); err != nil {
return nil, err
}
return ct.EncodeBinary(ci, dst)
}
type EditedInvoiceProductArray []*EditedInvoiceProduct
func (src EditedInvoiceProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
typeName := EditedInvoiceProductTypeName
dt, ok := ci.DataTypeForName(typeName)
if !ok {
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
}
arrayHeader := pgtype.ArrayHeader{
ElementOID: int32(dt.OID),
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(src)), LowerBound: 1}},
}
buf = arrayHeader.EncodeBinary(ci, buf)
for _, product := range src {
sp := len(buf)
buf = pgio.AppendInt32(buf, -1)
elemBuf, err := product.EncodeBinary(ci, buf)
if err != nil {
return nil, err
}
if elemBuf != nil {
buf = elemBuf
pgio.SetInt32(buf[sp:], int32(len(buf[sp:])-4))
}
}
return buf, nil
}

View File

@ -0,0 +1,76 @@
package database
import (
"fmt"
"github.com/jackc/pgio"
"github.com/jackc/pgtype"
)
const NewInvoiceProductTypeName = "new_invoice_product"
type NewInvoiceProduct struct {
ProductId int
Name string
Description string
Price string
Quantity int
Discount float64
Taxes []int
}
func (src NewInvoiceProduct) EncodeBinary(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) {
typeName := NewInvoiceProductTypeName
dt, ok := ci.DataTypeForName(typeName)
if !ok {
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
}
var productId interface{}
if src.ProductId > 0 {
productId = src.ProductId
}
values := []interface{}{
productId,
src.Name,
src.Description,
src.Price,
src.Quantity,
src.Discount,
src.Taxes,
}
ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType)
if err := ct.Set(values); err != nil {
return nil, err
}
return ct.EncodeBinary(ci, dst)
}
type NewInvoiceProductArray []*NewInvoiceProduct
func (src NewInvoiceProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
typeName := NewInvoiceProductTypeName
dt, ok := ci.DataTypeForName(typeName)
if !ok {
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
}
arrayHeader := pgtype.ArrayHeader{
ElementOID: int32(dt.OID),
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(src)), LowerBound: 1}},
}
buf = arrayHeader.EncodeBinary(ci, buf)
for _, product := range src {
sp := len(buf)
buf = pgio.AppendInt32(buf, -1)
elemBuf, err := product.EncodeBinary(ci, buf)
if err != nil {
return nil, err
}
if elemBuf != nil {
buf = elemBuf
pgio.SetInt32(buf[sp:], int32(len(buf[sp:])-4))
}
}
return buf, nil
}

View File

@ -370,3 +370,19 @@ func (c *Conn) CheckInGuests(ctx context.Context, bookingSlug string, guests []*
_, err := c.Exec(ctx, "select check_in_guests(booking_id, $2) from booking where slug = $1", bookingSlug, CheckedInGuestArray(guests)) _, err := c.Exec(ctx, "select check_in_guests(booking_id, $2) from booking where slug = $1", bookingSlug, CheckedInGuestArray(guests))
return err return err
} }
func (c *Conn) AddInvoice(ctx context.Context, companyID int, date string, customerID int, notes string, paymentMethodID int, products NewInvoiceProductArray) (string, error) {
return c.GetText(ctx, "select add_invoice($1, $2, $3, $4, $5, $6)", companyID, date, customerID, notes, paymentMethodID, products)
}
func (c *Conn) EditInvoice(ctx context.Context, invoiceSlug string, invoiceStatus string, contactID int, notes string, paymentMethodID int, products EditedInvoiceProductArray) (string, error) {
return c.GetText(ctx, "select edit_invoice($1, $2, $3, $4, $5, $6)", invoiceSlug, invoiceStatus, contactID, notes, paymentMethodID, products)
}
func (tx *Tx) AddContact(ctx context.Context, companyID int, name string, idDocumentType string, idDocumentNumber string, phone string, email string, address string, city string, province string, postalCode string, countryCode string) (string, error) {
return tx.GetText(ctx, "select add_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", companyID, name, idDocumentType, idDocumentNumber, phone, email, address, city, province, postalCode, countryCode)
}
func (tx *Tx) EditContact(ctx context.Context, contactSlug, name string, idDocumentType string, idDocumentNumber string, phone string, email string, address string, city string, province string, postalCode string, countryCode string) (string, error) {
return tx.GetText(ctx, "select edit_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", contactSlug, name, idDocumentType, idDocumentNumber, phone, email, address, city, province, postalCode, countryCode)
}

View File

@ -49,6 +49,11 @@ func registerConnectionTypes(ctx context.Context, conn *pgx.Conn) error {
return err return err
} }
discountRateOID, err := registerType(ctx, conn, &pgtype.Numeric{}, "discount_rate")
if err != nil {
return err
}
redsysRequestType, err := pgtype.NewCompositeType( redsysRequestType, err := pgtype.NewCompositeType(
RedsysRequestTypeName, RedsysRequestTypeName,
[]pgtype.CompositeTypeField{ []pgtype.CompositeTypeField{
@ -150,6 +155,47 @@ func registerConnectionTypes(ctx context.Context, conn *pgx.Conn) error {
return err return err
} }
newInvoiceProductType, err := pgtype.NewCompositeType(
NewInvoiceProductTypeName,
[]pgtype.CompositeTypeField{
{"product_id", pgtype.Int4OID},
{"name", pgtype.TextOID},
{"description", pgtype.TextOID},
{"price", pgtype.TextOID},
{"quantity", pgtype.Int4OID},
{"discount_rate", discountRateOID},
{"tax", pgtype.Int4ArrayOID},
},
conn.ConnInfo(),
)
if err != nil {
return err
}
if _, err = registerType(ctx, conn, newInvoiceProductType, newInvoiceProductType.TypeName()); err != nil {
return err
}
editedInvoiceProductType, err := pgtype.NewCompositeType(
EditedInvoiceProductTypeName,
[]pgtype.CompositeTypeField{
{"invoice_product_id", pgtype.Int4OID},
{"product_id", pgtype.Int4OID},
{"name", pgtype.TextOID},
{"description", pgtype.TextOID},
{"price", pgtype.TextOID},
{"quantity", pgtype.Int4OID},
{"discount_rate", discountRateOID},
{"tax", pgtype.Int4ArrayOID},
},
conn.ConnInfo(),
)
if err != nil {
return err
}
if _, err = registerType(ctx, conn, editedInvoiceProductType, editedInvoiceProductType.TypeName()); err != nil {
return err
}
return nil return nil
} }

View File

@ -8,6 +8,7 @@ package form
import ( import (
"context" "context"
"database/sql/driver" "database/sql/driver"
"dev.tandem.ws/tandem/camper/pkg/locale"
"net/http" "net/http"
"strconv" "strconv"
@ -113,3 +114,11 @@ func MustGetOptions(ctx context.Context, conn *database.Conn, sql string, args .
return options return options
} }
func MustGetCountryOptions(ctx context.Context, conn *database.Conn, l *locale.Locale) []*Option {
return MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", l.Language)
}
func MustGetDocumentTypeOptions(ctx context.Context, conn *database.Conn, l *locale.Locale) []*Option {
return MustGetOptions(ctx, conn, "select idt.id_document_type_id::text, coalesce(i18n.name, idt.name) as l10n_name from id_document_type as idt left join id_document_type_i18n as i18n on idt.id_document_type_id = i18n.id_document_type_id and i18n.lang_tag = $1 order by l10n_name", l.Language)
}

View File

@ -11,9 +11,10 @@ import (
) )
const ( const (
HxLocation = "HX-Location" HxLocation = "HX-Location"
HxRedirect = "HX-Redirect" HxRedirect = "HX-Redirect"
HxRequest = "HX-Request" HxRequest = "HX-Request"
HxTriggerAfterSettle = "HX-Trigger-After-Settle"
) )
func Relocate(w http.ResponseWriter, r *http.Request, url string, code int) { func Relocate(w http.ResponseWriter, r *http.Request, url string, code int) {
@ -37,6 +38,10 @@ func Redirect(w http.ResponseWriter, r *http.Request, url string, code int) {
} }
} }
func TriggerAfterSettle(w http.ResponseWriter, trigger string) {
w.Header().Set(HxTriggerAfterSettle, trigger)
}
func IsHTMxRequest(r *http.Request) bool { func IsHTMxRequest(r *http.Request) bool {
return r.Header.Get(HxRequest) == "true" return r.Header.Get(HxRequest) == "true"
} }

1295
pkg/invoice/admin.go Normal file

File diff suppressed because it is too large Load Diff

65
pkg/invoice/ods.go Normal file
View File

@ -0,0 +1,65 @@
package invoice
import (
"sort"
"strings"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/ods"
)
func mustWriteInvoicesOds(invoices []*IndexEntry, taxes map[int]taxMap, taxColumns map[int]string, company *auth.Company, locale *locale.Locale) []byte {
taxIDs := extractTaxIDs(taxColumns)
columns := make([]string, 6+len(taxIDs))
columns[0] = "Date"
columns[1] = "Invoice Num."
columns[2] = "Customer"
columns[3] = "Status"
i := 4
for _, taxID := range taxIDs {
columns[i] = taxColumns[taxID]
i++
}
columns[i] = "Amount"
table, err := ods.WriteTable(invoices, columns, locale, func(sb *strings.Builder, invoice *IndexEntry) error {
ods.WriteCellDate(sb, invoice.Date)
if err := ods.WriteCellString(sb, invoice.Number); err != nil {
return err
}
if err := ods.WriteCellString(sb, invoice.CustomerName); err != nil {
return err
}
if err := ods.WriteCellString(sb, invoice.StatusLabel); err != nil {
return err
}
writeTaxes(sb, taxes[invoice.ID], taxIDs, company, locale)
ods.WriteCellFloat(sb, invoice.Total, company, locale)
return nil
})
if err != nil {
panic(err)
}
return table
}
func extractTaxIDs(taxColumns map[int]string) []int {
taxIDs := make([]int, len(taxColumns))
i := 0
for k := range taxColumns {
taxIDs[i] = k
i++
}
sort.Ints(taxIDs[:])
return taxIDs
}
func writeTaxes(sb *strings.Builder, taxes taxMap, taxIDs []int, company *auth.Company, locale *locale.Locale) {
for _, taxID := range taxIDs {
var amount string
if taxes != nil {
amount = taxes[taxID]
}
ods.WriteCellFloat(sb, amount, company, locale)
}
}

75
pkg/invoice/pdf.go Normal file
View File

@ -0,0 +1,75 @@
package invoice
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/template"
)
func mustWriteInvoicesPdf(r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, slugs []string) []byte {
buf := new(bytes.Buffer)
w := zip.NewWriter(buf)
for _, slug := range slugs {
inv := mustGetInvoice(r.Context(), conn, company, slug)
if inv == nil {
continue
}
f, err := w.Create(fmt.Sprintf("%s-%s.pdf", inv.Number, template.Slugify(inv.Invoicee.Name)))
if err != nil {
panic(err)
}
mustWriteInvoicePdf(f, r, user, company, inv)
}
mustClose(w)
return buf.Bytes()
}
func mustWriteInvoicePdf(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, inv *invoice) {
cmd := exec.Command("weasyprint", "--stylesheet", "web/static/invoice.css", "-", "-")
var stderr bytes.Buffer
cmd.Stderr = &stderr
stdin, err := cmd.StdinPipe()
if err != nil {
panic(err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
panic(err)
}
defer func() {
err := stdout.Close()
if !errors.Is(err, os.ErrClosed) {
panic(err)
}
}()
if err = cmd.Start(); err != nil {
panic(err)
}
go func() {
defer mustClose(stdin)
template.MustRenderAdmin(stdin, r, user, company, "invoice/view.gohtml", inv)
}()
if _, err = io.Copy(w, stdout); err != nil {
panic(err)
}
if err := cmd.Wait(); err != nil {
log.Printf("ERR - %v\n", stderr.String())
panic(err)
}
}
func mustClose(closer io.Closer) {
if err := closer.Close(); err != nil {
panic(err)
}
}

View File

@ -1,8 +1,10 @@
package booking package ods
import ( import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/template"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"net/http" "net/http"
@ -44,7 +46,7 @@ const (
` `
) )
func writeTableOds[K interface{}](rows []*K, columns []string, locale *locale.Locale, writeRow func(*strings.Builder, *K) error) ([]byte, error) { func WriteTable[K interface{}](rows []*K, columns []string, locale *locale.Locale, writeRow func(*strings.Builder, *K) error) ([]byte, error) {
var sb strings.Builder var sb strings.Builder
sb.WriteString(`<?xml version="1.0" encoding="UTF-8"?> sb.WriteString(`<?xml version="1.0" encoding="UTF-8"?>
@ -90,7 +92,7 @@ func writeTableOds[K interface{}](rows []*K, columns []string, locale *locale.Lo
sb.WriteString(` <table:table-row table:style-name="ro1"> sb.WriteString(` <table:table-row table:style-name="ro1">
`) `)
for _, t := range columns { for _, t := range columns {
if err := writeCellString(&sb, locale.GetC(t, "header")); err != nil { if err := WriteCellString(&sb, locale.GetC(t, "header")); err != nil {
return nil, err return nil, err
} }
} }
@ -148,7 +150,7 @@ func writeOdsFile(ods *zip.Writer, name string, content string, method uint16) e
return err return err
} }
func writeCellString(sb *strings.Builder, s string) error { func WriteCellString(sb *strings.Builder, s string) error {
sb.WriteString(` <table:table-cell office:value-type="string" calcext:value-type="string"><text:p>`) sb.WriteString(` <table:table-cell office:value-type="string" calcext:value-type="string"><text:p>`)
if err := xml.EscapeText(sb, []byte(s)); err != nil { if err := xml.EscapeText(sb, []byte(s)); err != nil {
return err return err
@ -157,11 +159,15 @@ func writeCellString(sb *strings.Builder, s string) error {
return nil return nil
} }
func writeCellDate(sb *strings.Builder, t time.Time) { func WriteCellDate(sb *strings.Builder, t time.Time) {
sb.WriteString(fmt.Sprintf(" <table:table-cell table:style-name=\"ce1\" office:value-type=\"date\" office:date-value=\"%s\" calcext:value-type=\"date\"><text:p>%s</text:p></table:table-cell>\n", t.Format(database.ISODateFormat), t.Format("02/01/06"))) sb.WriteString(fmt.Sprintf(" <table:table-cell table:style-name=\"ce1\" office:value-type=\"date\" office:date-value=\"%s\" calcext:value-type=\"date\"><text:p>%s</text:p></table:table-cell>\n", t.Format(database.ISODateFormat), t.Format("02/01/06")))
} }
func mustWriteOdsResponse(w http.ResponseWriter, ods []byte, filename string) { func WriteCellFloat(sb *strings.Builder, s string, company *auth.Company, locale *locale.Locale) {
sb.WriteString(fmt.Sprintf(" <table:table-cell office:value-type=\"float\" office:value=\"%s\" calcext:value-type=\"float\"><text:p>%s</text:p></table:table-cell>\n", s, template.FormatPrice(s, locale.Language, locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol)))
}
func MustWriteResponse(w http.ResponseWriter, ods []byte, filename string) {
w.Header().Set("Content-Type", "application/vnd.oasis.opendocument.spreadsheet") w.Header().Set("Content-Type", "application/vnd.oasis.opendocument.spreadsheet")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)

View File

@ -117,6 +117,9 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ
"formatDateAttr": func(time time.Time) string { "formatDateAttr": func(time time.Time) string {
return time.Format(database.ISODateFormat) return time.Format(database.ISODateFormat)
}, },
"formatPercent": func(value int) string {
return fmt.Sprintf("%d %%", value)
},
"today": func() string { "today": func() string {
return time.Now().Format(database.ISODateFormat) return time.Now().Format(database.ISODateFormat)
}, },
@ -129,22 +132,35 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ
"dec": func(i int) int { "dec": func(i int) int {
return i - 1 return i - 1
}, },
"add": func(y, x int) int {
return x + y
},
"sub": func(y, x int) int {
return x - y
},
"int": func(v interface{}) int { "int": func(v interface{}) int {
switch v := v.(type) { switch v := v.(type) {
case int: case int:
return v return v
case bool:
if v {
return 1
} else {
return 0
}
case time.Weekday: case time.Weekday:
return int(v) return int(v)
case time.Month: case time.Month:
return int(v) return int(v)
default: default:
panic(fmt.Errorf("Could not convert to integer")) panic(fmt.Errorf("could not convert to integer"))
} }
}, },
"hexToDec": func(s string) int { "hexToDec": func(s string) int {
num, _ := strconv.ParseInt(s, 16, 0) num, _ := strconv.ParseInt(s, 16, 0)
return int(num) return int(num)
}, },
"slugify": Slugify,
}) })
templates = append(templates, "form.gohtml") templates = append(templates, "form.gohtml")
files := make([]string, len(templates)) files := make([]string, len(templates))

23
pkg/template/slug.go Normal file
View File

@ -0,0 +1,23 @@
package template
import (
"regexp"
"strings"
"github.com/rainycape/unidecode"
)
var (
nonValidChars = regexp.MustCompile("[^a-z0-9-_]")
multipleDashes = regexp.MustCompile("-+")
)
func Slugify(s string) (slug string) {
slug = strings.TrimSpace(s)
slug = unidecode.Unidecode(slug)
slug = strings.ToLower(slug)
slug = nonValidChars.ReplaceAllString(slug, "-")
slug = multipleDashes.ReplaceAllString(slug, "-")
slug = strings.Trim(slug, "-_")
return slug
}

614
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-04-26 16:53+0200\n" "POT-Creation-Date: 2024-04-28 20:05+0200\n"
"PO-Revision-Date: 2024-02-06 10:04+0100\n" "PO-Revision-Date: 2024-02-06 10:04+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -239,6 +239,7 @@ msgstr "Opcions del tipus dallotjament"
#: web/templates/mail/payment/details.gotxt:39 #: web/templates/mail/payment/details.gotxt:39
#: web/templates/public/booking/fields.gohtml:146 #: web/templates/public/booking/fields.gohtml:146
#: web/templates/admin/payment/details.gohtml:140 #: web/templates/admin/payment/details.gohtml:140
#: web/templates/admin/customer/form.gohtml:28
#: web/templates/admin/booking/fields.gohtml:188 #: web/templates/admin/booking/fields.gohtml:188
msgctxt "title" msgctxt "title"
msgid "Customer Details" msgid "Customer Details"
@ -247,6 +248,7 @@ msgstr "Detalls del client"
#: web/templates/mail/payment/details.gotxt:41 #: web/templates/mail/payment/details.gotxt:41
#: web/templates/public/booking/fields.gohtml:149 #: web/templates/public/booking/fields.gohtml:149
#: web/templates/admin/payment/details.gohtml:143 #: web/templates/admin/payment/details.gohtml:143
#: web/templates/admin/customer/form.gohtml:31
#: web/templates/admin/booking/fields.gohtml:191 #: web/templates/admin/booking/fields.gohtml:191
msgctxt "input" msgctxt "input"
msgid "Full name" msgid "Full name"
@ -255,6 +257,7 @@ msgstr "Nom i cognoms"
#: web/templates/mail/payment/details.gotxt:42 #: web/templates/mail/payment/details.gotxt:42
#: web/templates/public/booking/fields.gohtml:158 #: web/templates/public/booking/fields.gohtml:158
#: web/templates/admin/payment/details.gohtml:147 #: web/templates/admin/payment/details.gohtml:147
#: web/templates/admin/customer/form.gohtml:69
#: web/templates/admin/taxDetails.gohtml:69 #: web/templates/admin/taxDetails.gohtml:69
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
@ -263,6 +266,7 @@ msgstr "Adreça"
#: web/templates/mail/payment/details.gotxt:43 #: web/templates/mail/payment/details.gotxt:43
#: web/templates/public/booking/fields.gohtml:167 #: web/templates/public/booking/fields.gohtml:167
#: web/templates/admin/payment/details.gohtml:151 #: web/templates/admin/payment/details.gohtml:151
#: web/templates/admin/customer/form.gohtml:105
#: web/templates/admin/taxDetails.gohtml:93 #: web/templates/admin/taxDetails.gohtml:93
msgctxt "input" msgctxt "input"
msgid "Postcode" msgid "Postcode"
@ -278,6 +282,7 @@ msgstr "Població"
#: web/templates/mail/payment/details.gotxt:45 #: web/templates/mail/payment/details.gotxt:45
#: web/templates/public/booking/fields.gohtml:187 #: web/templates/public/booking/fields.gohtml:187
#: web/templates/admin/payment/details.gohtml:159 #: web/templates/admin/payment/details.gohtml:159
#: web/templates/admin/customer/form.gohtml:117
#: web/templates/admin/taxDetails.gohtml:101 #: web/templates/admin/taxDetails.gohtml:101
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
@ -411,11 +416,16 @@ msgid "Order Number"
msgstr "Número de comanda" msgstr "Número de comanda"
#: web/templates/public/payment/details.gohtml:8 #: web/templates/public/payment/details.gohtml:8
#: web/templates/admin/invoice/index.gohtml:103
#: web/templates/admin/invoice/view.gohtml:26
msgctxt "title" msgctxt "title"
msgid "Date" msgid "Date"
msgstr "Data" msgstr "Data"
#: web/templates/public/payment/details.gohtml:12 #: web/templates/public/payment/details.gohtml:12
#: web/templates/admin/invoice/form.gohtml:119
#: web/templates/admin/invoice/view.gohtml:63
#: web/templates/admin/invoice/view.gohtml:103
msgctxt "title" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
@ -579,6 +589,11 @@ msgctxt "input"
msgid "Year" msgid "Year"
msgstr "Any" msgstr "Any"
#: web/templates/public/form.gohtml:83 web/templates/admin/form.gohtml:83
msgctxt "action"
msgid "Filters"
msgstr "Filtres"
#: web/templates/public/campsite/type.gohtml:49 #: web/templates/public/campsite/type.gohtml:49
#: web/templates/public/booking/fields.gohtml:278 #: web/templates/public/booking/fields.gohtml:278
msgctxt "action" msgctxt "action"
@ -929,7 +944,7 @@ msgstr "Menú"
#: web/templates/admin/campsite/type/option/form.gohtml:16 #: web/templates/admin/campsite/type/option/form.gohtml:16
#: web/templates/admin/campsite/type/option/index.gohtml:10 #: web/templates/admin/campsite/type/option/index.gohtml:10
#: web/templates/admin/campsite/type/index.gohtml:10 #: web/templates/admin/campsite/type/index.gohtml:10
#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:95 #: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:101
#: web/templates/admin/booking/fields.gohtml:266 #: web/templates/admin/booking/fields.gohtml:266
msgctxt "title" msgctxt "title"
msgid "Campsites" msgid "Campsites"
@ -1004,13 +1019,15 @@ msgid "Campground map"
msgstr "Mapa del càmping" msgstr "Mapa del càmping"
#: web/templates/public/booking/fields.gohtml:176 #: web/templates/public/booking/fields.gohtml:176
#: web/templates/admin/customer/form.gohtml:81
msgctxt "input" msgctxt "input"
msgid "Town or village" msgid "Town or village"
msgstr "Població" msgstr "Població"
#: web/templates/public/booking/fields.gohtml:193 #: web/templates/public/booking/fields.gohtml:193
#: web/templates/admin/customer/form.gohtml:121
#: web/templates/admin/booking/fields.gohtml:204 #: web/templates/admin/booking/fields.gohtml:204
#: web/templates/admin/booking/guest.gohtml:109 #: web/templates/admin/booking/guest.gohtml:111
msgid "Choose a country" msgid "Choose a country"
msgstr "Esculli un país" msgstr "Esculli un país"
@ -1164,6 +1181,7 @@ msgstr "Àlies"
#: web/templates/admin/campsite/type/form.gohtml:51 #: web/templates/admin/campsite/type/form.gohtml:51
#: web/templates/admin/campsite/type/option/form.gohtml:41 #: web/templates/admin/campsite/type/option/form.gohtml:41
#: web/templates/admin/season/form.gohtml:50 #: web/templates/admin/season/form.gohtml:50
#: web/templates/admin/invoice/product-form.gohtml:16
#: web/templates/admin/services/form.gohtml:53 #: web/templates/admin/services/form.gohtml:53
#: web/templates/admin/profile.gohtml:29 #: web/templates/admin/profile.gohtml:29
#: web/templates/admin/surroundings/form.gohtml:41 #: web/templates/admin/surroundings/form.gohtml:41
@ -1188,6 +1206,8 @@ msgstr "Contingut"
#: web/templates/admin/campsite/type/form.gohtml:287 #: web/templates/admin/campsite/type/form.gohtml:287
#: web/templates/admin/campsite/type/option/form.gohtml:98 #: web/templates/admin/campsite/type/option/form.gohtml:98
#: web/templates/admin/season/form.gohtml:73 #: web/templates/admin/season/form.gohtml:73
#: web/templates/admin/customer/form.gohtml:153
#: web/templates/admin/invoice/form.gohtml:137
#: web/templates/admin/services/form.gohtml:81 #: web/templates/admin/services/form.gohtml:81
#: web/templates/admin/surroundings/form.gohtml:69 #: web/templates/admin/surroundings/form.gohtml:69
#: web/templates/admin/surroundings/index.gohtml:58 #: web/templates/admin/surroundings/index.gohtml:58
@ -1211,6 +1231,7 @@ msgstr "Actualitza"
#: web/templates/admin/campsite/type/form.gohtml:289 #: web/templates/admin/campsite/type/form.gohtml:289
#: web/templates/admin/campsite/type/option/form.gohtml:100 #: web/templates/admin/campsite/type/option/form.gohtml:100
#: web/templates/admin/season/form.gohtml:75 #: web/templates/admin/season/form.gohtml:75
#: web/templates/admin/customer/form.gohtml:155
#: web/templates/admin/services/form.gohtml:83 #: web/templates/admin/services/form.gohtml:83
#: web/templates/admin/surroundings/form.gohtml:71 #: web/templates/admin/surroundings/form.gohtml:71
#: web/templates/admin/amenity/feature/form.gohtml:67 #: web/templates/admin/amenity/feature/form.gohtml:67
@ -1232,6 +1253,7 @@ msgstr "Afegeix text legal"
#: web/templates/admin/campsite/type/option/index.gohtml:30 #: web/templates/admin/campsite/type/option/index.gohtml:30
#: web/templates/admin/campsite/type/index.gohtml:29 #: web/templates/admin/campsite/type/index.gohtml:29
#: web/templates/admin/season/index.gohtml:29 #: web/templates/admin/season/index.gohtml:29
#: web/templates/admin/customer/index.gohtml:19
#: web/templates/admin/user/index.gohtml:20 #: web/templates/admin/user/index.gohtml:20
#: web/templates/admin/surroundings/index.gohtml:83 #: web/templates/admin/surroundings/index.gohtml:83
#: web/templates/admin/amenity/feature/index.gohtml:30 #: web/templates/admin/amenity/feature/index.gohtml:30
@ -1725,6 +1747,7 @@ msgid "Per night"
msgstr "Per nit" msgstr "Per nit"
#: web/templates/admin/campsite/type/option/form.gohtml:84 #: web/templates/admin/campsite/type/option/form.gohtml:84
#: web/templates/admin/invoice/product-form.gohtml:29
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Preu" msgstr "Preu"
@ -1824,12 +1847,316 @@ msgctxt "action"
msgid "Cancel" msgid "Cancel"
msgstr "Canceŀla" msgstr "Canceŀla"
#: web/templates/admin/customer/form.gohtml:8
msgctxt "title"
msgid "Edit Customer"
msgstr "Edició del client"
#: web/templates/admin/customer/form.gohtml:10
msgctxt "title"
msgid "New Customer"
msgstr "Nou client"
#: web/templates/admin/customer/form.gohtml:15
#: web/templates/admin/invoice/index.gohtml:105
msgctxt "title"
msgid "Customer"
msgstr "Client"
#: web/templates/admin/customer/form.gohtml:44
#: web/templates/admin/booking/guest.gohtml:8
msgctxt "input"
msgid "ID document number"
msgstr "Número de document didentitat"
#: web/templates/admin/customer/form.gohtml:56
#: web/templates/admin/booking/guest.gohtml:20
msgctxt "input"
msgid "ID document type"
msgstr "Tipus de document"
#: web/templates/admin/customer/form.gohtml:61
#: web/templates/admin/booking/guest.gohtml:25
msgid "Choose an ID document type"
msgstr "Esculli un tipus de document"
#: web/templates/admin/customer/form.gohtml:93
#: web/templates/admin/taxDetails.gohtml:85
msgctxt "input"
msgid "Province"
msgstr "Província"
#: web/templates/admin/customer/form.gohtml:129
#: web/templates/admin/booking/fields.gohtml:239
msgctxt "input"
msgid "Email (optional)"
msgstr "Correu-e (opcional)"
#: web/templates/admin/customer/form.gohtml:140
#: web/templates/admin/booking/fields.gohtml:248
#: web/templates/admin/booking/guest.gohtml:119
msgctxt "input"
msgid "Phone (optional)"
msgstr "Telèfon (opcional)"
#: web/templates/admin/customer/index.gohtml:6
#: web/templates/admin/layout.gohtml:95
msgctxt "title"
msgid "Customers"
msgstr "Clients"
#: web/templates/admin/customer/index.gohtml:14
msgctxt "action"
msgid "Add Customer"
msgstr "Afegeix client"
#: web/templates/admin/customer/index.gohtml:20
#: web/templates/admin/user/login-attempts.gohtml:20
#: web/templates/admin/user/index.gohtml:21
msgctxt "header"
msgid "Email"
msgstr "Correu-e"
#: web/templates/admin/customer/index.gohtml:21
msgctxt "header"
msgid "Phone"
msgstr "Telèfon"
#: web/templates/admin/customer/index.gohtml:33
msgid "No customer found."
msgstr "No sha trobat cap client."
#: web/templates/admin/dashboard.gohtml:6 #: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:89 #: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:89
msgctxt "title" msgctxt "title"
msgid "Dashboard" msgid "Dashboard"
msgstr "Tauler" msgstr "Tauler"
#: web/templates/admin/invoice/product-form.gohtml:11
#: web/templates/admin/booking/guest.gohtml:5
msgctxt "action"
msgid "Remove"
msgstr "Esborra"
#: web/templates/admin/invoice/product-form.gohtml:44
msgctxt "input"
msgid "Quantity"
msgstr "Quantitat"
#: web/templates/admin/invoice/product-form.gohtml:58
msgctxt "input"
msgid "Discount (%)"
msgstr "Descompte (%)"
#: web/templates/admin/invoice/product-form.gohtml:73
msgctxt "input"
msgid "Taxes"
msgstr "Imposts"
#: web/templates/admin/invoice/product-form.gohtml:79
msgid "Select a TAX"
msgstr "Escolliu un impost"
#: web/templates/admin/invoice/form.gohtml:4
msgctxt "title"
msgid "Edit Invoice “%s”"
msgstr "Edició de la factura «%s»"
#: web/templates/admin/invoice/form.gohtml:6
msgctxt "title"
msgid "New Invoice"
msgstr "Nova factura"
#: web/templates/admin/invoice/form.gohtml:15
#: web/templates/admin/invoice/index.gohtml:2
#: web/templates/admin/invoice/view.gohtml:6
#: web/templates/admin/layout.gohtml:98
msgctxt "title"
msgid "Invoices"
msgstr "Factures"
#: web/templates/admin/invoice/form.gohtml:32
msgid "Product “%s” removed"
msgstr "Sha esborrat el producte «%s»"
#: web/templates/admin/invoice/form.gohtml:36
msgctxt "action"
msgid "Undo"
msgstr "Desfes"
#: web/templates/admin/invoice/form.gohtml:51
#: web/templates/admin/invoice/index.gohtml:39
msgctxt "input"
msgid "Customer"
msgstr "Client"
#: web/templates/admin/invoice/form.gohtml:56
msgid "Select a customer"
msgstr "Esculliu un client"
#: web/templates/admin/invoice/form.gohtml:64
msgctxt "input"
msgid "Invoice date"
msgstr "Data de la factura"
#: web/templates/admin/invoice/form.gohtml:77
#: web/templates/admin/invoice/index.gohtml:51
msgctxt "input"
msgid "Invoice status"
msgstr "Estat de la factura"
#: web/templates/admin/invoice/form.gohtml:92
msgctxt "input"
msgid "Notes (optional)"
msgstr "Notes (opcional)"
#: web/templates/admin/invoice/form.gohtml:109
#: web/templates/admin/invoice/view.gohtml:59
msgctxt "title"
msgid "Subtotal"
msgstr "Subtotal"
#: web/templates/admin/invoice/form.gohtml:133
msgctxt "action"
msgid "Add products"
msgstr "Afegeix productes"
#: web/templates/admin/invoice/form.gohtml:140
msgctxt "action"
msgid "Save"
msgstr "Desa"
#: web/templates/admin/invoice/index.gohtml:25
msgctxt "action"
msgid "Download invoices"
msgstr "Descarrega factures"
#: web/templates/admin/invoice/index.gohtml:28
msgctxt "action"
msgid "Export list"
msgstr "Exporta llista"
#: web/templates/admin/invoice/index.gohtml:43
msgid "All customers"
msgstr "Tots els clients"
#: web/templates/admin/invoice/index.gohtml:55
msgid "All statuses"
msgstr "Tots els estats"
#: web/templates/admin/invoice/index.gohtml:63
msgctxt "input"
msgid "From date"
msgstr "De la data"
#: web/templates/admin/invoice/index.gohtml:72
msgctxt "input"
msgid "To date"
msgstr "A la data"
#: web/templates/admin/invoice/index.gohtml:81
msgctxt "input"
msgid "Invoice number"
msgstr "Número de factura"
#: web/templates/admin/invoice/index.gohtml:91
msgctxt "action"
msgid "Filter"
msgstr "Filtra"
#: web/templates/admin/invoice/index.gohtml:94
msgctxt "action"
msgid "Reset"
msgstr "Restableix"
#: web/templates/admin/invoice/index.gohtml:97
msgctxt "action"
msgid "Add invoice"
msgstr "Afegeix factura"
#: web/templates/admin/invoice/index.gohtml:102
msgctxt "invoice"
msgid "All"
msgstr "Totes"
#: web/templates/admin/invoice/index.gohtml:104
msgctxt "title"
msgid "Invoice Num."
msgstr "Núm. de factura"
#: web/templates/admin/invoice/index.gohtml:106
msgctxt "title"
msgid "Status"
msgstr "Estat"
#: web/templates/admin/invoice/index.gohtml:107
msgctxt "title"
msgid "Download"
msgstr "Descàrrega"
#: web/templates/admin/invoice/index.gohtml:108
msgctxt "title"
msgid "Amount"
msgstr "Import"
#: web/templates/admin/invoice/index.gohtml:115
msgctxt "action"
msgid "Select invoice %v"
msgstr "Selecciona la factura %v"
#: web/templates/admin/invoice/index.gohtml:144
msgctxt "action"
msgid "Download invoice %s"
msgstr "Descarrega la factura %s"
#: web/templates/admin/invoice/index.gohtml:154
msgid "No invoices added yet."
msgstr "No sha afegit cap factura encara."
#: web/templates/admin/invoice/index.gohtml:161
msgid "Total"
msgstr "Total"
#: web/templates/admin/invoice/view.gohtml:2
msgctxt "title"
msgid "Invoice %s"
msgstr "Factura %s"
#: web/templates/admin/invoice/view.gohtml:15
msgctxt "action"
msgid "Edit"
msgstr "Edita"
#: web/templates/admin/invoice/view.gohtml:18
msgctxt "action"
msgid "Download invoice"
msgstr "Descarrega factura"
#: web/templates/admin/invoice/view.gohtml:53
msgctxt "title"
msgid "Concept"
msgstr "Concepte"
#: web/templates/admin/invoice/view.gohtml:54
msgctxt "title"
msgid "Price"
msgstr "Preu"
#: web/templates/admin/invoice/view.gohtml:56
msgctxt "title"
msgid "Discount"
msgstr "Descompte"
#: web/templates/admin/invoice/view.gohtml:58
msgctxt "title"
msgid "Units"
msgstr "Unitats"
#: web/templates/admin/invoice/view.gohtml:93
msgctxt "title"
msgid "Tax Base"
msgstr "Base imposable"
#: web/templates/admin/login.gohtml:6 web/templates/admin/login.gohtml:18 #: web/templates/admin/login.gohtml:6 web/templates/admin/login.gohtml:18
msgctxt "title" msgctxt "title"
msgid "Login" msgid "Login"
@ -1928,12 +2255,6 @@ msgctxt "title"
msgid "Users" msgid "Users"
msgstr "Usuaris" msgstr "Usuaris"
#: web/templates/admin/user/login-attempts.gohtml:20
#: web/templates/admin/user/index.gohtml:21
msgctxt "header"
msgid "Email"
msgstr "Correu-e"
#: web/templates/admin/user/login-attempts.gohtml:21 #: web/templates/admin/user/login-attempts.gohtml:21
msgctxt "header" msgctxt "header"
msgid "IP Address" msgid "IP Address"
@ -1985,11 +2306,6 @@ msgctxt "input"
msgid "Trade Name" msgid "Trade Name"
msgstr "Nom comercial" msgstr "Nom comercial"
#: web/templates/admin/taxDetails.gohtml:85
msgctxt "input"
msgid "Province"
msgstr "Província"
#: web/templates/admin/taxDetails.gohtml:111 #: web/templates/admin/taxDetails.gohtml:111
msgctxt "input" msgctxt "input"
msgid "Currency" msgid "Currency"
@ -2214,11 +2530,11 @@ msgctxt "title"
msgid "Bookings" msgid "Bookings"
msgstr "Reserves" msgstr "Reserves"
#: web/templates/admin/layout.gohtml:101 #: web/templates/admin/layout.gohtml:107
msgid "Breadcrumb" msgid "Breadcrumb"
msgstr "Fil dAriadna" msgstr "Fil dAriadna"
#: web/templates/admin/layout.gohtml:113 #: web/templates/admin/layout.gohtml:119
msgid "Camper Version: %s" msgid "Camper Version: %s"
msgstr "Camper versió: %s" msgstr "Camper versió: %s"
@ -2340,7 +2656,7 @@ msgid "Country (optional)"
msgstr "País (opcional)" msgstr "País (opcional)"
#: web/templates/admin/booking/fields.gohtml:212 #: web/templates/admin/booking/fields.gohtml:212
#: web/templates/admin/booking/guest.gohtml:128 #: web/templates/admin/booking/guest.gohtml:130
msgctxt "input" msgctxt "input"
msgid "Address (optional)" msgid "Address (optional)"
msgstr "Adreça (opcional)" msgstr "Adreça (opcional)"
@ -2355,17 +2671,6 @@ msgctxt "input"
msgid "Town or village (optional)" msgid "Town or village (optional)"
msgstr "Població (opcional)" msgstr "Població (opcional)"
#: web/templates/admin/booking/fields.gohtml:239
msgctxt "input"
msgid "Email (optional)"
msgstr "Correu-e (opcional)"
#: web/templates/admin/booking/fields.gohtml:248
#: web/templates/admin/booking/guest.gohtml:117
msgctxt "input"
msgid "Phone (optional)"
msgstr "Telèfon (opcional)"
#: web/templates/admin/booking/form.gohtml:8 #: web/templates/admin/booking/form.gohtml:8
msgctxt "title" msgctxt "title"
msgid "Edit Booking" msgid "Edit Booking"
@ -2430,60 +2735,41 @@ msgstr "Nom del titular"
msgid "No booking found." msgid "No booking found."
msgstr "No sha trobat cap reserva." msgstr "No sha trobat cap reserva."
#: web/templates/admin/booking/guest.gohtml:5
msgctxt "action"
msgid "Remove"
msgstr "Esborra"
#: web/templates/admin/booking/guest.gohtml:8
msgctxt "input"
msgid "ID document number"
msgstr "Número de document didentitat"
#: web/templates/admin/booking/guest.gohtml:20
msgctxt "input"
msgid "ID document type"
msgstr "Tipus de document"
#: web/templates/admin/booking/guest.gohtml:25
msgid "Choose an ID document type"
msgstr "Esculli un tipus de document"
#: web/templates/admin/booking/guest.gohtml:33 #: web/templates/admin/booking/guest.gohtml:33
msgctxt "input" msgctxt "input"
msgid "ID document issue date (if any)" msgid "ID document issue date (if any)"
msgstr "Data dexpedició (si hi consta)" msgstr "Data dexpedició (si hi consta)"
#: web/templates/admin/booking/guest.gohtml:44 #: web/templates/admin/booking/guest.gohtml:45
msgctxt "input" msgctxt "input"
msgid "First surname" msgid "First surname"
msgstr "Primer cognom" msgstr "Primer cognom"
#: web/templates/admin/booking/guest.gohtml:56 #: web/templates/admin/booking/guest.gohtml:57
msgctxt "input" msgctxt "input"
msgid "Second surname (if has one)" msgid "Second surname (if has one)"
msgstr "Segon cognom (si en té)" msgstr "Segon cognom (si en té)"
#: web/templates/admin/booking/guest.gohtml:67 #: web/templates/admin/booking/guest.gohtml:68
msgctxt "input" msgctxt "input"
msgid "Given name" msgid "Given name"
msgstr "Nom" msgstr "Nom"
#: web/templates/admin/booking/guest.gohtml:79 #: web/templates/admin/booking/guest.gohtml:80
msgctxt "input" msgctxt "input"
msgid "Sex" msgid "Sex"
msgstr "Sexe" msgstr "Sexe"
#: web/templates/admin/booking/guest.gohtml:84 #: web/templates/admin/booking/guest.gohtml:85
msgid "Choose a sex" msgid "Choose a sex"
msgstr "Esculli un sexe" msgstr "Esculli un sexe"
#: web/templates/admin/booking/guest.gohtml:92 #: web/templates/admin/booking/guest.gohtml:93
msgctxt "input" msgctxt "input"
msgid "Birthdate" msgid "Birthdate"
msgstr "Data de naixement" msgstr "Data de naixement"
#: web/templates/admin/booking/guest.gohtml:104 #: web/templates/admin/booking/guest.gohtml:106
msgctxt "input" msgctxt "input"
msgid "Nationality" msgid "Nationality"
msgstr "Nacionalitat" msgstr "Nacionalitat"
@ -2553,8 +2839,9 @@ msgstr "Rebut amb èxit el pagament de la reserva"
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365 #: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365
#: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577 #: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
#: pkg/campsite/feature.go:269 pkg/season/admin.go:411 #: pkg/campsite/feature.go:269 pkg/season/admin.go:411
#: pkg/services/admin.go:316 pkg/surroundings/admin.go:340 #: pkg/invoice/admin.go:1092 pkg/services/admin.go:316
#: pkg/amenity/feature.go:269 pkg/amenity/admin.go:283 #: pkg/surroundings/admin.go:340 pkg/amenity/feature.go:269
#: pkg/amenity/admin.go:283
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc." msgstr "No podeu deixar el nom en blanc."
@ -2589,12 +2876,12 @@ msgid "Slide image must be an image media type."
msgstr "La imatge de la diapositiva ha de ser un mèdia de tipus imatge." msgstr "La imatge de la diapositiva ha de ser un mèdia de tipus imatge."
#: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:224 #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:224
#: pkg/booking/public.go:596 #: pkg/booking/public.go:592
msgid "Email can not be empty." msgid "Email can not be empty."
msgstr "No podeu deixar el correu-e en blanc." msgstr "No podeu deixar el correu-e en blanc."
#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:225 #: pkg/app/login.go:57 pkg/app/user.go:247 pkg/customer/admin.go:312
#: pkg/booking/admin.go:437 pkg/booking/public.go:597 #: pkg/company/admin.go:225 pkg/booking/admin.go:438 pkg/booking/public.go:593
msgid "This email is not valid. It should be like name@domain.com." msgid "This email is not valid. It should be like name@domain.com."
msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com." msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com."
@ -2623,7 +2910,7 @@ msgstr "Lidioma escollit no és vàlid."
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida."
#: pkg/app/admin.go:73 #: pkg/app/admin.go:79
msgid "Access forbidden" msgid "Access forbidden"
msgstr "Accés prohibit" msgstr "Accés prohibit"
@ -2651,15 +2938,15 @@ msgstr "El valor del màxim ha de ser un número enter."
msgid "Maximum must be equal or greater than minimum." msgid "Maximum must be equal or greater than minimum."
msgstr "El valor del màxim ha de ser igual o superir al del mínim." msgstr "El valor del màxim ha de ser igual o superir al del mínim."
#: pkg/campsite/types/option.go:382 #: pkg/campsite/types/option.go:382 pkg/invoice/admin.go:1093
msgid "Price can not be empty." msgid "Price can not be empty."
msgstr "No podeu deixar el preu en blanc." msgstr "No podeu deixar el preu en blanc."
#: pkg/campsite/types/option.go:383 #: pkg/campsite/types/option.go:383 pkg/invoice/admin.go:1094
msgid "Price must be a decimal number." msgid "Price must be a decimal number."
msgstr "El preu ha de ser un número decimal." msgstr "El preu ha de ser un número decimal."
#: pkg/campsite/types/option.go:384 #: pkg/campsite/types/option.go:384 pkg/invoice/admin.go:1095
msgid "Price must be zero or greater." msgid "Price must be zero or greater."
msgstr "El preu ha de ser com a mínim zero." msgstr "El preu ha de ser com a mínim zero."
@ -2805,7 +3092,7 @@ msgctxt "header"
msgid "Children (aged 2 to 10)" msgid "Children (aged 2 to 10)"
msgstr "Mainada (entre 2 i 10 anys)" msgstr "Mainada (entre 2 i 10 anys)"
#: pkg/campsite/admin.go:280 pkg/booking/admin.go:413 pkg/booking/public.go:177 #: pkg/campsite/admin.go:280 pkg/booking/admin.go:414 pkg/booking/public.go:177
#: pkg/booking/public.go:232 #: pkg/booking/public.go:232
msgid "Selected campsite type is not valid." msgid "Selected campsite type is not valid."
msgstr "El tipus dallotjament escollit no és vàlid." msgstr "El tipus dallotjament escollit no és vàlid."
@ -2843,6 +3130,136 @@ msgstr "No podeu deixar la data de fi en blanc."
msgid "End date must be a valid date." msgid "End date must be a valid date."
msgstr "La data de fi ha de ser una data vàlida." msgstr "La data de fi ha de ser una data vàlida."
#: pkg/customer/admin.go:293 pkg/company/admin.go:207
#: pkg/booking/checkin.go:297 pkg/booking/public.go:577
msgid "Selected country is not valid."
msgstr "El país escollit no és vàlid."
#: pkg/customer/admin.go:297 pkg/booking/checkin.go:281
msgid "Selected ID document type is not valid."
msgstr "El tipus de document didentitat escollit no és vàlid."
#: pkg/customer/admin.go:298 pkg/booking/checkin.go:282
msgid "ID document number can not be empty."
msgstr "No podeu deixar el número document didentitat en blanc."
#: pkg/customer/admin.go:300 pkg/booking/checkin.go:288
#: pkg/booking/checkin.go:289 pkg/booking/admin.go:426
#: pkg/booking/public.go:581
msgid "Full name can not be empty."
msgstr "No podeu deixar el nom i els cognoms en blanc."
#: pkg/customer/admin.go:301 pkg/booking/admin.go:427 pkg/booking/public.go:582
msgid "Full name must have at least one letter."
msgstr "El nom i els cognoms han de tenir com a mínim una lletra."
#: pkg/customer/admin.go:304 pkg/company/admin.go:230 pkg/booking/public.go:585
msgid "Address can not be empty."
msgstr "No podeu deixar ladreça en blanc."
#: pkg/customer/admin.go:305 pkg/booking/public.go:586
msgid "Town or village can not be empty."
msgstr "No podeu deixar la població en blanc."
#: pkg/customer/admin.go:306 pkg/company/admin.go:233 pkg/booking/public.go:587
msgid "Postcode can not be empty."
msgstr "No podeu deixar el codi postal en blanc."
#: pkg/customer/admin.go:307 pkg/company/admin.go:234 pkg/booking/admin.go:433
#: pkg/booking/public.go:588
msgid "This postcode is not valid."
msgstr "Aquest codi postal no és vàlid."
#: pkg/customer/admin.go:315 pkg/company/admin.go:220
#: pkg/booking/checkin.go:301 pkg/booking/admin.go:443
#: pkg/booking/public.go:596
msgid "This phone number is not valid."
msgstr "Aquest número de telèfon no és vàlid."
#: pkg/invoice/admin.go:649
msgctxt "filename"
msgid "invoices.zip"
msgstr "factures.zip"
#: pkg/invoice/admin.go:664
msgctxt "filename"
msgid "invoices.ods"
msgstr "factures.ods"
#: pkg/invoice/admin.go:666 pkg/invoice/admin.go:1285 pkg/invoice/admin.go:1292
msgid "Invalid action"
msgstr "Acció invàlida"
#: pkg/invoice/admin.go:830
msgid "Selected invoice status is not valid."
msgstr "Lestat de factura escollit no és vàlid."
#: pkg/invoice/admin.go:831
msgid "Selected customer is not valid."
msgstr "El client escollit no és vàlid."
#: pkg/invoice/admin.go:832
msgid "Invoice date can not be empty."
msgstr "No podeu deixar la data de factura en blanc."
#: pkg/invoice/admin.go:833
msgid "Invoice date must be a valid date."
msgstr "La data de factura ha de ser una data vàlida."
#: pkg/invoice/admin.go:980
#, c-format
msgid "Re: quotation #%s of %s"
msgstr ""
#: pkg/invoice/admin.go:981
msgctxt "to_char"
msgid "MM/DD/YYYY"
msgstr "DD/MM/YYYY"
#: pkg/invoice/admin.go:1083
msgid "Invoice product ID must be an integer."
msgstr "LID de producte de factura ha de ser enter."
#: pkg/invoice/admin.go:1084
msgid "Invoice product ID one or greater."
msgstr "LID de producte de factura ha de ser com a mínim u."
#: pkg/invoice/admin.go:1088
msgid "Product ID must be an integer."
msgstr "LID de producte ha de ser un número enter."
#: pkg/invoice/admin.go:1089
msgid "Product ID must zero or greater."
msgstr "LID de producte ha de ser com a mínim zero."
#: pkg/invoice/admin.go:1098
msgid "Quantity can not be empty."
msgstr "No podeu deixar la quantitat en blanc."
#: pkg/invoice/admin.go:1099
msgid "Quantity must be an integer."
msgstr "La quantitat ha de ser un número enter."
#: pkg/invoice/admin.go:1100
msgid "Quantity must one or greater."
msgstr "La quantitat ha de ser com a mínim u."
#: pkg/invoice/admin.go:1103
msgid "Discount can not be empty."
msgstr "No podeu deixar el descompte en blanc."
#: pkg/invoice/admin.go:1104
msgid "Discount must be an integer."
msgstr "El descompte ha de ser un número enter."
#: pkg/invoice/admin.go:1105 pkg/invoice/admin.go:1106
msgid "Discount must be a percentage between 0 and 100."
msgstr "El descompte ha de ser un percentatge entre 0 i 100"
#: pkg/invoice/admin.go:1110
msgid "Selected tax is not valid."
msgstr "Limpost escollit no és vàlid."
#: pkg/user/admin.go:18 #: pkg/user/admin.go:18
msgctxt "role" msgctxt "role"
msgid "guest" msgid "guest"
@ -2902,11 +3319,6 @@ msgstr "No podeu deixar ladreça de lenllaç en blanc."
msgid "This web address is not valid. It should be like https://domain.com/." msgid "This web address is not valid. It should be like https://domain.com/."
msgstr "Aquesta adreça web no és vàlida. Hauria de ser similar a https://domini.com/." msgstr "Aquesta adreça web no és vàlida. Hauria de ser similar a https://domini.com/."
#: pkg/company/admin.go:207 pkg/booking/checkin.go:301
#: pkg/booking/public.go:581
msgid "Selected country is not valid."
msgstr "El país escollit no és vàlid."
#: pkg/company/admin.go:211 #: pkg/company/admin.go:211
msgid "Business name can not be empty." msgid "Business name can not be empty."
msgstr "No podeu deixar el nom dempresa en blanc." msgstr "No podeu deixar el nom dempresa en blanc."
@ -2923,19 +3335,10 @@ msgstr "No podeu deixar el NIF en blanc."
msgid "This VAT number is not valid." msgid "This VAT number is not valid."
msgstr "Aquest NIF no és vàlid." msgstr "Aquest NIF no és vàlid."
#: pkg/company/admin.go:219 pkg/booking/public.go:599 #: pkg/company/admin.go:219 pkg/booking/public.go:595
msgid "Phone can not be empty." msgid "Phone can not be empty."
msgstr "No podeu deixar el telèfon en blanc." msgstr "No podeu deixar el telèfon en blanc."
#: pkg/company/admin.go:220 pkg/booking/checkin.go:305 pkg/booking/admin.go:442
#: pkg/booking/public.go:600
msgid "This phone number is not valid."
msgstr "Aquest número de telèfon no és vàlid."
#: pkg/company/admin.go:230 pkg/booking/public.go:589
msgid "Address can not be empty."
msgstr "No podeu deixar ladreça en blanc."
#: pkg/company/admin.go:231 #: pkg/company/admin.go:231
msgid "City can not be empty." msgid "City can not be empty."
msgstr "No podeu deixar la població en blanc." msgstr "No podeu deixar la població en blanc."
@ -2944,14 +3347,6 @@ msgstr "No podeu deixar la població en blanc."
msgid "Province can not be empty." msgid "Province can not be empty."
msgstr "No podeu deixar la província en blanc." msgstr "No podeu deixar la província en blanc."
#: pkg/company/admin.go:233 pkg/booking/public.go:591
msgid "Postcode can not be empty."
msgstr "No podeu deixar el codi postal en blanc."
#: pkg/company/admin.go:234 pkg/booking/admin.go:432 pkg/booking/public.go:592
msgid "This postcode is not valid."
msgstr "Aquest codi postal no és vàlid."
#: pkg/company/admin.go:238 #: pkg/company/admin.go:238
msgid "RTC number can not be empty." msgid "RTC number can not be empty."
msgstr "No podeu deixar el número dRTC en blanc." msgstr "No podeu deixar el número dRTC en blanc."
@ -3000,40 +3395,27 @@ msgstr "No podeu deixar el fitxer del mèdia en blanc."
msgid "Filename can not be empty." msgid "Filename can not be empty."
msgstr "No podeu deixar el nom del fitxer en blanc." msgstr "No podeu deixar el nom del fitxer en blanc."
#: pkg/booking/checkin.go:285 #: pkg/booking/checkin.go:284
msgid "Selected ID document type is not valid."
msgstr "El tipus de document didentitat escollit no és vàlid."
#: pkg/booking/checkin.go:286
msgid "ID document number can not be empty."
msgstr "No podeu deixar el número document didentitat en blanc."
#: pkg/booking/checkin.go:288
msgid "ID document issue date must be a valid date." msgid "ID document issue date must be a valid date."
msgstr "La data dexpedició del document didentitat ha de ser una data vàlida." msgstr "La data dexpedició del document didentitat ha de ser una data vàlida."
#: pkg/booking/checkin.go:289 #: pkg/booking/checkin.go:285
msgid "ID document issue date must be in the past." msgid "ID document issue date must be in the past."
msgstr "La data dexpedició del document didentitat ha de ser al passat." msgstr "La data dexpedició del document didentitat ha de ser al passat."
#: pkg/booking/checkin.go:292 pkg/booking/checkin.go:293 #: pkg/booking/checkin.go:290
#: pkg/booking/admin.go:425 pkg/booking/public.go:585
msgid "Full name can not be empty."
msgstr "No podeu deixar el nom i els cognoms en blanc."
#: pkg/booking/checkin.go:294
msgid "Selected sex is not valid." msgid "Selected sex is not valid."
msgstr "El sexe escollit no és vàlid." msgstr "El sexe escollit no és vàlid."
#: pkg/booking/checkin.go:295 #: pkg/booking/checkin.go:291
msgid "Birthdate can not be empty" msgid "Birthdate can not be empty"
msgstr "No podeu deixar la data de naixement en blanc." msgstr "No podeu deixar la data de naixement en blanc."
#: pkg/booking/checkin.go:296 #: pkg/booking/checkin.go:292
msgid "Birthdate must be a valid date." msgid "Birthdate must be a valid date."
msgstr "La data de naixement ha de ser una data vàlida." msgstr "La data de naixement ha de ser una data vàlida."
#: pkg/booking/checkin.go:297 #: pkg/booking/checkin.go:293
msgid "Birthdate must be in the past." msgid "Birthdate must be in the past."
msgstr "La data de naixement ha de ser al passat." msgstr "La data de naixement ha de ser al passat."
@ -3057,28 +3439,24 @@ msgctxt "cart"
msgid "Dog" msgid "Dog"
msgstr "Gos" msgstr "Gos"
#: pkg/booking/admin.go:217 #: pkg/booking/admin.go:218
msgctxt "filename" msgctxt "filename"
msgid "bookings.ods" msgid "bookings.ods"
msgstr "reserves.ods" msgstr "reserves.ods"
#: pkg/booking/admin.go:426 pkg/booking/public.go:586 #: pkg/booking/admin.go:432
msgid "Full name must have at least one letter."
msgstr "El nom i els cognoms han de tenir com a mínim una lletra."
#: pkg/booking/admin.go:431
msgid "Country can not be empty to validate the postcode." msgid "Country can not be empty to validate the postcode."
msgstr "No podeu deixar el país en blanc per validar el codi postal." msgstr "No podeu deixar el país en blanc per validar el codi postal."
#: pkg/booking/admin.go:441 #: pkg/booking/admin.go:442
msgid "Country can not be empty to validate the phone." msgid "Country can not be empty to validate the phone."
msgstr "No podeu deixar el país en blanc per validar el telèfon." msgstr "No podeu deixar el país en blanc per validar el telèfon."
#: pkg/booking/admin.go:448 #: pkg/booking/admin.go:449
msgid "You must select at least one accommodation." msgid "You must select at least one accommodation."
msgstr "Heu descollir com a mínim un allotjament." msgstr "Heu descollir com a mínim un allotjament."
#: pkg/booking/admin.go:454 #: pkg/booking/admin.go:455
msgid "The selected accommodations have no available openings in the requested dates." msgid "The selected accommodations have no available openings in the requested dates."
msgstr "Els allotjaments escollits no estan disponibles a les dates demanades." msgstr "Els allotjaments escollits no estan disponibles a les dates demanades."
@ -3191,11 +3569,7 @@ msgstr "El valor de %s ha de ser com a mínim %d."
msgid "%s must be at most %d." msgid "%s must be at most %d."
msgstr "El valor de %s ha de ser com a màxim %d." msgstr "El valor de %s ha de ser com a màxim %d."
#: pkg/booking/public.go:590 #: pkg/booking/public.go:601
msgid "Town or village can not be empty."
msgstr "No podeu deixar la població en blanc."
#: pkg/booking/public.go:605
msgid "It is mandatory to agree to the reservation conditions." msgid "It is mandatory to agree to the reservation conditions."
msgstr "És obligatori acceptar les condicions de reserves." msgstr "És obligatori acceptar les condicions de reserves."

614
po/es.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-04-26 16:53+0200\n" "POT-Creation-Date: 2024-04-28 20:05+0200\n"
"PO-Revision-Date: 2024-02-06 10:04+0100\n" "PO-Revision-Date: 2024-02-06 10:04+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -239,6 +239,7 @@ msgstr "Opciones del tipo de alojamiento"
#: web/templates/mail/payment/details.gotxt:39 #: web/templates/mail/payment/details.gotxt:39
#: web/templates/public/booking/fields.gohtml:146 #: web/templates/public/booking/fields.gohtml:146
#: web/templates/admin/payment/details.gohtml:140 #: web/templates/admin/payment/details.gohtml:140
#: web/templates/admin/customer/form.gohtml:28
#: web/templates/admin/booking/fields.gohtml:188 #: web/templates/admin/booking/fields.gohtml:188
msgctxt "title" msgctxt "title"
msgid "Customer Details" msgid "Customer Details"
@ -247,6 +248,7 @@ msgstr "Detalles del cliente"
#: web/templates/mail/payment/details.gotxt:41 #: web/templates/mail/payment/details.gotxt:41
#: web/templates/public/booking/fields.gohtml:149 #: web/templates/public/booking/fields.gohtml:149
#: web/templates/admin/payment/details.gohtml:143 #: web/templates/admin/payment/details.gohtml:143
#: web/templates/admin/customer/form.gohtml:31
#: web/templates/admin/booking/fields.gohtml:191 #: web/templates/admin/booking/fields.gohtml:191
msgctxt "input" msgctxt "input"
msgid "Full name" msgid "Full name"
@ -255,6 +257,7 @@ msgstr "Nombre y apellidos"
#: web/templates/mail/payment/details.gotxt:42 #: web/templates/mail/payment/details.gotxt:42
#: web/templates/public/booking/fields.gohtml:158 #: web/templates/public/booking/fields.gohtml:158
#: web/templates/admin/payment/details.gohtml:147 #: web/templates/admin/payment/details.gohtml:147
#: web/templates/admin/customer/form.gohtml:69
#: web/templates/admin/taxDetails.gohtml:69 #: web/templates/admin/taxDetails.gohtml:69
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
@ -263,6 +266,7 @@ msgstr "Dirección"
#: web/templates/mail/payment/details.gotxt:43 #: web/templates/mail/payment/details.gotxt:43
#: web/templates/public/booking/fields.gohtml:167 #: web/templates/public/booking/fields.gohtml:167
#: web/templates/admin/payment/details.gohtml:151 #: web/templates/admin/payment/details.gohtml:151
#: web/templates/admin/customer/form.gohtml:105
#: web/templates/admin/taxDetails.gohtml:93 #: web/templates/admin/taxDetails.gohtml:93
msgctxt "input" msgctxt "input"
msgid "Postcode" msgid "Postcode"
@ -278,6 +282,7 @@ msgstr "Población"
#: web/templates/mail/payment/details.gotxt:45 #: web/templates/mail/payment/details.gotxt:45
#: web/templates/public/booking/fields.gohtml:187 #: web/templates/public/booking/fields.gohtml:187
#: web/templates/admin/payment/details.gohtml:159 #: web/templates/admin/payment/details.gohtml:159
#: web/templates/admin/customer/form.gohtml:117
#: web/templates/admin/taxDetails.gohtml:101 #: web/templates/admin/taxDetails.gohtml:101
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
@ -411,11 +416,16 @@ msgid "Order Number"
msgstr "Número de pedido" msgstr "Número de pedido"
#: web/templates/public/payment/details.gohtml:8 #: web/templates/public/payment/details.gohtml:8
#: web/templates/admin/invoice/index.gohtml:103
#: web/templates/admin/invoice/view.gohtml:26
msgctxt "title" msgctxt "title"
msgid "Date" msgid "Date"
msgstr "Fecha" msgstr "Fecha"
#: web/templates/public/payment/details.gohtml:12 #: web/templates/public/payment/details.gohtml:12
#: web/templates/admin/invoice/form.gohtml:119
#: web/templates/admin/invoice/view.gohtml:63
#: web/templates/admin/invoice/view.gohtml:103
msgctxt "title" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
@ -579,6 +589,11 @@ msgctxt "input"
msgid "Year" msgid "Year"
msgstr "Año" msgstr "Año"
#: web/templates/public/form.gohtml:83 web/templates/admin/form.gohtml:83
msgctxt "action"
msgid "Filters"
msgstr "Filtros"
#: web/templates/public/campsite/type.gohtml:49 #: web/templates/public/campsite/type.gohtml:49
#: web/templates/public/booking/fields.gohtml:278 #: web/templates/public/booking/fields.gohtml:278
msgctxt "action" msgctxt "action"
@ -929,7 +944,7 @@ msgstr "Menú"
#: web/templates/admin/campsite/type/option/form.gohtml:16 #: web/templates/admin/campsite/type/option/form.gohtml:16
#: web/templates/admin/campsite/type/option/index.gohtml:10 #: web/templates/admin/campsite/type/option/index.gohtml:10
#: web/templates/admin/campsite/type/index.gohtml:10 #: web/templates/admin/campsite/type/index.gohtml:10
#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:95 #: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:101
#: web/templates/admin/booking/fields.gohtml:266 #: web/templates/admin/booking/fields.gohtml:266
msgctxt "title" msgctxt "title"
msgid "Campsites" msgid "Campsites"
@ -1004,13 +1019,15 @@ msgid "Campground map"
msgstr "Mapa del camping" msgstr "Mapa del camping"
#: web/templates/public/booking/fields.gohtml:176 #: web/templates/public/booking/fields.gohtml:176
#: web/templates/admin/customer/form.gohtml:81
msgctxt "input" msgctxt "input"
msgid "Town or village" msgid "Town or village"
msgstr "Población" msgstr "Población"
#: web/templates/public/booking/fields.gohtml:193 #: web/templates/public/booking/fields.gohtml:193
#: web/templates/admin/customer/form.gohtml:121
#: web/templates/admin/booking/fields.gohtml:204 #: web/templates/admin/booking/fields.gohtml:204
#: web/templates/admin/booking/guest.gohtml:109 #: web/templates/admin/booking/guest.gohtml:111
msgid "Choose a country" msgid "Choose a country"
msgstr "Escoja un país" msgstr "Escoja un país"
@ -1164,6 +1181,7 @@ msgstr "Álias"
#: web/templates/admin/campsite/type/form.gohtml:51 #: web/templates/admin/campsite/type/form.gohtml:51
#: web/templates/admin/campsite/type/option/form.gohtml:41 #: web/templates/admin/campsite/type/option/form.gohtml:41
#: web/templates/admin/season/form.gohtml:50 #: web/templates/admin/season/form.gohtml:50
#: web/templates/admin/invoice/product-form.gohtml:16
#: web/templates/admin/services/form.gohtml:53 #: web/templates/admin/services/form.gohtml:53
#: web/templates/admin/profile.gohtml:29 #: web/templates/admin/profile.gohtml:29
#: web/templates/admin/surroundings/form.gohtml:41 #: web/templates/admin/surroundings/form.gohtml:41
@ -1188,6 +1206,8 @@ msgstr "Contenido"
#: web/templates/admin/campsite/type/form.gohtml:287 #: web/templates/admin/campsite/type/form.gohtml:287
#: web/templates/admin/campsite/type/option/form.gohtml:98 #: web/templates/admin/campsite/type/option/form.gohtml:98
#: web/templates/admin/season/form.gohtml:73 #: web/templates/admin/season/form.gohtml:73
#: web/templates/admin/customer/form.gohtml:153
#: web/templates/admin/invoice/form.gohtml:137
#: web/templates/admin/services/form.gohtml:81 #: web/templates/admin/services/form.gohtml:81
#: web/templates/admin/surroundings/form.gohtml:69 #: web/templates/admin/surroundings/form.gohtml:69
#: web/templates/admin/surroundings/index.gohtml:58 #: web/templates/admin/surroundings/index.gohtml:58
@ -1211,6 +1231,7 @@ msgstr "Actualizar"
#: web/templates/admin/campsite/type/form.gohtml:289 #: web/templates/admin/campsite/type/form.gohtml:289
#: web/templates/admin/campsite/type/option/form.gohtml:100 #: web/templates/admin/campsite/type/option/form.gohtml:100
#: web/templates/admin/season/form.gohtml:75 #: web/templates/admin/season/form.gohtml:75
#: web/templates/admin/customer/form.gohtml:155
#: web/templates/admin/services/form.gohtml:83 #: web/templates/admin/services/form.gohtml:83
#: web/templates/admin/surroundings/form.gohtml:71 #: web/templates/admin/surroundings/form.gohtml:71
#: web/templates/admin/amenity/feature/form.gohtml:67 #: web/templates/admin/amenity/feature/form.gohtml:67
@ -1232,6 +1253,7 @@ msgstr "Añadir texto legal"
#: web/templates/admin/campsite/type/option/index.gohtml:30 #: web/templates/admin/campsite/type/option/index.gohtml:30
#: web/templates/admin/campsite/type/index.gohtml:29 #: web/templates/admin/campsite/type/index.gohtml:29
#: web/templates/admin/season/index.gohtml:29 #: web/templates/admin/season/index.gohtml:29
#: web/templates/admin/customer/index.gohtml:19
#: web/templates/admin/user/index.gohtml:20 #: web/templates/admin/user/index.gohtml:20
#: web/templates/admin/surroundings/index.gohtml:83 #: web/templates/admin/surroundings/index.gohtml:83
#: web/templates/admin/amenity/feature/index.gohtml:30 #: web/templates/admin/amenity/feature/index.gohtml:30
@ -1725,6 +1747,7 @@ msgid "Per night"
msgstr "Por noche" msgstr "Por noche"
#: web/templates/admin/campsite/type/option/form.gohtml:84 #: web/templates/admin/campsite/type/option/form.gohtml:84
#: web/templates/admin/invoice/product-form.gohtml:29
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Precio" msgstr "Precio"
@ -1824,12 +1847,316 @@ msgctxt "action"
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
#: web/templates/admin/customer/form.gohtml:8
msgctxt "title"
msgid "Edit Customer"
msgstr "Edición del cliente"
#: web/templates/admin/customer/form.gohtml:10
msgctxt "title"
msgid "New Customer"
msgstr "Nuevo cliente"
#: web/templates/admin/customer/form.gohtml:15
#: web/templates/admin/invoice/index.gohtml:105
msgctxt "title"
msgid "Customer"
msgstr "Cliente"
#: web/templates/admin/customer/form.gohtml:44
#: web/templates/admin/booking/guest.gohtml:8
msgctxt "input"
msgid "ID document number"
msgstr "Número de documento de identidad"
#: web/templates/admin/customer/form.gohtml:56
#: web/templates/admin/booking/guest.gohtml:20
msgctxt "input"
msgid "ID document type"
msgstr "Tipo de documento"
#: web/templates/admin/customer/form.gohtml:61
#: web/templates/admin/booking/guest.gohtml:25
msgid "Choose an ID document type"
msgstr "Escoja un tipo de documento"
#: web/templates/admin/customer/form.gohtml:93
#: web/templates/admin/taxDetails.gohtml:85
msgctxt "input"
msgid "Province"
msgstr "Provincia"
#: web/templates/admin/customer/form.gohtml:129
#: web/templates/admin/booking/fields.gohtml:239
msgctxt "input"
msgid "Email (optional)"
msgstr "Correo-e (opcional)"
#: web/templates/admin/customer/form.gohtml:140
#: web/templates/admin/booking/fields.gohtml:248
#: web/templates/admin/booking/guest.gohtml:119
msgctxt "input"
msgid "Phone (optional)"
msgstr "Teléfono (opcional)"
#: web/templates/admin/customer/index.gohtml:6
#: web/templates/admin/layout.gohtml:95
msgctxt "title"
msgid "Customers"
msgstr "Clientes"
#: web/templates/admin/customer/index.gohtml:14
msgctxt "action"
msgid "Add Customer"
msgstr "Añadir cliente"
#: web/templates/admin/customer/index.gohtml:20
#: web/templates/admin/user/login-attempts.gohtml:20
#: web/templates/admin/user/index.gohtml:21
msgctxt "header"
msgid "Email"
msgstr "Correo-e"
#: web/templates/admin/customer/index.gohtml:21
msgctxt "header"
msgid "Phone"
msgstr "Teléfono"
#: web/templates/admin/customer/index.gohtml:33
msgid "No customer found."
msgstr "No se ha encontrado ningún cliente."
#: web/templates/admin/dashboard.gohtml:6 #: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:89 #: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:89
msgctxt "title" msgctxt "title"
msgid "Dashboard" msgid "Dashboard"
msgstr "Panel" msgstr "Panel"
#: web/templates/admin/invoice/product-form.gohtml:11
#: web/templates/admin/booking/guest.gohtml:5
msgctxt "action"
msgid "Remove"
msgstr "Borrar"
#: web/templates/admin/invoice/product-form.gohtml:44
msgctxt "input"
msgid "Quantity"
msgstr "Cantidad"
#: web/templates/admin/invoice/product-form.gohtml:58
msgctxt "input"
msgid "Discount (%)"
msgstr "Descuento (%)"
#: web/templates/admin/invoice/product-form.gohtml:73
msgctxt "input"
msgid "Taxes"
msgstr "Impuestos"
#: web/templates/admin/invoice/product-form.gohtml:79
msgid "Select a TAX"
msgstr "Escoja un impuesto"
#: web/templates/admin/invoice/form.gohtml:4
msgctxt "title"
msgid "Edit Invoice “%s”"
msgstr "Edición de la factura «%s»"
#: web/templates/admin/invoice/form.gohtml:6
msgctxt "title"
msgid "New Invoice"
msgstr "Nueva factura"
#: web/templates/admin/invoice/form.gohtml:15
#: web/templates/admin/invoice/index.gohtml:2
#: web/templates/admin/invoice/view.gohtml:6
#: web/templates/admin/layout.gohtml:98
msgctxt "title"
msgid "Invoices"
msgstr "Facturas"
#: web/templates/admin/invoice/form.gohtml:32
msgid "Product “%s” removed"
msgstr "Se ha borrado el producto «%s»"
#: web/templates/admin/invoice/form.gohtml:36
msgctxt "action"
msgid "Undo"
msgstr "Deshacer"
#: web/templates/admin/invoice/form.gohtml:51
#: web/templates/admin/invoice/index.gohtml:39
msgctxt "input"
msgid "Customer"
msgstr "Cliente"
#: web/templates/admin/invoice/form.gohtml:56
msgid "Select a customer"
msgstr "Escoja un cliente"
#: web/templates/admin/invoice/form.gohtml:64
msgctxt "input"
msgid "Invoice date"
msgstr "Fecha de la factura"
#: web/templates/admin/invoice/form.gohtml:77
#: web/templates/admin/invoice/index.gohtml:51
msgctxt "input"
msgid "Invoice status"
msgstr "Estado de factura"
#: web/templates/admin/invoice/form.gohtml:92
msgctxt "input"
msgid "Notes (optional)"
msgstr "Notas (opcional)"
#: web/templates/admin/invoice/form.gohtml:109
#: web/templates/admin/invoice/view.gohtml:59
msgctxt "title"
msgid "Subtotal"
msgstr "Subtotal"
#: web/templates/admin/invoice/form.gohtml:133
msgctxt "action"
msgid "Add products"
msgstr "Añadir productos"
#: web/templates/admin/invoice/form.gohtml:140
msgctxt "action"
msgid "Save"
msgstr "Guardar"
#: web/templates/admin/invoice/index.gohtml:25
msgctxt "action"
msgid "Download invoices"
msgstr "Descargar facturas"
#: web/templates/admin/invoice/index.gohtml:28
msgctxt "action"
msgid "Export list"
msgstr "Exportar lista"
#: web/templates/admin/invoice/index.gohtml:43
msgid "All customers"
msgstr "Todos los clientes"
#: web/templates/admin/invoice/index.gohtml:55
msgid "All statuses"
msgstr "Todos los estados"
#: web/templates/admin/invoice/index.gohtml:63
msgctxt "input"
msgid "From date"
msgstr "De la fecha"
#: web/templates/admin/invoice/index.gohtml:72
msgctxt "input"
msgid "To date"
msgstr "A la fecha"
#: web/templates/admin/invoice/index.gohtml:81
msgctxt "input"
msgid "Invoice number"
msgstr "Número de factura"
#: web/templates/admin/invoice/index.gohtml:91
msgctxt "action"
msgid "Filter"
msgstr "Filtrar"
#: web/templates/admin/invoice/index.gohtml:94
msgctxt "action"
msgid "Reset"
msgstr "Restablecer"
#: web/templates/admin/invoice/index.gohtml:97
msgctxt "action"
msgid "Add invoice"
msgstr "Añadir factura"
#: web/templates/admin/invoice/index.gohtml:102
msgctxt "invoice"
msgid "All"
msgstr "Todas"
#: web/templates/admin/invoice/index.gohtml:104
msgctxt "title"
msgid "Invoice Num."
msgstr "Núm. de factura"
#: web/templates/admin/invoice/index.gohtml:106
msgctxt "title"
msgid "Status"
msgstr "Estado"
#: web/templates/admin/invoice/index.gohtml:107
msgctxt "title"
msgid "Download"
msgstr "Descarga"
#: web/templates/admin/invoice/index.gohtml:108
msgctxt "title"
msgid "Amount"
msgstr "Importe"
#: web/templates/admin/invoice/index.gohtml:115
msgctxt "action"
msgid "Select invoice %v"
msgstr "Seleccionar factura %v"
#: web/templates/admin/invoice/index.gohtml:144
msgctxt "action"
msgid "Download invoice %s"
msgstr "Descargar factura %s"
#: web/templates/admin/invoice/index.gohtml:154
msgid "No invoices added yet."
msgstr "No se ha añadido ninguna factura todavía."
#: web/templates/admin/invoice/index.gohtml:161
msgid "Total"
msgstr "Total"
#: web/templates/admin/invoice/view.gohtml:2
msgctxt "title"
msgid "Invoice %s"
msgstr "Factura %s"
#: web/templates/admin/invoice/view.gohtml:15
msgctxt "action"
msgid "Edit"
msgstr "Editar"
#: web/templates/admin/invoice/view.gohtml:18
msgctxt "action"
msgid "Download invoice"
msgstr "Descargar factura"
#: web/templates/admin/invoice/view.gohtml:53
msgctxt "title"
msgid "Concept"
msgstr "Concepto"
#: web/templates/admin/invoice/view.gohtml:54
msgctxt "title"
msgid "Price"
msgstr "Precio"
#: web/templates/admin/invoice/view.gohtml:56
msgctxt "title"
msgid "Discount"
msgstr "Descuento"
#: web/templates/admin/invoice/view.gohtml:58
msgctxt "title"
msgid "Units"
msgstr "Unidades"
#: web/templates/admin/invoice/view.gohtml:93
msgctxt "title"
msgid "Tax Base"
msgstr "Base imponible"
#: web/templates/admin/login.gohtml:6 web/templates/admin/login.gohtml:18 #: web/templates/admin/login.gohtml:6 web/templates/admin/login.gohtml:18
msgctxt "title" msgctxt "title"
msgid "Login" msgid "Login"
@ -1928,12 +2255,6 @@ msgctxt "title"
msgid "Users" msgid "Users"
msgstr "Usuarios" msgstr "Usuarios"
#: web/templates/admin/user/login-attempts.gohtml:20
#: web/templates/admin/user/index.gohtml:21
msgctxt "header"
msgid "Email"
msgstr "Correo-e"
#: web/templates/admin/user/login-attempts.gohtml:21 #: web/templates/admin/user/login-attempts.gohtml:21
msgctxt "header" msgctxt "header"
msgid "IP Address" msgid "IP Address"
@ -1985,11 +2306,6 @@ msgctxt "input"
msgid "Trade Name" msgid "Trade Name"
msgstr "Nombre comercial" msgstr "Nombre comercial"
#: web/templates/admin/taxDetails.gohtml:85
msgctxt "input"
msgid "Province"
msgstr "Provincia"
#: web/templates/admin/taxDetails.gohtml:111 #: web/templates/admin/taxDetails.gohtml:111
msgctxt "input" msgctxt "input"
msgid "Currency" msgid "Currency"
@ -2214,11 +2530,11 @@ msgctxt "title"
msgid "Bookings" msgid "Bookings"
msgstr "Reservas" msgstr "Reservas"
#: web/templates/admin/layout.gohtml:101 #: web/templates/admin/layout.gohtml:107
msgid "Breadcrumb" msgid "Breadcrumb"
msgstr "Migas de pan" msgstr "Migas de pan"
#: web/templates/admin/layout.gohtml:113 #: web/templates/admin/layout.gohtml:119
msgid "Camper Version: %s" msgid "Camper Version: %s"
msgstr "Camper versión: %s" msgstr "Camper versión: %s"
@ -2340,7 +2656,7 @@ msgid "Country (optional)"
msgstr "País (opcional)" msgstr "País (opcional)"
#: web/templates/admin/booking/fields.gohtml:212 #: web/templates/admin/booking/fields.gohtml:212
#: web/templates/admin/booking/guest.gohtml:128 #: web/templates/admin/booking/guest.gohtml:130
msgctxt "input" msgctxt "input"
msgid "Address (optional)" msgid "Address (optional)"
msgstr "Dirección (opcional)" msgstr "Dirección (opcional)"
@ -2355,17 +2671,6 @@ msgctxt "input"
msgid "Town or village (optional)" msgid "Town or village (optional)"
msgstr "Población (opcional)" msgstr "Población (opcional)"
#: web/templates/admin/booking/fields.gohtml:239
msgctxt "input"
msgid "Email (optional)"
msgstr "Correo-e (opcional)"
#: web/templates/admin/booking/fields.gohtml:248
#: web/templates/admin/booking/guest.gohtml:117
msgctxt "input"
msgid "Phone (optional)"
msgstr "Teléfono (opcional)"
#: web/templates/admin/booking/form.gohtml:8 #: web/templates/admin/booking/form.gohtml:8
msgctxt "title" msgctxt "title"
msgid "Edit Booking" msgid "Edit Booking"
@ -2430,60 +2735,41 @@ msgstr "Nombre del titular"
msgid "No booking found." msgid "No booking found."
msgstr "No se ha encontrado ninguna reserva." msgstr "No se ha encontrado ninguna reserva."
#: web/templates/admin/booking/guest.gohtml:5
msgctxt "action"
msgid "Remove"
msgstr "Borrar"
#: web/templates/admin/booking/guest.gohtml:8
msgctxt "input"
msgid "ID document number"
msgstr "Número de documento de identidad"
#: web/templates/admin/booking/guest.gohtml:20
msgctxt "input"
msgid "ID document type"
msgstr "Tipo de documento"
#: web/templates/admin/booking/guest.gohtml:25
msgid "Choose an ID document type"
msgstr "Escoja un tipo de documento"
#: web/templates/admin/booking/guest.gohtml:33 #: web/templates/admin/booking/guest.gohtml:33
msgctxt "input" msgctxt "input"
msgid "ID document issue date (if any)" msgid "ID document issue date (if any)"
msgstr "Fecha expedición del documento (si hay)" msgstr "Fecha expedición del documento (si hay)"
#: web/templates/admin/booking/guest.gohtml:44 #: web/templates/admin/booking/guest.gohtml:45
msgctxt "input" msgctxt "input"
msgid "First surname" msgid "First surname"
msgstr "Primer apellido" msgstr "Primer apellido"
#: web/templates/admin/booking/guest.gohtml:56 #: web/templates/admin/booking/guest.gohtml:57
msgctxt "input" msgctxt "input"
msgid "Second surname (if has one)" msgid "Second surname (if has one)"
msgstr "Segundo apellido (si tiene)" msgstr "Segundo apellido (si tiene)"
#: web/templates/admin/booking/guest.gohtml:67 #: web/templates/admin/booking/guest.gohtml:68
msgctxt "input" msgctxt "input"
msgid "Given name" msgid "Given name"
msgstr "Nombre" msgstr "Nombre"
#: web/templates/admin/booking/guest.gohtml:79 #: web/templates/admin/booking/guest.gohtml:80
msgctxt "input" msgctxt "input"
msgid "Sex" msgid "Sex"
msgstr "Sexo" msgstr "Sexo"
#: web/templates/admin/booking/guest.gohtml:84 #: web/templates/admin/booking/guest.gohtml:85
msgid "Choose a sex" msgid "Choose a sex"
msgstr "Escoja un sexo" msgstr "Escoja un sexo"
#: web/templates/admin/booking/guest.gohtml:92 #: web/templates/admin/booking/guest.gohtml:93
msgctxt "input" msgctxt "input"
msgid "Birthdate" msgid "Birthdate"
msgstr "Fecha de nacimiento" msgstr "Fecha de nacimiento"
#: web/templates/admin/booking/guest.gohtml:104 #: web/templates/admin/booking/guest.gohtml:106
msgctxt "input" msgctxt "input"
msgid "Nationality" msgid "Nationality"
msgstr "Nacionalidad" msgstr "Nacionalidad"
@ -2553,8 +2839,9 @@ msgstr "Se ha recibido correctamente el pago de la reserva"
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365 #: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365
#: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577 #: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
#: pkg/campsite/feature.go:269 pkg/season/admin.go:411 #: pkg/campsite/feature.go:269 pkg/season/admin.go:411
#: pkg/services/admin.go:316 pkg/surroundings/admin.go:340 #: pkg/invoice/admin.go:1092 pkg/services/admin.go:316
#: pkg/amenity/feature.go:269 pkg/amenity/admin.go:283 #: pkg/surroundings/admin.go:340 pkg/amenity/feature.go:269
#: pkg/amenity/admin.go:283
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco." msgstr "No podéis dejar el nombre en blanco."
@ -2589,12 +2876,12 @@ msgid "Slide image must be an image media type."
msgstr "La imagen de la diapositiva tiene que ser un medio de tipo imagen." msgstr "La imagen de la diapositiva tiene que ser un medio de tipo imagen."
#: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:224 #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:224
#: pkg/booking/public.go:596 #: pkg/booking/public.go:592
msgid "Email can not be empty." msgid "Email can not be empty."
msgstr "No podéis dejar el correo-e en blanco." msgstr "No podéis dejar el correo-e en blanco."
#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:225 #: pkg/app/login.go:57 pkg/app/user.go:247 pkg/customer/admin.go:312
#: pkg/booking/admin.go:437 pkg/booking/public.go:597 #: pkg/company/admin.go:225 pkg/booking/admin.go:438 pkg/booking/public.go:593
msgid "This email is not valid. It should be like name@domain.com." msgid "This email is not valid. It should be like name@domain.com."
msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com." msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com."
@ -2623,7 +2910,7 @@ msgstr "El idioma escogido no es válido."
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." msgstr "El archivo tiene que ser una imagen PNG o JPEG válida."
#: pkg/app/admin.go:73 #: pkg/app/admin.go:79
msgid "Access forbidden" msgid "Access forbidden"
msgstr "Acceso prohibido" msgstr "Acceso prohibido"
@ -2651,15 +2938,15 @@ msgstr "El valor del máximo tiene que ser un número entero."
msgid "Maximum must be equal or greater than minimum." msgid "Maximum must be equal or greater than minimum."
msgstr "El valor del máximo tiene que ser igual o mayor al del mínimo." msgstr "El valor del máximo tiene que ser igual o mayor al del mínimo."
#: pkg/campsite/types/option.go:382 #: pkg/campsite/types/option.go:382 pkg/invoice/admin.go:1093
msgid "Price can not be empty." msgid "Price can not be empty."
msgstr "No podéis dejar el precio en blanco." msgstr "No podéis dejar el precio en blanco."
#: pkg/campsite/types/option.go:383 #: pkg/campsite/types/option.go:383 pkg/invoice/admin.go:1094
msgid "Price must be a decimal number." msgid "Price must be a decimal number."
msgstr "El precio tiene que ser un número decimal." msgstr "El precio tiene que ser un número decimal."
#: pkg/campsite/types/option.go:384 #: pkg/campsite/types/option.go:384 pkg/invoice/admin.go:1095
msgid "Price must be zero or greater." msgid "Price must be zero or greater."
msgstr "El precio tiene que ser como mínimo cero." msgstr "El precio tiene que ser como mínimo cero."
@ -2805,7 +3092,7 @@ msgctxt "header"
msgid "Children (aged 2 to 10)" msgid "Children (aged 2 to 10)"
msgstr "Niños (de 2 a 10 años)" msgstr "Niños (de 2 a 10 años)"
#: pkg/campsite/admin.go:280 pkg/booking/admin.go:413 pkg/booking/public.go:177 #: pkg/campsite/admin.go:280 pkg/booking/admin.go:414 pkg/booking/public.go:177
#: pkg/booking/public.go:232 #: pkg/booking/public.go:232
msgid "Selected campsite type is not valid." msgid "Selected campsite type is not valid."
msgstr "El tipo de alojamiento escogido no es válido." msgstr "El tipo de alojamiento escogido no es válido."
@ -2843,6 +3130,136 @@ msgstr "No podéis dejar la fecha final en blanco."
msgid "End date must be a valid date." msgid "End date must be a valid date."
msgstr "La fecha final tiene que ser una fecha válida." msgstr "La fecha final tiene que ser una fecha válida."
#: pkg/customer/admin.go:293 pkg/company/admin.go:207
#: pkg/booking/checkin.go:297 pkg/booking/public.go:577
msgid "Selected country is not valid."
msgstr "El país escogido no es válido."
#: pkg/customer/admin.go:297 pkg/booking/checkin.go:281
msgid "Selected ID document type is not valid."
msgstr "El tipo de documento de identidad escogido no es válido."
#: pkg/customer/admin.go:298 pkg/booking/checkin.go:282
msgid "ID document number can not be empty."
msgstr "No podéis dejar el número del documento de identidad en blanco."
#: pkg/customer/admin.go:300 pkg/booking/checkin.go:288
#: pkg/booking/checkin.go:289 pkg/booking/admin.go:426
#: pkg/booking/public.go:581
msgid "Full name can not be empty."
msgstr "No podéis dejar el nombre y los apellidos en blanco."
#: pkg/customer/admin.go:301 pkg/booking/admin.go:427 pkg/booking/public.go:582
msgid "Full name must have at least one letter."
msgstr "El nombre y los apellidos tienen que tener como mínimo una letra."
#: pkg/customer/admin.go:304 pkg/company/admin.go:230 pkg/booking/public.go:585
msgid "Address can not be empty."
msgstr "No podéis dejar la dirección en blanco."
#: pkg/customer/admin.go:305 pkg/booking/public.go:586
msgid "Town or village can not be empty."
msgstr "No podéis dejar la población en blanco."
#: pkg/customer/admin.go:306 pkg/company/admin.go:233 pkg/booking/public.go:587
msgid "Postcode can not be empty."
msgstr "No podéis dejar el código postal en blanco."
#: pkg/customer/admin.go:307 pkg/company/admin.go:234 pkg/booking/admin.go:433
#: pkg/booking/public.go:588
msgid "This postcode is not valid."
msgstr "Este código postal no es válido."
#: pkg/customer/admin.go:315 pkg/company/admin.go:220
#: pkg/booking/checkin.go:301 pkg/booking/admin.go:443
#: pkg/booking/public.go:596
msgid "This phone number is not valid."
msgstr "Este teléfono no es válido."
#: pkg/invoice/admin.go:649
msgctxt "filename"
msgid "invoices.zip"
msgstr "facturas.zip"
#: pkg/invoice/admin.go:664
msgctxt "filename"
msgid "invoices.ods"
msgstr "facturas.ods"
#: pkg/invoice/admin.go:666 pkg/invoice/admin.go:1285 pkg/invoice/admin.go:1292
msgid "Invalid action"
msgstr "Acción inválida"
#: pkg/invoice/admin.go:830
msgid "Selected invoice status is not valid."
msgstr "El estado de factura escogida no es válido."
#: pkg/invoice/admin.go:831
msgid "Selected customer is not valid."
msgstr "El cliente escogido no es válido."
#: pkg/invoice/admin.go:832
msgid "Invoice date can not be empty."
msgstr "No podéis dejar la fecha de factura en blanco."
#: pkg/invoice/admin.go:833
msgid "Invoice date must be a valid date."
msgstr "La fecha de factura tiene que ser una fecha válida."
#: pkg/invoice/admin.go:980
#, c-format
msgid "Re: quotation #%s of %s"
msgstr ""
#: pkg/invoice/admin.go:981
msgctxt "to_char"
msgid "MM/DD/YYYY"
msgstr "DD/MM/YYYY"
#: pkg/invoice/admin.go:1083
msgid "Invoice product ID must be an integer."
msgstr "El ID de producto de factura tiene que ser entero."
#: pkg/invoice/admin.go:1084
msgid "Invoice product ID one or greater."
msgstr "El ID de producto de factura tiene que ser como mínimo uno."
#: pkg/invoice/admin.go:1088
msgid "Product ID must be an integer."
msgstr "El ID de producto tiene que ser un número entero."
#: pkg/invoice/admin.go:1089
msgid "Product ID must zero or greater."
msgstr "El ID de producto tiene que ser como mínimo cero."
#: pkg/invoice/admin.go:1098
msgid "Quantity can not be empty."
msgstr "No podéis dejar la cantidad en blanco."
#: pkg/invoice/admin.go:1099
msgid "Quantity must be an integer."
msgstr "La cantidad tiene que ser un número entero."
#: pkg/invoice/admin.go:1100
msgid "Quantity must one or greater."
msgstr "La cantidad tiene que ser como mínimo uno."
#: pkg/invoice/admin.go:1103
msgid "Discount can not be empty."
msgstr "No podéis dejar el descuento en blanco."
#: pkg/invoice/admin.go:1104
msgid "Discount must be an integer."
msgstr "El descuento tiene que ser un número entero."
#: pkg/invoice/admin.go:1105 pkg/invoice/admin.go:1106
msgid "Discount must be a percentage between 0 and 100."
msgstr "El descuento tiene que ser un porcentaje entre 1 y 100."
#: pkg/invoice/admin.go:1110
msgid "Selected tax is not valid."
msgstr "El impuesto escogido no es válido."
#: pkg/user/admin.go:18 #: pkg/user/admin.go:18
msgctxt "role" msgctxt "role"
msgid "guest" msgid "guest"
@ -2902,11 +3319,6 @@ msgstr "No podéis dejar la dirección del enlace en blanco."
msgid "This web address is not valid. It should be like https://domain.com/." msgid "This web address is not valid. It should be like https://domain.com/."
msgstr "Esta dirección web no es válida. Tiene que ser parecido a https://dominio.com/." msgstr "Esta dirección web no es válida. Tiene que ser parecido a https://dominio.com/."
#: pkg/company/admin.go:207 pkg/booking/checkin.go:301
#: pkg/booking/public.go:581
msgid "Selected country is not valid."
msgstr "El país escogido no es válido."
#: pkg/company/admin.go:211 #: pkg/company/admin.go:211
msgid "Business name can not be empty." msgid "Business name can not be empty."
msgstr "No podéis dejar el nombre de empresa en blanco." msgstr "No podéis dejar el nombre de empresa en blanco."
@ -2923,19 +3335,10 @@ msgstr "No podéis dejar el NIF en blanco."
msgid "This VAT number is not valid." msgid "This VAT number is not valid."
msgstr "Este NIF no es válido." msgstr "Este NIF no es válido."
#: pkg/company/admin.go:219 pkg/booking/public.go:599 #: pkg/company/admin.go:219 pkg/booking/public.go:595
msgid "Phone can not be empty." msgid "Phone can not be empty."
msgstr "No podéis dejar el teléfono en blanco." msgstr "No podéis dejar el teléfono en blanco."
#: pkg/company/admin.go:220 pkg/booking/checkin.go:305 pkg/booking/admin.go:442
#: pkg/booking/public.go:600
msgid "This phone number is not valid."
msgstr "Este teléfono no es válido."
#: pkg/company/admin.go:230 pkg/booking/public.go:589
msgid "Address can not be empty."
msgstr "No podéis dejar la dirección en blanco."
#: pkg/company/admin.go:231 #: pkg/company/admin.go:231
msgid "City can not be empty." msgid "City can not be empty."
msgstr "No podéis dejar la población en blanco." msgstr "No podéis dejar la población en blanco."
@ -2944,14 +3347,6 @@ msgstr "No podéis dejar la población en blanco."
msgid "Province can not be empty." msgid "Province can not be empty."
msgstr "No podéis dejar la provincia en blanco." msgstr "No podéis dejar la provincia en blanco."
#: pkg/company/admin.go:233 pkg/booking/public.go:591
msgid "Postcode can not be empty."
msgstr "No podéis dejar el código postal en blanco."
#: pkg/company/admin.go:234 pkg/booking/admin.go:432 pkg/booking/public.go:592
msgid "This postcode is not valid."
msgstr "Este código postal no es válido."
#: pkg/company/admin.go:238 #: pkg/company/admin.go:238
msgid "RTC number can not be empty." msgid "RTC number can not be empty."
msgstr "No podéis dejar el número RTC en blanco." msgstr "No podéis dejar el número RTC en blanco."
@ -3000,40 +3395,27 @@ msgstr "No podéis dejar el archivo del medio en blanco."
msgid "Filename can not be empty." msgid "Filename can not be empty."
msgstr "No podéis dejar el nombre del archivo en blanco." msgstr "No podéis dejar el nombre del archivo en blanco."
#: pkg/booking/checkin.go:285 #: pkg/booking/checkin.go:284
msgid "Selected ID document type is not valid."
msgstr "El tipo de documento de identidad escogido no es válido."
#: pkg/booking/checkin.go:286
msgid "ID document number can not be empty."
msgstr "No podéis dejar el número del documento de identidad en blanco."
#: pkg/booking/checkin.go:288
msgid "ID document issue date must be a valid date." msgid "ID document issue date must be a valid date."
msgstr "La fecha de expedición del documento de identidad tiene que ser una fecha válida." msgstr "La fecha de expedición del documento de identidad tiene que ser una fecha válida."
#: pkg/booking/checkin.go:289 #: pkg/booking/checkin.go:285
msgid "ID document issue date must be in the past." msgid "ID document issue date must be in the past."
msgstr "La fecha de expedición del documento de identidad tiene que ser del pasado." msgstr "La fecha de expedición del documento de identidad tiene que ser del pasado."
#: pkg/booking/checkin.go:292 pkg/booking/checkin.go:293 #: pkg/booking/checkin.go:290
#: pkg/booking/admin.go:425 pkg/booking/public.go:585
msgid "Full name can not be empty."
msgstr "No podéis dejar el nombre y los apellidos en blanco."
#: pkg/booking/checkin.go:294
msgid "Selected sex is not valid." msgid "Selected sex is not valid."
msgstr "El sexo escogido no es válido." msgstr "El sexo escogido no es válido."
#: pkg/booking/checkin.go:295 #: pkg/booking/checkin.go:291
msgid "Birthdate can not be empty" msgid "Birthdate can not be empty"
msgstr "No podéis dejar la fecha de nacimiento en blanco." msgstr "No podéis dejar la fecha de nacimiento en blanco."
#: pkg/booking/checkin.go:296 #: pkg/booking/checkin.go:292
msgid "Birthdate must be a valid date." msgid "Birthdate must be a valid date."
msgstr "La fecha de nacimiento tiene que ser una fecha válida." msgstr "La fecha de nacimiento tiene que ser una fecha válida."
#: pkg/booking/checkin.go:297 #: pkg/booking/checkin.go:293
msgid "Birthdate must be in the past." msgid "Birthdate must be in the past."
msgstr "La fecha de nacimiento tiene que ser del pasado." msgstr "La fecha de nacimiento tiene que ser del pasado."
@ -3057,28 +3439,24 @@ msgctxt "cart"
msgid "Dog" msgid "Dog"
msgstr "Perro" msgstr "Perro"
#: pkg/booking/admin.go:217 #: pkg/booking/admin.go:218
msgctxt "filename" msgctxt "filename"
msgid "bookings.ods" msgid "bookings.ods"
msgstr "reservas.ods" msgstr "reservas.ods"
#: pkg/booking/admin.go:426 pkg/booking/public.go:586 #: pkg/booking/admin.go:432
msgid "Full name must have at least one letter."
msgstr "El nombre y los apellidos tienen que tener como mínimo una letra."
#: pkg/booking/admin.go:431
msgid "Country can not be empty to validate the postcode." msgid "Country can not be empty to validate the postcode."
msgstr "No podéis dejar el país en blanco para validar el código postal." msgstr "No podéis dejar el país en blanco para validar el código postal."
#: pkg/booking/admin.go:441 #: pkg/booking/admin.go:442
msgid "Country can not be empty to validate the phone." msgid "Country can not be empty to validate the phone."
msgstr "No podéis dejar el país en blanco para validar el teléfono." msgstr "No podéis dejar el país en blanco para validar el teléfono."
#: pkg/booking/admin.go:448 #: pkg/booking/admin.go:449
msgid "You must select at least one accommodation." msgid "You must select at least one accommodation."
msgstr "Tenéis que seleccionar como mínimo un alojamiento." msgstr "Tenéis que seleccionar como mínimo un alojamiento."
#: pkg/booking/admin.go:454 #: pkg/booking/admin.go:455
msgid "The selected accommodations have no available openings in the requested dates." msgid "The selected accommodations have no available openings in the requested dates."
msgstr "Los alojamientos seleccionados no tienen disponibilidad en las fechas pedidas." msgstr "Los alojamientos seleccionados no tienen disponibilidad en las fechas pedidas."
@ -3191,11 +3569,7 @@ msgstr "%s tiene que ser como mínimo %d."
msgid "%s must be at most %d." msgid "%s must be at most %d."
msgstr "%s tiene que ser como máximo %d" msgstr "%s tiene que ser como máximo %d"
#: pkg/booking/public.go:590 #: pkg/booking/public.go:601
msgid "Town or village can not be empty."
msgstr "No podéis dejar la población en blanco."
#: pkg/booking/public.go:605
msgid "It is mandatory to agree to the reservation conditions." msgid "It is mandatory to agree to the reservation conditions."
msgstr "Es obligatorio aceptar las condiciones de reserva." msgstr "Es obligatorio aceptar las condiciones de reserva."

614
po/fr.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-04-26 16:53+0200\n" "POT-Creation-Date: 2024-04-28 20:05+0200\n"
"PO-Revision-Date: 2024-02-06 10:05+0100\n" "PO-Revision-Date: 2024-02-06 10:05+0100\n"
"Last-Translator: Oriol Carbonell <info@oriolcarbonell.cat>\n" "Last-Translator: Oriol Carbonell <info@oriolcarbonell.cat>\n"
"Language-Team: French <traduc@traduc.org>\n" "Language-Team: French <traduc@traduc.org>\n"
@ -239,6 +239,7 @@ msgstr "Options de type demplacement de camping"
#: web/templates/mail/payment/details.gotxt:39 #: web/templates/mail/payment/details.gotxt:39
#: web/templates/public/booking/fields.gohtml:146 #: web/templates/public/booking/fields.gohtml:146
#: web/templates/admin/payment/details.gohtml:140 #: web/templates/admin/payment/details.gohtml:140
#: web/templates/admin/customer/form.gohtml:28
#: web/templates/admin/booking/fields.gohtml:188 #: web/templates/admin/booking/fields.gohtml:188
msgctxt "title" msgctxt "title"
msgid "Customer Details" msgid "Customer Details"
@ -247,6 +248,7 @@ msgstr "Détails du client"
#: web/templates/mail/payment/details.gotxt:41 #: web/templates/mail/payment/details.gotxt:41
#: web/templates/public/booking/fields.gohtml:149 #: web/templates/public/booking/fields.gohtml:149
#: web/templates/admin/payment/details.gohtml:143 #: web/templates/admin/payment/details.gohtml:143
#: web/templates/admin/customer/form.gohtml:31
#: web/templates/admin/booking/fields.gohtml:191 #: web/templates/admin/booking/fields.gohtml:191
msgctxt "input" msgctxt "input"
msgid "Full name" msgid "Full name"
@ -255,6 +257,7 @@ msgstr "Nom et prénom"
#: web/templates/mail/payment/details.gotxt:42 #: web/templates/mail/payment/details.gotxt:42
#: web/templates/public/booking/fields.gohtml:158 #: web/templates/public/booking/fields.gohtml:158
#: web/templates/admin/payment/details.gohtml:147 #: web/templates/admin/payment/details.gohtml:147
#: web/templates/admin/customer/form.gohtml:69
#: web/templates/admin/taxDetails.gohtml:69 #: web/templates/admin/taxDetails.gohtml:69
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
@ -263,6 +266,7 @@ msgstr "Adresse"
#: web/templates/mail/payment/details.gotxt:43 #: web/templates/mail/payment/details.gotxt:43
#: web/templates/public/booking/fields.gohtml:167 #: web/templates/public/booking/fields.gohtml:167
#: web/templates/admin/payment/details.gohtml:151 #: web/templates/admin/payment/details.gohtml:151
#: web/templates/admin/customer/form.gohtml:105
#: web/templates/admin/taxDetails.gohtml:93 #: web/templates/admin/taxDetails.gohtml:93
msgctxt "input" msgctxt "input"
msgid "Postcode" msgid "Postcode"
@ -278,6 +282,7 @@ msgstr "Ville"
#: web/templates/mail/payment/details.gotxt:45 #: web/templates/mail/payment/details.gotxt:45
#: web/templates/public/booking/fields.gohtml:187 #: web/templates/public/booking/fields.gohtml:187
#: web/templates/admin/payment/details.gohtml:159 #: web/templates/admin/payment/details.gohtml:159
#: web/templates/admin/customer/form.gohtml:117
#: web/templates/admin/taxDetails.gohtml:101 #: web/templates/admin/taxDetails.gohtml:101
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
@ -411,11 +416,16 @@ msgid "Order Number"
msgstr "Numéro de commande" msgstr "Numéro de commande"
#: web/templates/public/payment/details.gohtml:8 #: web/templates/public/payment/details.gohtml:8
#: web/templates/admin/invoice/index.gohtml:103
#: web/templates/admin/invoice/view.gohtml:26
msgctxt "title" msgctxt "title"
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
#: web/templates/public/payment/details.gohtml:12 #: web/templates/public/payment/details.gohtml:12
#: web/templates/admin/invoice/form.gohtml:119
#: web/templates/admin/invoice/view.gohtml:63
#: web/templates/admin/invoice/view.gohtml:103
msgctxt "title" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Totale" msgstr "Totale"
@ -579,6 +589,11 @@ msgctxt "input"
msgid "Year" msgid "Year"
msgstr "Année" msgstr "Année"
#: web/templates/public/form.gohtml:83 web/templates/admin/form.gohtml:83
msgctxt "action"
msgid "Filters"
msgstr "Filtres"
#: web/templates/public/campsite/type.gohtml:49 #: web/templates/public/campsite/type.gohtml:49
#: web/templates/public/booking/fields.gohtml:278 #: web/templates/public/booking/fields.gohtml:278
msgctxt "action" msgctxt "action"
@ -929,7 +944,7 @@ msgstr "Menu"
#: web/templates/admin/campsite/type/option/form.gohtml:16 #: web/templates/admin/campsite/type/option/form.gohtml:16
#: web/templates/admin/campsite/type/option/index.gohtml:10 #: web/templates/admin/campsite/type/option/index.gohtml:10
#: web/templates/admin/campsite/type/index.gohtml:10 #: web/templates/admin/campsite/type/index.gohtml:10
#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:95 #: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:101
#: web/templates/admin/booking/fields.gohtml:266 #: web/templates/admin/booking/fields.gohtml:266
msgctxt "title" msgctxt "title"
msgid "Campsites" msgid "Campsites"
@ -1004,13 +1019,15 @@ msgid "Campground map"
msgstr "Plan du camping" msgstr "Plan du camping"
#: web/templates/public/booking/fields.gohtml:176 #: web/templates/public/booking/fields.gohtml:176
#: web/templates/admin/customer/form.gohtml:81
msgctxt "input" msgctxt "input"
msgid "Town or village" msgid "Town or village"
msgstr "Ville" msgstr "Ville"
#: web/templates/public/booking/fields.gohtml:193 #: web/templates/public/booking/fields.gohtml:193
#: web/templates/admin/customer/form.gohtml:121
#: web/templates/admin/booking/fields.gohtml:204 #: web/templates/admin/booking/fields.gohtml:204
#: web/templates/admin/booking/guest.gohtml:109 #: web/templates/admin/booking/guest.gohtml:111
msgid "Choose a country" msgid "Choose a country"
msgstr "Choisissez un pays" msgstr "Choisissez un pays"
@ -1164,6 +1181,7 @@ msgstr "Slug"
#: web/templates/admin/campsite/type/form.gohtml:51 #: web/templates/admin/campsite/type/form.gohtml:51
#: web/templates/admin/campsite/type/option/form.gohtml:41 #: web/templates/admin/campsite/type/option/form.gohtml:41
#: web/templates/admin/season/form.gohtml:50 #: web/templates/admin/season/form.gohtml:50
#: web/templates/admin/invoice/product-form.gohtml:16
#: web/templates/admin/services/form.gohtml:53 #: web/templates/admin/services/form.gohtml:53
#: web/templates/admin/profile.gohtml:29 #: web/templates/admin/profile.gohtml:29
#: web/templates/admin/surroundings/form.gohtml:41 #: web/templates/admin/surroundings/form.gohtml:41
@ -1188,6 +1206,8 @@ msgstr "Contenu"
#: web/templates/admin/campsite/type/form.gohtml:287 #: web/templates/admin/campsite/type/form.gohtml:287
#: web/templates/admin/campsite/type/option/form.gohtml:98 #: web/templates/admin/campsite/type/option/form.gohtml:98
#: web/templates/admin/season/form.gohtml:73 #: web/templates/admin/season/form.gohtml:73
#: web/templates/admin/customer/form.gohtml:153
#: web/templates/admin/invoice/form.gohtml:137
#: web/templates/admin/services/form.gohtml:81 #: web/templates/admin/services/form.gohtml:81
#: web/templates/admin/surroundings/form.gohtml:69 #: web/templates/admin/surroundings/form.gohtml:69
#: web/templates/admin/surroundings/index.gohtml:58 #: web/templates/admin/surroundings/index.gohtml:58
@ -1211,6 +1231,7 @@ msgstr "Mettre à jour"
#: web/templates/admin/campsite/type/form.gohtml:289 #: web/templates/admin/campsite/type/form.gohtml:289
#: web/templates/admin/campsite/type/option/form.gohtml:100 #: web/templates/admin/campsite/type/option/form.gohtml:100
#: web/templates/admin/season/form.gohtml:75 #: web/templates/admin/season/form.gohtml:75
#: web/templates/admin/customer/form.gohtml:155
#: web/templates/admin/services/form.gohtml:83 #: web/templates/admin/services/form.gohtml:83
#: web/templates/admin/surroundings/form.gohtml:71 #: web/templates/admin/surroundings/form.gohtml:71
#: web/templates/admin/amenity/feature/form.gohtml:67 #: web/templates/admin/amenity/feature/form.gohtml:67
@ -1232,6 +1253,7 @@ msgstr "Ajouter un texte juridique"
#: web/templates/admin/campsite/type/option/index.gohtml:30 #: web/templates/admin/campsite/type/option/index.gohtml:30
#: web/templates/admin/campsite/type/index.gohtml:29 #: web/templates/admin/campsite/type/index.gohtml:29
#: web/templates/admin/season/index.gohtml:29 #: web/templates/admin/season/index.gohtml:29
#: web/templates/admin/customer/index.gohtml:19
#: web/templates/admin/user/index.gohtml:20 #: web/templates/admin/user/index.gohtml:20
#: web/templates/admin/surroundings/index.gohtml:83 #: web/templates/admin/surroundings/index.gohtml:83
#: web/templates/admin/amenity/feature/index.gohtml:30 #: web/templates/admin/amenity/feature/index.gohtml:30
@ -1725,6 +1747,7 @@ msgid "Per night"
msgstr "Par nuit" msgstr "Par nuit"
#: web/templates/admin/campsite/type/option/form.gohtml:84 #: web/templates/admin/campsite/type/option/form.gohtml:84
#: web/templates/admin/invoice/product-form.gohtml:29
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Prix" msgstr "Prix"
@ -1824,12 +1847,316 @@ msgctxt "action"
msgid "Cancel" msgid "Cancel"
msgstr "Annuler" msgstr "Annuler"
#: web/templates/admin/customer/form.gohtml:8
msgctxt "title"
msgid "Edit Customer"
msgstr "Modifier le client"
#: web/templates/admin/customer/form.gohtml:10
msgctxt "title"
msgid "New Customer"
msgstr "Nouveau client"
#: web/templates/admin/customer/form.gohtml:15
#: web/templates/admin/invoice/index.gohtml:105
msgctxt "title"
msgid "Customer"
msgstr "Client"
#: web/templates/admin/customer/form.gohtml:44
#: web/templates/admin/booking/guest.gohtml:8
msgctxt "input"
msgid "ID document number"
msgstr "Numéro de document didentité"
#: web/templates/admin/customer/form.gohtml:56
#: web/templates/admin/booking/guest.gohtml:20
msgctxt "input"
msgid "ID document type"
msgstr "Type de document didentité"
#: web/templates/admin/customer/form.gohtml:61
#: web/templates/admin/booking/guest.gohtml:25
msgid "Choose an ID document type"
msgstr "Choisissez un type de document didentité"
#: web/templates/admin/customer/form.gohtml:93
#: web/templates/admin/taxDetails.gohtml:85
msgctxt "input"
msgid "Province"
msgstr "Province"
#: web/templates/admin/customer/form.gohtml:129
#: web/templates/admin/booking/fields.gohtml:239
msgctxt "input"
msgid "Email (optional)"
msgstr "E-mail (facultatif)"
#: web/templates/admin/customer/form.gohtml:140
#: web/templates/admin/booking/fields.gohtml:248
#: web/templates/admin/booking/guest.gohtml:119
msgctxt "input"
msgid "Phone (optional)"
msgstr "Téléphone (facultatif)"
#: web/templates/admin/customer/index.gohtml:6
#: web/templates/admin/layout.gohtml:95
msgctxt "title"
msgid "Customers"
msgstr "Clients"
#: web/templates/admin/customer/index.gohtml:14
msgctxt "action"
msgid "Add Customer"
msgstr "Ajouter un client"
#: web/templates/admin/customer/index.gohtml:20
#: web/templates/admin/user/login-attempts.gohtml:20
#: web/templates/admin/user/index.gohtml:21
msgctxt "header"
msgid "Email"
msgstr "E-mail"
#: web/templates/admin/customer/index.gohtml:21
msgctxt "header"
msgid "Phone"
msgstr "Téléphone"
#: web/templates/admin/customer/index.gohtml:33
msgid "No customer found."
msgstr "Aucun client trouvée."
#: web/templates/admin/dashboard.gohtml:6 #: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:89 #: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:89
msgctxt "title" msgctxt "title"
msgid "Dashboard" msgid "Dashboard"
msgstr "Tableau de bord" msgstr "Tableau de bord"
#: web/templates/admin/invoice/product-form.gohtml:11
#: web/templates/admin/booking/guest.gohtml:5
msgctxt "action"
msgid "Remove"
msgstr "Retirer"
#: web/templates/admin/invoice/product-form.gohtml:44
msgctxt "input"
msgid "Quantity"
msgstr "Quantité"
#: web/templates/admin/invoice/product-form.gohtml:58
msgctxt "input"
msgid "Discount (%)"
msgstr "Rabais (%)"
#: web/templates/admin/invoice/product-form.gohtml:73
msgctxt "input"
msgid "Taxes"
msgstr "Taxes"
#: web/templates/admin/invoice/product-form.gohtml:79
msgid "Select a TAX"
msgstr "Choisissez une taxe"
#: web/templates/admin/invoice/form.gohtml:4
msgctxt "title"
msgid "Edit Invoice “%s”"
msgstr "Modifier la facture «%s»"
#: web/templates/admin/invoice/form.gohtml:6
msgctxt "title"
msgid "New Invoice"
msgstr "Nouvelle facture"
#: web/templates/admin/invoice/form.gohtml:15
#: web/templates/admin/invoice/index.gohtml:2
#: web/templates/admin/invoice/view.gohtml:6
#: web/templates/admin/layout.gohtml:98
msgctxt "title"
msgid "Invoices"
msgstr "Factures"
#: web/templates/admin/invoice/form.gohtml:32
msgid "Product “%s” removed"
msgstr "Produit «%s» supprimé"
#: web/templates/admin/invoice/form.gohtml:36
msgctxt "action"
msgid "Undo"
msgstr "Annuler"
#: web/templates/admin/invoice/form.gohtml:51
#: web/templates/admin/invoice/index.gohtml:39
msgctxt "input"
msgid "Customer"
msgstr "Client"
#: web/templates/admin/invoice/form.gohtml:56
msgid "Select a customer"
msgstr "Choisissez un client"
#: web/templates/admin/invoice/form.gohtml:64
msgctxt "input"
msgid "Invoice date"
msgstr "Date de facture"
#: web/templates/admin/invoice/form.gohtml:77
#: web/templates/admin/invoice/index.gohtml:51
msgctxt "input"
msgid "Invoice status"
msgstr "Statut de la facture"
#: web/templates/admin/invoice/form.gohtml:92
msgctxt "input"
msgid "Notes (optional)"
msgstr "Remarques (facultatif)"
#: web/templates/admin/invoice/form.gohtml:109
#: web/templates/admin/invoice/view.gohtml:59
msgctxt "title"
msgid "Subtotal"
msgstr "Sous-totale"
#: web/templates/admin/invoice/form.gohtml:133
msgctxt "action"
msgid "Add products"
msgstr "Ajouter des produits"
#: web/templates/admin/invoice/form.gohtml:140
msgctxt "action"
msgid "Save"
msgstr "Enregistrer"
#: web/templates/admin/invoice/index.gohtml:25
msgctxt "action"
msgid "Download invoices"
msgstr "Télécharger les factures"
#: web/templates/admin/invoice/index.gohtml:28
msgctxt "action"
msgid "Export list"
msgstr "Exporter la liste"
#: web/templates/admin/invoice/index.gohtml:43
msgid "All customers"
msgstr "Tous les clients"
#: web/templates/admin/invoice/index.gohtml:55
msgid "All statuses"
msgstr "Tous les statuts"
#: web/templates/admin/invoice/index.gohtml:63
msgctxt "input"
msgid "From date"
msgstr "Partir de la date"
#: web/templates/admin/invoice/index.gohtml:72
msgctxt "input"
msgid "To date"
msgstr "À ce jour"
#: web/templates/admin/invoice/index.gohtml:81
msgctxt "input"
msgid "Invoice number"
msgstr "Numéro de facture"
#: web/templates/admin/invoice/index.gohtml:91
msgctxt "action"
msgid "Filter"
msgstr "Filtrer"
#: web/templates/admin/invoice/index.gohtml:94
msgctxt "action"
msgid "Reset"
msgstr "Réinitialiser"
#: web/templates/admin/invoice/index.gohtml:97
msgctxt "action"
msgid "Add invoice"
msgstr "Nouvelle facture"
#: web/templates/admin/invoice/index.gohtml:102
msgctxt "invoice"
msgid "All"
msgstr "Toutes"
#: web/templates/admin/invoice/index.gohtml:104
msgctxt "title"
msgid "Invoice Num."
msgstr "Num. de facture"
#: web/templates/admin/invoice/index.gohtml:106
msgctxt "title"
msgid "Status"
msgstr "Statut"
#: web/templates/admin/invoice/index.gohtml:107
msgctxt "title"
msgid "Download"
msgstr "Téléchargement"
#: web/templates/admin/invoice/index.gohtml:108
msgctxt "title"
msgid "Amount"
msgstr "Import"
#: web/templates/admin/invoice/index.gohtml:115
msgctxt "action"
msgid "Select invoice %v"
msgstr "Sélectionner la facture %v"
#: web/templates/admin/invoice/index.gohtml:144
msgctxt "action"
msgid "Download invoice %s"
msgstr "Télécharger la facture %s"
#: web/templates/admin/invoice/index.gohtml:154
msgid "No invoices added yet."
msgstr "Aucune facture na encore été ajouté."
#: web/templates/admin/invoice/index.gohtml:161
msgid "Total"
msgstr "Totale"
#: web/templates/admin/invoice/view.gohtml:2
msgctxt "title"
msgid "Invoice %s"
msgstr "Facture %s"
#: web/templates/admin/invoice/view.gohtml:15
msgctxt "action"
msgid "Edit"
msgstr "Éditer"
#: web/templates/admin/invoice/view.gohtml:18
msgctxt "action"
msgid "Download invoice"
msgstr "Télécharger facture"
#: web/templates/admin/invoice/view.gohtml:53
msgctxt "title"
msgid "Concept"
msgstr "Concept"
#: web/templates/admin/invoice/view.gohtml:54
msgctxt "title"
msgid "Price"
msgstr "Prix"
#: web/templates/admin/invoice/view.gohtml:56
msgctxt "title"
msgid "Discount"
msgstr "Rabais"
#: web/templates/admin/invoice/view.gohtml:58
msgctxt "title"
msgid "Units"
msgstr "Unités"
#: web/templates/admin/invoice/view.gohtml:93
msgctxt "title"
msgid "Tax Base"
msgstr "Import imposable"
#: web/templates/admin/login.gohtml:6 web/templates/admin/login.gohtml:18 #: web/templates/admin/login.gohtml:6 web/templates/admin/login.gohtml:18
msgctxt "title" msgctxt "title"
msgid "Login" msgid "Login"
@ -1928,12 +2255,6 @@ msgctxt "title"
msgid "Users" msgid "Users"
msgstr "Utilisateurs" msgstr "Utilisateurs"
#: web/templates/admin/user/login-attempts.gohtml:20
#: web/templates/admin/user/index.gohtml:21
msgctxt "header"
msgid "Email"
msgstr "E-mail"
#: web/templates/admin/user/login-attempts.gohtml:21 #: web/templates/admin/user/login-attempts.gohtml:21
msgctxt "header" msgctxt "header"
msgid "IP Address" msgid "IP Address"
@ -1985,11 +2306,6 @@ msgctxt "input"
msgid "Trade Name" msgid "Trade Name"
msgstr "Nom commercial" msgstr "Nom commercial"
#: web/templates/admin/taxDetails.gohtml:85
msgctxt "input"
msgid "Province"
msgstr "Province"
#: web/templates/admin/taxDetails.gohtml:111 #: web/templates/admin/taxDetails.gohtml:111
msgctxt "input" msgctxt "input"
msgid "Currency" msgid "Currency"
@ -2214,11 +2530,11 @@ msgctxt "title"
msgid "Bookings" msgid "Bookings"
msgstr "Réservations" msgstr "Réservations"
#: web/templates/admin/layout.gohtml:101 #: web/templates/admin/layout.gohtml:107
msgid "Breadcrumb" msgid "Breadcrumb"
msgstr "Fil dAriane" msgstr "Fil dAriane"
#: web/templates/admin/layout.gohtml:113 #: web/templates/admin/layout.gohtml:119
msgid "Camper Version: %s" msgid "Camper Version: %s"
msgstr "Camper version: %s" msgstr "Camper version: %s"
@ -2340,7 +2656,7 @@ msgid "Country (optional)"
msgstr "Pays (facultatif)" msgstr "Pays (facultatif)"
#: web/templates/admin/booking/fields.gohtml:212 #: web/templates/admin/booking/fields.gohtml:212
#: web/templates/admin/booking/guest.gohtml:128 #: web/templates/admin/booking/guest.gohtml:130
msgctxt "input" msgctxt "input"
msgid "Address (optional)" msgid "Address (optional)"
msgstr "Adresse (facultatif)" msgstr "Adresse (facultatif)"
@ -2355,17 +2671,6 @@ msgctxt "input"
msgid "Town or village (optional)" msgid "Town or village (optional)"
msgstr "Ville (facultatif)" msgstr "Ville (facultatif)"
#: web/templates/admin/booking/fields.gohtml:239
msgctxt "input"
msgid "Email (optional)"
msgstr "E-mail (facultatif)"
#: web/templates/admin/booking/fields.gohtml:248
#: web/templates/admin/booking/guest.gohtml:117
msgctxt "input"
msgid "Phone (optional)"
msgstr "Téléphone (facultatif)"
#: web/templates/admin/booking/form.gohtml:8 #: web/templates/admin/booking/form.gohtml:8
msgctxt "title" msgctxt "title"
msgid "Edit Booking" msgid "Edit Booking"
@ -2430,60 +2735,41 @@ msgstr "Nom du titulaire"
msgid "No booking found." msgid "No booking found."
msgstr "Aucune réservation trouvée." msgstr "Aucune réservation trouvée."
#: web/templates/admin/booking/guest.gohtml:5
msgctxt "action"
msgid "Remove"
msgstr "Retirer"
#: web/templates/admin/booking/guest.gohtml:8
msgctxt "input"
msgid "ID document number"
msgstr "Numéro de document didentité"
#: web/templates/admin/booking/guest.gohtml:20
msgctxt "input"
msgid "ID document type"
msgstr "Type de document didentité"
#: web/templates/admin/booking/guest.gohtml:25
msgid "Choose an ID document type"
msgstr "Choisissez un type de document didentité"
#: web/templates/admin/booking/guest.gohtml:33 #: web/templates/admin/booking/guest.gohtml:33
msgctxt "input" msgctxt "input"
msgid "ID document issue date (if any)" msgid "ID document issue date (if any)"
msgstr "Date de délivrance du document didentité (si j'en ai)" msgstr "Date de délivrance du document didentité (si j'en ai)"
#: web/templates/admin/booking/guest.gohtml:44 #: web/templates/admin/booking/guest.gohtml:45
msgctxt "input" msgctxt "input"
msgid "First surname" msgid "First surname"
msgstr "Premier nom" msgstr "Premier nom"
#: web/templates/admin/booking/guest.gohtml:56 #: web/templates/admin/booking/guest.gohtml:57
msgctxt "input" msgctxt "input"
msgid "Second surname (if has one)" msgid "Second surname (if has one)"
msgstr "Deuxième nom" msgstr "Deuxième nom"
#: web/templates/admin/booking/guest.gohtml:67 #: web/templates/admin/booking/guest.gohtml:68
msgctxt "input" msgctxt "input"
msgid "Given name" msgid "Given name"
msgstr "Prénom" msgstr "Prénom"
#: web/templates/admin/booking/guest.gohtml:79 #: web/templates/admin/booking/guest.gohtml:80
msgctxt "input" msgctxt "input"
msgid "Sex" msgid "Sex"
msgstr "Sexe" msgstr "Sexe"
#: web/templates/admin/booking/guest.gohtml:84 #: web/templates/admin/booking/guest.gohtml:85
msgid "Choose a sex" msgid "Choose a sex"
msgstr "Choisissez un sexe" msgstr "Choisissez un sexe"
#: web/templates/admin/booking/guest.gohtml:92 #: web/templates/admin/booking/guest.gohtml:93
msgctxt "input" msgctxt "input"
msgid "Birthdate" msgid "Birthdate"
msgstr "Date de naissance" msgstr "Date de naissance"
#: web/templates/admin/booking/guest.gohtml:104 #: web/templates/admin/booking/guest.gohtml:106
msgctxt "input" msgctxt "input"
msgid "Nationality" msgid "Nationality"
msgstr "Nationalité" msgstr "Nationalité"
@ -2553,8 +2839,9 @@ msgstr "Paiement de réservation reçu avec succès"
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365 #: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:365
#: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577 #: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
#: pkg/campsite/feature.go:269 pkg/season/admin.go:411 #: pkg/campsite/feature.go:269 pkg/season/admin.go:411
#: pkg/services/admin.go:316 pkg/surroundings/admin.go:340 #: pkg/invoice/admin.go:1092 pkg/services/admin.go:316
#: pkg/amenity/feature.go:269 pkg/amenity/admin.go:283 #: pkg/surroundings/admin.go:340 pkg/amenity/feature.go:269
#: pkg/amenity/admin.go:283
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "Le nom ne peut pas être laissé vide." msgstr "Le nom ne peut pas être laissé vide."
@ -2589,12 +2876,12 @@ msgid "Slide image must be an image media type."
msgstr "Limage de la diapositive doit être de type média dimage." msgstr "Limage de la diapositive doit être de type média dimage."
#: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:224 #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:224
#: pkg/booking/public.go:596 #: pkg/booking/public.go:592
msgid "Email can not be empty." msgid "Email can not be empty."
msgstr "Le-mail ne peut pas être vide." msgstr "Le-mail ne peut pas être vide."
#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:225 #: pkg/app/login.go:57 pkg/app/user.go:247 pkg/customer/admin.go:312
#: pkg/booking/admin.go:437 pkg/booking/public.go:597 #: pkg/company/admin.go:225 pkg/booking/admin.go:438 pkg/booking/public.go:593
msgid "This email is not valid. It should be like name@domain.com." msgid "This email is not valid. It should be like name@domain.com."
msgstr "Cette adresse e-mail nest pas valide. Il devrait en être name@domain.com." msgstr "Cette adresse e-mail nest pas valide. Il devrait en être name@domain.com."
@ -2623,7 +2910,7 @@ msgstr "La langue sélectionnée nest pas valide."
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "Le fichier doit être une image PNG ou JPEG valide." msgstr "Le fichier doit être une image PNG ou JPEG valide."
#: pkg/app/admin.go:73 #: pkg/app/admin.go:79
msgid "Access forbidden" msgid "Access forbidden"
msgstr "Accès interdit" msgstr "Accès interdit"
@ -2651,15 +2938,15 @@ msgstr "Le maximum doit être un nombre entier."
msgid "Maximum must be equal or greater than minimum." msgid "Maximum must be equal or greater than minimum."
msgstr "Le maximum doit être égal ou supérieur au minimum." msgstr "Le maximum doit être égal ou supérieur au minimum."
#: pkg/campsite/types/option.go:382 #: pkg/campsite/types/option.go:382 pkg/invoice/admin.go:1093
msgid "Price can not be empty." msgid "Price can not be empty."
msgstr "Le prix ne peut pas être vide." msgstr "Le prix ne peut pas être vide."
#: pkg/campsite/types/option.go:383 #: pkg/campsite/types/option.go:383 pkg/invoice/admin.go:1094
msgid "Price must be a decimal number." msgid "Price must be a decimal number."
msgstr "Le prix doit être un nombre décimal." msgstr "Le prix doit être un nombre décimal."
#: pkg/campsite/types/option.go:384 #: pkg/campsite/types/option.go:384 pkg/invoice/admin.go:1095
msgid "Price must be zero or greater." msgid "Price must be zero or greater."
msgstr "Le prix doit être égal ou supérieur à zéro." msgstr "Le prix doit être égal ou supérieur à zéro."
@ -2805,7 +3092,7 @@ msgctxt "header"
msgid "Children (aged 2 to 10)" msgid "Children (aged 2 to 10)"
msgstr "Enfants (de 2 à 10 anys)" msgstr "Enfants (de 2 à 10 anys)"
#: pkg/campsite/admin.go:280 pkg/booking/admin.go:413 pkg/booking/public.go:177 #: pkg/campsite/admin.go:280 pkg/booking/admin.go:414 pkg/booking/public.go:177
#: pkg/booking/public.go:232 #: pkg/booking/public.go:232
msgid "Selected campsite type is not valid." msgid "Selected campsite type is not valid."
msgstr "Le type demplacement sélectionné nest pas valide." msgstr "Le type demplacement sélectionné nest pas valide."
@ -2843,6 +3130,136 @@ msgstr "La date de fin ne peut pas être vide."
msgid "End date must be a valid date." msgid "End date must be a valid date."
msgstr "La date de fin doit être une date valide." msgstr "La date de fin doit être une date valide."
#: pkg/customer/admin.go:293 pkg/company/admin.go:207
#: pkg/booking/checkin.go:297 pkg/booking/public.go:577
msgid "Selected country is not valid."
msgstr "Le pays sélectionné nest pas valide."
#: pkg/customer/admin.go:297 pkg/booking/checkin.go:281
msgid "Selected ID document type is not valid."
msgstr "Le type de document didentité sélectionné nest pas valide."
#: pkg/customer/admin.go:298 pkg/booking/checkin.go:282
msgid "ID document number can not be empty."
msgstr "Le numéro de documento didentité ne peut pas être vide."
#: pkg/customer/admin.go:300 pkg/booking/checkin.go:288
#: pkg/booking/checkin.go:289 pkg/booking/admin.go:426
#: pkg/booking/public.go:581
msgid "Full name can not be empty."
msgstr "Le nom complet ne peut pas être vide."
#: pkg/customer/admin.go:301 pkg/booking/admin.go:427 pkg/booking/public.go:582
msgid "Full name must have at least one letter."
msgstr "Le nom complet doit comporter au moins une lettre."
#: pkg/customer/admin.go:304 pkg/company/admin.go:230 pkg/booking/public.go:585
msgid "Address can not be empty."
msgstr "Ladresse ne peut pas être vide."
#: pkg/customer/admin.go:305 pkg/booking/public.go:586
msgid "Town or village can not be empty."
msgstr "La ville ne peut pas être vide."
#: pkg/customer/admin.go:306 pkg/company/admin.go:233 pkg/booking/public.go:587
msgid "Postcode can not be empty."
msgstr "Le code postal ne peut pas être vide."
#: pkg/customer/admin.go:307 pkg/company/admin.go:234 pkg/booking/admin.go:433
#: pkg/booking/public.go:588
msgid "This postcode is not valid."
msgstr "Ce code postal nest pas valide."
#: pkg/customer/admin.go:315 pkg/company/admin.go:220
#: pkg/booking/checkin.go:301 pkg/booking/admin.go:443
#: pkg/booking/public.go:596
msgid "This phone number is not valid."
msgstr "Ce numéro de téléphone nest pas valide."
#: pkg/invoice/admin.go:649
msgctxt "filename"
msgid "invoices.zip"
msgstr "factures.zip"
#: pkg/invoice/admin.go:664
msgctxt "filename"
msgid "invoices.ods"
msgstr "factures.ods"
#: pkg/invoice/admin.go:666 pkg/invoice/admin.go:1285 pkg/invoice/admin.go:1292
msgid "Invalid action"
msgstr "Actin invalide"
#: pkg/invoice/admin.go:830
msgid "Selected invoice status is not valid."
msgstr "Lstatut sélectionné nest pas valide."
#: pkg/invoice/admin.go:831
msgid "Selected customer is not valid."
msgstr "Le client sélectionné nest pas valide."
#: pkg/invoice/admin.go:832
msgid "Invoice date can not be empty."
msgstr "La date de facture ne peut pas être vide."
#: pkg/invoice/admin.go:833
msgid "Invoice date must be a valid date."
msgstr "La date de facture doit être une date valide."
#: pkg/invoice/admin.go:980
#, c-format
msgid "Re: quotation #%s of %s"
msgstr ""
#: pkg/invoice/admin.go:981
msgctxt "to_char"
msgid "MM/DD/YYYY"
msgstr "DD/MM/YYYY"
#: pkg/invoice/admin.go:1083
msgid "Invoice product ID must be an integer."
msgstr "Le ID de produit de facture doit être un entier."
#: pkg/invoice/admin.go:1084
msgid "Invoice product ID one or greater."
msgstr "Le ID de produit de facture doit être égal ou supérieur à un."
#: pkg/invoice/admin.go:1088
msgid "Product ID must be an integer."
msgstr "Le ID de produit doit être un entier."
#: pkg/invoice/admin.go:1089
msgid "Product ID must zero or greater."
msgstr "Le ID de produit doit être égal ou supérieur à zéro."
#: pkg/invoice/admin.go:1098
msgid "Quantity can not be empty."
msgstr "La quantité ne peut pas être vide."
#: pkg/invoice/admin.go:1099
msgid "Quantity must be an integer."
msgstr "La quantité doit être un entier."
#: pkg/invoice/admin.go:1100
msgid "Quantity must one or greater."
msgstr "La quantité doit être égnal ou supérieur à zéro."
#: pkg/invoice/admin.go:1103
msgid "Discount can not be empty."
msgstr "Le rabais ne peut pas être vide."
#: pkg/invoice/admin.go:1104
msgid "Discount must be an integer."
msgstr "Le rabais doit être un entier."
#: pkg/invoice/admin.go:1105 pkg/invoice/admin.go:1106
msgid "Discount must be a percentage between 0 and 100."
msgstr "Le rabais doit être un pourcentage compris entre 0 et 100."
#: pkg/invoice/admin.go:1110
msgid "Selected tax is not valid."
msgstr "La taxe sélectionnée nest pas valide."
#: pkg/user/admin.go:18 #: pkg/user/admin.go:18
msgctxt "role" msgctxt "role"
msgid "guest" msgid "guest"
@ -2902,11 +3319,6 @@ msgstr "Laddresse du lien ne peut pas être vide."
msgid "This web address is not valid. It should be like https://domain.com/." msgid "This web address is not valid. It should be like https://domain.com/."
msgstr "Cette adresse web nest pas valide. Il devrait en être https://domain.com/." msgstr "Cette adresse web nest pas valide. Il devrait en être https://domain.com/."
#: pkg/company/admin.go:207 pkg/booking/checkin.go:301
#: pkg/booking/public.go:581
msgid "Selected country is not valid."
msgstr "Le pays sélectionné nest pas valide."
#: pkg/company/admin.go:211 #: pkg/company/admin.go:211
msgid "Business name can not be empty." msgid "Business name can not be empty."
msgstr "Le nom de lentreprise ne peut pas être vide." msgstr "Le nom de lentreprise ne peut pas être vide."
@ -2923,19 +3335,10 @@ msgstr "Le numéro de TVA ne peut pas être vide."
msgid "This VAT number is not valid." msgid "This VAT number is not valid."
msgstr "Ce numéro de TVA nest pas valide." msgstr "Ce numéro de TVA nest pas valide."
#: pkg/company/admin.go:219 pkg/booking/public.go:599 #: pkg/company/admin.go:219 pkg/booking/public.go:595
msgid "Phone can not be empty." msgid "Phone can not be empty."
msgstr "Le téléphone ne peut pas être vide." msgstr "Le téléphone ne peut pas être vide."
#: pkg/company/admin.go:220 pkg/booking/checkin.go:305 pkg/booking/admin.go:442
#: pkg/booking/public.go:600
msgid "This phone number is not valid."
msgstr "Ce numéro de téléphone nest pas valide."
#: pkg/company/admin.go:230 pkg/booking/public.go:589
msgid "Address can not be empty."
msgstr "Ladresse ne peut pas être vide."
#: pkg/company/admin.go:231 #: pkg/company/admin.go:231
msgid "City can not be empty." msgid "City can not be empty."
msgstr "La ville ne peut pas être vide." msgstr "La ville ne peut pas être vide."
@ -2944,14 +3347,6 @@ msgstr "La ville ne peut pas être vide."
msgid "Province can not be empty." msgid "Province can not be empty."
msgstr "La province ne peut pas être vide." msgstr "La province ne peut pas être vide."
#: pkg/company/admin.go:233 pkg/booking/public.go:591
msgid "Postcode can not be empty."
msgstr "Le code postal ne peut pas être vide."
#: pkg/company/admin.go:234 pkg/booking/admin.go:432 pkg/booking/public.go:592
msgid "This postcode is not valid."
msgstr "Ce code postal nest pas valide."
#: pkg/company/admin.go:238 #: pkg/company/admin.go:238
msgid "RTC number can not be empty." msgid "RTC number can not be empty."
msgstr "Le numéro RTC ne peut pas être vide." msgstr "Le numéro RTC ne peut pas être vide."
@ -3000,40 +3395,27 @@ msgstr "Le fichier téléchargé ne peut pas être vide."
msgid "Filename can not be empty." msgid "Filename can not be empty."
msgstr "Le nom de fichier ne peut pas être vide." msgstr "Le nom de fichier ne peut pas être vide."
#: pkg/booking/checkin.go:285 #: pkg/booking/checkin.go:284
msgid "Selected ID document type is not valid."
msgstr "Le type de document didentité sélectionné nest pas valide."
#: pkg/booking/checkin.go:286
msgid "ID document number can not be empty."
msgstr "Le numéro de documento didentité ne peut pas être vide."
#: pkg/booking/checkin.go:288
msgid "ID document issue date must be a valid date." msgid "ID document issue date must be a valid date."
msgstr "La date de délivrance du document didentité doit être une date valide." msgstr "La date de délivrance du document didentité doit être une date valide."
#: pkg/booking/checkin.go:289 #: pkg/booking/checkin.go:285
msgid "ID document issue date must be in the past." msgid "ID document issue date must be in the past."
msgstr "La ate de délivrance du document didentité doit être du passé." msgstr "La ate de délivrance du document didentité doit être du passé."
#: pkg/booking/checkin.go:292 pkg/booking/checkin.go:293 #: pkg/booking/checkin.go:290
#: pkg/booking/admin.go:425 pkg/booking/public.go:585
msgid "Full name can not be empty."
msgstr "Le nom complet ne peut pas être vide."
#: pkg/booking/checkin.go:294
msgid "Selected sex is not valid." msgid "Selected sex is not valid."
msgstr "Le sexe sélectionné nest pas valide." msgstr "Le sexe sélectionné nest pas valide."
#: pkg/booking/checkin.go:295 #: pkg/booking/checkin.go:291
msgid "Birthdate can not be empty" msgid "Birthdate can not be empty"
msgstr "La date de naissance ne peut pas être vide." msgstr "La date de naissance ne peut pas être vide."
#: pkg/booking/checkin.go:296 #: pkg/booking/checkin.go:292
msgid "Birthdate must be a valid date." msgid "Birthdate must be a valid date."
msgstr "La date de naissance doit être une date valide." msgstr "La date de naissance doit être une date valide."
#: pkg/booking/checkin.go:297 #: pkg/booking/checkin.go:293
msgid "Birthdate must be in the past." msgid "Birthdate must be in the past."
msgstr "La date de naissance doit être du passé." msgstr "La date de naissance doit être du passé."
@ -3057,28 +3439,24 @@ msgctxt "cart"
msgid "Dog" msgid "Dog"
msgstr "Chien" msgstr "Chien"
#: pkg/booking/admin.go:217 #: pkg/booking/admin.go:218
msgctxt "filename" msgctxt "filename"
msgid "bookings.ods" msgid "bookings.ods"
msgstr "reservations.ods" msgstr "reservations.ods"
#: pkg/booking/admin.go:426 pkg/booking/public.go:586 #: pkg/booking/admin.go:432
msgid "Full name must have at least one letter."
msgstr "Le nom complet doit comporter au moins une lettre."
#: pkg/booking/admin.go:431
msgid "Country can not be empty to validate the postcode." msgid "Country can not be empty to validate the postcode."
msgstr "Le pays ne peut pas être vide pour valider le code postal." msgstr "Le pays ne peut pas être vide pour valider le code postal."
#: pkg/booking/admin.go:441 #: pkg/booking/admin.go:442
msgid "Country can not be empty to validate the phone." msgid "Country can not be empty to validate the phone."
msgstr "Le pays ne peut pas être vide pour valider le téléphone." msgstr "Le pays ne peut pas être vide pour valider le téléphone."
#: pkg/booking/admin.go:448 #: pkg/booking/admin.go:449
msgid "You must select at least one accommodation." msgid "You must select at least one accommodation."
msgstr "Vous devez sélectionner au moins un hébergement." msgstr "Vous devez sélectionner au moins un hébergement."
#: pkg/booking/admin.go:454 #: pkg/booking/admin.go:455
msgid "The selected accommodations have no available openings in the requested dates." msgid "The selected accommodations have no available openings in the requested dates."
msgstr "Les hébergements sélectionnés nont pas de disponibilités aux dates demandées." msgstr "Les hébergements sélectionnés nont pas de disponibilités aux dates demandées."
@ -3191,11 +3569,7 @@ msgstr "%s doit être %d ou plus."
msgid "%s must be at most %d." msgid "%s must be at most %d."
msgstr "%s doit être tout au plus %d." msgstr "%s doit être tout au plus %d."
#: pkg/booking/public.go:590 #: pkg/booking/public.go:601
msgid "Town or village can not be empty."
msgstr "La ville ne peut pas être vide."
#: pkg/booking/public.go:605
msgid "It is mandatory to agree to the reservation conditions." msgid "It is mandatory to agree to the reservation conditions."
msgstr "Il est obligatoire daccepter les conditions de réservation." msgstr "Il est obligatoire daccepter les conditions de réservation."

17
revert/add_contact.sql Normal file
View File

@ -0,0 +1,17 @@
-- Deploy camper:add_contact to pg
-- requires: schema_camper
-- 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 camper, public;
drop function if exists add_contact(integer, text, text, text, text, text, text, text, text, text, country_code);
commit;

7
revert/add_invoice.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:add_invoice from pg
begin;
drop function if exists camper.add_invoice(integer, date, integer, text, integer, camper.new_invoice_product[]);
commit;

View File

@ -1,4 +1,4 @@
-- Revert numerus:available_currencies from pg -- Revert camper:available_currencies from pg
begin; begin;

View File

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

View File

@ -0,0 +1,7 @@
-- Revert camper:compute_new_invoice_amount from pg
begin;
drop function if exists camper.compute_new_invoice_amount(integer, camper.new_invoice_product[]);
commit;

8
revert/contact.sql Normal file
View File

@ -0,0 +1,8 @@
-- Revert camper:contact from pg
begin;
drop policy if exists company_policy on camper.contact;
drop table if exists camper.contact;
commit;

23
revert/contact_email.sql Normal file
View File

@ -0,0 +1,23 @@
-- Revert camper:contact_email from pg
begin;
set search_path to camper, 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;

24
revert/contact_phone.sql Normal file
View File

@ -0,0 +1,24 @@
-- Revert camper:contact_phone from pg
begin;
set search_path to camper, public;
alter table contact
add column phone packed_phone_number default '+34000000000'
;
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
, alter column phone drop default
;
drop table if exists contact_phone;
commit;

7
revert/discount_rate.sql Normal file
View File

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

17
revert/edit_contact.sql Normal file
View File

@ -0,0 +1,17 @@
-- Deploy camper:edit_contact to pg
-- requires: schema_camper
-- 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 camper, public;
drop function if exists edit_contact(uuid, text, text, text, text, text, text, text, text, text, country_code);
commit;

7
revert/edit_invoice.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:edit_invoice from pg
begin;
drop function if exists camper.edit_invoice(uuid, text, integer, text, integer, camper.edited_invoice_product[]);
commit;

View File

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

7
revert/invoice.sql Normal file
View File

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

View File

@ -0,0 +1,7 @@
-- Revert camper:invoice_amount from pg
begin;
drop view if exists camper.invoice_amount;
commit;

View File

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

View File

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

View File

@ -0,0 +1,7 @@
-- Revert camper:invoice_product_amount from pg
begin;
drop view if exists camper.invoice_product_amount;
commit;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
-- Revert camper:invoice_tax_amount from pg
begin;
drop view if exists camper.invoice_tax_amount;
commit;

View File

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

View File

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

View File

@ -0,0 +1,7 @@
-- Revert camper:next_invoice_number from pg
begin;
drop function if exists camper.next_invoice_number(integer, date);
commit;

View File

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

7
revert/product.sql Normal file
View File

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

7
revert/product_tax.sql Normal file
View File

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

8
revert/tax.sql Normal file
View File

@ -0,0 +1,8 @@
-- Revert camper:tax from pg
begin;
drop policy if exists company_policy on camper.tax;
drop table if exists camper.tax;
commit;

7
revert/tax_class.sql Normal file
View File

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

7
revert/tax_rate.sql Normal file
View File

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

View File

@ -299,3 +299,33 @@ available_id_document_types [id_document_type id_document_type_i18n] 2024-04-25T
booking_guest [roles schema_camper booking sex id_document_type extension_pg_libphonenumber] 2024-04-26T09:40:17Z jordi fita mas <jordi@tandem.blog> # Add relation of booking guests booking_guest [roles schema_camper booking sex id_document_type extension_pg_libphonenumber] 2024-04-26T09:40:17Z jordi fita mas <jordi@tandem.blog> # Add relation of booking guests
checked_in_guest [schema_camper] 2024-04-26T09:58:54Z jordi fita mas <jordi@tandem.blog> # Add type for checked-in guest checked_in_guest [schema_camper] 2024-04-26T09:58:54Z jordi fita mas <jordi@tandem.blog> # Add type for checked-in guest
check_in_guests [roles schema_camper booking booking_guest checked_in_guest extension_pg_libphonenumber] 2024-04-26T10:31:53Z jordi fita mas <jordi@tandem.blog> # Add function to check-in guests check_in_guests [roles schema_camper booking booking_guest checked_in_guest extension_pg_libphonenumber] 2024-04-26T10:31:53Z jordi fita mas <jordi@tandem.blog> # Add function to check-in guests
contact [roles schema_camper user_profile company id_document_type country_code country] 2024-04-27T22:48:18Z jordi fita mas <jordi@tandem.blog> # Add the relation for contacts
contact_phone [roles schema_camper extension_pg_libphonenumber] 2024-04-27T23:18:19Z jordi fita mas <jordi@tandem.blog> # Add relation to keep contacts phone numbers
contact_email [roles schema_camper email contact] 2024-04-27T23:18:19Z jordi fita mas <jordi@tandem.blog> # Add relation to keep contacts emails
invoice_status [schema_camper] 2024-04-27T22:48:26Z jordi fita mas <jordi@tandem.blog> # A relation of invoice status
invoice_status_i18n [schema_camper invoice_status language] 2024-04-27T22:48:18Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice status translatable texts
available_invoice_status [schema_camper invoice_status invoice_status_i18n] 2024-04-27T22:48:06Z jordi fita mas <jordi@tandem.blog> # Add the list of available invoice status
payment_method [roles schema_camper user_profile company] 2024-04-27T23:49:41Z jordi fita mas <jordi@tandem.blog> # Add relation of payment method
invoice [roles schema_camper user_profile company contact invoice_status payment_method currency] 2024-04-27T22:46:21Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice
discount_rate [roles schema_camper] 2024-04-27T23:54:40Z jordi fita mas <jordi@tandem.blog> # Add domain for discount rates
invoice_product [roles schema_camper invoice discount_rate] 2024-04-27T23:54:08Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice product
tax_class [roles schema_camper user_profile company] 2024-04-27T23:57:14Z jordi fita mas <jordi@tandem.blog> # Add the relation for tax classes
tax_rate [roles schema_camper] 2024-04-27T23:57:39Z jordi fita mas <jordi@tandem.blog> # Add domain for tax rates
tax [roles schema_camper user_profile company tax_rate tax_class] 2024-04-27T23:57:47Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes
product [roles schema_camper user_profile company] 2024-04-28T00:44:24Z jordi fita mas <jordi@tandem.blog> # Add relation for products
product_tax [roles schema_camper product tax] 2024-04-28T00:44:49Z jordi fita mas <jordi@tandem.blog> # Add relation of product taxes
invoice_product_product [roles schema_camper invoice_product product] 2024-04-28T00:43:30Z jordi fita mas <jordi@tandem.blog> # Add relation of invoice products and registered products
invoice_product_tax [roles schema_camper invoice_product tax tax_rate] 2024-04-27T23:54:30Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes in invoice products
new_invoice_product [roles schema_camper discount_rate] 2024-04-27T23:54:01Z jordi fita mas <jordi@tandem.blog> # Add type for passing products to new invoices
invoice_number_counter [roles schema_camper company] 2024-04-27T23:54:48Z jordi fita mas <jordi@tandem.blog> # Add relation to count invoice numbers
next_invoice_number [roles schema_camper invoice_number_counter] 2024-04-27T23:54:48Z jordi fita mas <jordi@tandem.blog> # Add function to retrieve the next invoice number
add_invoice [roles schema_camper invoice company currency parse_price new_invoice_product tax invoice_product invoice_product_product invoice_product_tax next_invoice_number] 2024-04-27T23:54:46Z jordi fita mas <jordi@tandem.blog> # Add function to create new invoices
invoice_tax_amount [roles schema_camper invoice_product invoice_product_tax] 2024-04-27T23:54:35Z jordi fita mas <jordi@tandem.blog> # Add view for invoice tax amount
invoice_product_amount [roles schema_camper invoice_product invoice_product_tax] 2024-04-27T23:54:05Z jordi fita mas <jordi@tandem.blog> # Add view for invoice product subtotal and total
invoice_amount [roles schema_camper invoice_product invoice_product_amount] 2024-04-27T23:54:46Z jordi fita mas <jordi@tandem.blog> # Add view to compute subtotal and total for invoices
new_invoice_amount [roles schema_camper] 2024-04-27T23:54:25Z jordi fita mas <jordi@tandem.blog> # Add type to return when computing new invoice amounts
compute_new_invoice_amount [roles schema_camper company currency tax new_invoice_product new_invoice_amount] 2024-04-27T23:54:13Z jordi fita mas <jordi@tandem.blog> # Add function to compute the subtotal, taxes, and total amounts for a new invoice
edited_invoice_product [roles schema_camper discount_rate] 2024-04-27T23:54:24Z jordi fita mas <jordi@tandem.blog> # Add typo for passing products to edited invoices
edit_invoice [roles schema_camper invoice currency parse_price edited_invoice_product tax invoice_product invoice_product_product invoice_product_tax] 2024-04-27T23:54:50Z jordi fita mas <jordi@tandem.blog> # Add function to edit invoices
add_contact [roles schema_camper email extension_pg_libphonenumber country_code contact contact_email contact_phone] 2024-04-28T14:21:37Z jordi fita mas <jordi@tandem.blog> # Add function to create new contacts
edit_contact [roles schema_camper email country_code contact extension_pg_libphonenumber contact_email contact_phone] 2024-04-28T14:21:27Z jordi fita mas <jordi@tandem.blog> # Add function to edit contacts

71
test/add_contact.sql Normal file
View File

@ -0,0 +1,71 @@
-- Test add_contact
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(14);
set search_path to auth, camper, public;
select has_function('camper', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code']);
select function_lang_is('camper', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'plpgsql');
select function_returns('camper', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'uuid');
select isnt_definer('camper', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code']);
select volatility_is('camper', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'volatile');
select function_privs_are('camper', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'guest', array []::text[]);
select function_privs_are('camper', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'employee', array ['EXECUTE']);
select function_privs_are('camper', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'admin', array ['EXECUTE']);
select function_privs_are('camper', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate contact_email cascade;
truncate contact_phone cascade;
truncate contact cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 7, 'FR', 'USD', 'ca')
;
select lives_ok(
$$ select add_contact(1, 'Contact 2.2', 'D', '41414141A', '977 977 977', '', 'Fake St., 123', 'Fake City', 'Fake Province', '17400', 'ES') $$,
'Should be able to insert a second contact for the first company with no email but with phone'
);
select lives_ok(
$$ select add_contact(2, 'Contact 4.1', 'C', '123ABC', '', 'e@e', 'Bullshit Av., 1', 'Another City', 'Another Province', 'ARBNNL22', 'FR') $$,
'Should be able to insert a contact for the second company with no phone but with email'
);
select bag_eq(
$$ select company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code, created_at from contact $$,
$$ values (1, 'Contact 2.2', 'D', '41414141A', 'Fake St., 123', 'Fake City', 'Fake Province', '17400', 'ES', CURRENT_TIMESTAMP)
, (2, 'Contact 4.1', 'C', '123ABC', 'Bullshit Av., 1', 'Another City', 'Another Province', 'ARBNNL22', 'FR', CURRENT_TIMESTAMP)
$$,
'Should have created all contacts'
);
select bag_eq(
$$ select name, phone::text from contact join contact_phone using (contact_id) $$,
$$ values ('Contact 2.2', '+34 977 97 79 77')
$$,
'Should have created all contacts phone'
);
select bag_eq(
$$ select name, email::text from contact join contact_email using (contact_id) $$,
$$ values ('Contact 4.1', 'e@e')
$$,
'Should have created all contacts email'
);
select *
from finish();
rollback;

138
test/add_invoice.sql Normal file
View File

@ -0,0 +1,138 @@
-- Test add_invoice
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(16);
set search_path to auth, camper, public;
select has_function('camper', 'add_invoice', array ['integer', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]']);
select function_lang_is('camper', 'add_invoice', array ['integer', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'plpgsql');
select function_returns('camper', 'add_invoice', array ['integer', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'uuid');
select isnt_definer('camper', 'add_invoice', array ['integer', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]']);
select volatility_is('camper', 'add_invoice', array ['integer', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'volatile');
select function_privs_are('camper', 'add_invoice', array ['integer', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'guest', array []::text[]);
select function_privs_are('camper', 'add_invoice', array ['integer', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'employee', array ['EXECUTE']);
select function_privs_are('camper', 'add_invoice', array ['integer', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'admin', array ['EXECUTE']);
select function_privs_are('camper', 'add_invoice', array ['integer', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate invoice_number_counter cascade;
truncate invoice_product_tax cascade;
truncate invoice_product cascade;
truncate invoice cascade;
truncate contact cascade;
truncate product cascade;
truncate tax cascade;
truncate tax_class cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag, invoice_number_format)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca', '"F"YYYY0000')
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 7, 'FR', 'USD', 'ca', '"INV"000-YY')
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
insert into invoice_number_counter (company_id, year, currval)
values (1, 2023, '5')
, (2, 2023, '55')
;
insert into tax_class (tax_class_id, company_id, name)
values (11, 1, 'tax')
, (22, 2, 'tax')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (3, 1, 11, 'IRPF -15 %', -0.15)
, (4, 1, 11, 'IVA 21 %', 0.21)
, (5, 2, 22, 'IRPF -7 %', -0.07)
, (6, 2, 22, 'IVA 10 %', 0.10)
;
insert into contact (contact_id, company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (12, 1, 'Contact 2.1', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (13, 1, 'Contact 2.2', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (14, 2, 'Contact 4.1', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (15, 2, 'Contact 4.2', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
;
insert into product (product_id, company_id, name, price)
values ( 7, 1, 'Product 2.1', 1212)
, ( 8, 1, 'Product 2.2', 2424)
, ( 9, 2, 'Product 4.1', 4848)
, (10, 2, 'Product 4.2', 9696)
, (11, 2, 'Product 4.3', 1010)
;
select lives_ok(
$$ select add_invoice(1, '2023-02-15', 12, 'Notes 1', 111, '{"(7,Product 1,Description 1,12.24,2,0.0,{4})"}') $$,
'Should be able to insert an invoice for the first company with a product'
);
select lives_ok(
$$ select add_invoice(1, '2023-02-16', 13, 'Notes 2', 111, '{"(7,Product 1 bis,Description 1 bis,33.33,1,0.50,\"{4,3}\")","(8,Product 2,Description 2,24.00,3,0.75,{})"}') $$,
'Should be able to insert a second invoice for the first company with two product'
);
select lives_ok(
$$ select add_invoice(2, '2023-02-14', 15, 'Notes 3', 222, '{"(11,Product 4.3,,11.11,1,0.0,{6})","(,Product 4.4,Description 4.4,22.22,3,0.05,{})"}') $$,
'Should be able to insert an invoice for the second company with a product'
);
select bag_eq(
$$ select company_id, invoice_number, invoice_date, contact_id, invoice_status, notes, payment_method_id, currency_code, created_at from invoice $$,
$$ values (1, 'F20230006', '2023-02-15'::date, 12, 'created', 'Notes 1', 111, 'EUR', current_timestamp)
, (1, 'F20230007', '2023-02-16'::date, 13, 'created', 'Notes 2', 111, 'EUR', current_timestamp)
, (2, 'INV056-23', '2023-02-14'::date, 15, 'created', 'Notes 3', 222, 'USD', current_timestamp)
$$,
'Should have created all invoices'
);
select bag_eq(
$$ select invoice_number, name, description, price, quantity, discount_rate from invoice_product join invoice using (invoice_id) $$,
$$ values ('F20230006', 'Product 1', 'Description 1', 1224, 2, 0.00)
, ('F20230007', 'Product 1 bis', 'Description 1 bis', 3333, 1, 0.50)
, ('F20230007', 'Product 2', 'Description 2', 2400, 3, 0.75)
, ('INV056-23', 'Product 4.3', '', 1111, 1, 0.0)
, ('INV056-23', 'Product 4.4', 'Description 4.4', 2222, 3, 0.05)
$$,
'Should have created all invoice products'
);
select bag_eq(
$$ select invoice_number, product_id, name from invoice_product left join invoice_product_product using (invoice_product_id) join invoice using (invoice_id) $$,
$$ values ('F20230006', 7, 'Product 1')
, ('F20230007', 7, 'Product 1 bis')
, ('F20230007', 8, 'Product 2')
, ('INV056-23', 11, 'Product 4.3')
, ('INV056-23', NULL, 'Product 4.4')
$$,
'Should have linked all invoice products'
);
select bag_eq(
$$ select invoice_number, name, tax_id, tax_rate from invoice_product_tax join invoice_product using (invoice_product_id) join invoice using (invoice_id) $$,
$$ values ('F20230006', 'Product 1', 4, 0.21)
, ('F20230007', 'Product 1 bis', 4, 0.21)
, ('F20230007', 'Product 1 bis', 3, -0.15)
, ('INV056-23', 'Product 4.3', 6, 0.10)
$$,
'Should have created all invoice product taxes'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,71 @@
-- Test compute_new_invoice_amount
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(14);
set search_path to camper, auth, public;
select has_function('camper', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]']);
select function_lang_is('camper', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'plpgsql');
select function_returns('camper', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'new_invoice_amount');
select isnt_definer('camper', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]']);
select volatility_is('camper', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'stable');
select function_privs_are('camper', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'guest', array []::text[]);
select function_privs_are('camper', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'employee', array ['EXECUTE']);
select function_privs_are('camper', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'admin', array ['EXECUTE']);
select function_privs_are('camper', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate tax cascade;
truncate tax_class cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
;
insert into tax_class (tax_class_id, company_id, name)
values (11, 1, 'tax')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (2, 1, 11, 'IRPF -15 %', -0.15)
, (3, 1, 11, 'IVA 4 %', 0.04)
, (4, 1, 11, 'IVA 10 %', 0.10)
, (5, 1, 11, 'IVA 21 %', 0.21)
;
select is(
compute_new_invoice_amount(1, '{}'),
'(0.00,"{}",0.00)'::new_invoice_amount
);
select is(
compute_new_invoice_amount(1, '{"(6,P,D,1.00,1,0.0,\"{2,5}\")","(6,P,D,2.00,2,0.1,{3})"}'),
'(4.60,"{{IRPF -15 %,-0.15},{IVA 4 %,0.14},{IVA 21 %,0.21}}",4.80)'::new_invoice_amount
);
select is(
compute_new_invoice_amount(1, '{"(6,P,D,2.22,3,0.0,\"{2,4,5}\")","(6,P,D,3.33,4,0.2,{4})"}'),
'(17.32,"{{IRPF -15 %,-1.00},{IVA 10 %,1.74},{IVA 21 %,1.40}}",19.46)'::new_invoice_amount
);
select is(
compute_new_invoice_amount(1, '{"(6,P,D,4.44,5,0.0,\"{4,5}\")","(6,P,D,5.55,6,0.1,\"{5,3}\")"}'),
'(52.17,"{{IVA 4 %,1.20},{IVA 10 %,2.22},{IVA 21 %,10.95}}",66.54)'::new_invoice_amount
);
select is(
compute_new_invoice_amount(1, '{"(6,P,D,7.77,8,0.0,\"{}\")"}'),
'(62.16,"{}",62.16)'::new_invoice_amount
);
select *
from finish();
rollback;

170
test/contact.sql Normal file
View File

@ -0,0 +1,170 @@
-- Test contact
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(68);
set search_path to camper, auth, public;
select has_table('contact');
select has_pk('contact' );
select table_privs_are('contact', 'guest', array []::text[]);
select table_privs_are('contact', 'employee', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('contact', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('contact', 'authenticator', array []::text[]);
select has_column('contact', 'contact_id');
select col_is_pk('contact', 'contact_id');
select col_type_is('contact', 'contact_id', 'integer');
select col_not_null('contact', 'contact_id');
select col_hasnt_default('contact', 'contact_id');
select has_column('contact', 'company_id');
select col_is_fk('contact', 'company_id');
select fk_ok('contact', 'company_id', 'company', 'company_id');
select col_type_is('contact', 'company_id', 'integer');
select col_not_null('contact', 'company_id');
select col_hasnt_default('contact', 'company_id');
select has_column('contact', 'slug');
select col_is_unique('contact', 'slug');
select col_type_is('contact', 'slug', 'uuid');
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', 'name');
select col_type_is('contact', 'name', 'text');
select col_not_null('contact', 'name');
select col_hasnt_default('contact', 'name');
select has_column('contact', 'id_document_type_id');
select col_is_fk('contact', 'id_document_type_id');
select fk_ok('contact', 'id_document_type_id', 'id_document_type', 'id_document_type_id');
select col_type_is('contact', 'id_document_type_id', 'character varying(1)');
select col_not_null('contact', 'id_document_type_id');
select col_hasnt_default('contact', 'id_document_type_id');
select has_column('contact', 'id_document_number');
select col_type_is('contact', 'id_document_number', 'text');
select col_not_null('contact', 'id_document_number');
select col_hasnt_default('contact', 'id_document_number');
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_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', 'created_at');
select col_type_is('contact', 'created_at', 'timestamp with time zone');
select col_not_null('contact', 'created_at');
select col_has_default('contact', 'created_at');
select col_default_is('contact', 'created_at', 'CURRENT_TIMESTAMP');
set client_min_messages to warning;
truncate contact cascade;
truncate company_host cascade;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 7, 'FR', 'USD', 'ca')
;
insert into company_user (company_id, user_id, role)
values (2, 1, 'admin')
, (4, 5, 'admin')
;
insert into company_host (company_id, host)
values (2, 'co2')
, (4, 'co4')
;
insert into contact (company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (2, 'Contact 1', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (4, 'Contact 2', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
;
prepare contact_data as
select company_id, name
from contact
order by company_id, name;
set role employee;
select is_empty('contact_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
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', 'co4');
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', 'co2');
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 (company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (2, ' ', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
$$,
'23514', 'new row for relation "contact" violates check constraint "name_not_empty"',
'Should not allow contacts with blank trade name'
);
select *
from finish();
rollback;

115
test/contact_email.sql Normal file
View File

@ -0,0 +1,115 @@
-- 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 camper, 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', 'employee', 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_host cascade;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 7, 'FR', 'USD', 'ca')
;
insert into company_user (company_id, user_id, role)
values (2, 1, 'admin')
, (4, 5, 'admin')
;
insert into company_host (company_id, host)
values (2, 'co2')
, (4, 'co4')
;
insert into contact (contact_id, company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (6, 2, 'C1', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (8, 4, 'C2', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (9, 4, 'C3', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
;
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 employee;
select is_empty('contact_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
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', 'co4');
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', 'co4');
select throws_ok(
'contact_data',
'42501', 'permission denied for table contact',
'Should not allow select to guest users'
);
reset role;
select *
from finish();
rollback;

114
test/contact_phone.sql Normal file
View File

@ -0,0 +1,114 @@
-- 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 camper, 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', 'employee', 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_host cascade;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 7, 'FR', 'USD', 'ca')
;
insert into company_user (company_id, user_id, role)
values (2, 1, 'admin')
, (4, 5, 'admin')
;
insert into company_host (company_id, host)
values (2, 'co2')
, (4, 'co4')
;
insert into contact (contact_id, company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (6, 2, 'C1', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (8, 4, 'C2', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (9, 4, 'C3', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
;
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 employee;
select is_empty('contact_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
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', 'co4');
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', 'co4');
select throws_ok(
'contact_data',
'42501', 'permission denied for table contact',
'Should not allow select to guest users'
);
reset role;
select *
from finish();
rollback;

34
test/discount_rate.sql Normal file
View File

@ -0,0 +1,34 @@
-- Test discount_rate
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(7);
set search_path to camper, public;
select has_domain('discount_rate');
select domain_type_is('discount_rate', 'numeric');
select lives_ok($$ select 1::discount_rate $$, 'Should be able to cast valid 100 % to discount rate');
select lives_ok($$ select 0.21::discount_rate $$, 'Should be able to cast valid positive decimals to discount rate');
select lives_ok($$ select 0::discount_rate $$, 'Should be able to cast valid zero to discount rate');
select throws_ok(
$$ SELECT (-0.01)::discount_rate $$,
23514, null,
'Should reject negative discount rate'
);
select throws_ok(
$$ SELECT 1.01::discount_rate $$,
23514, null,
'Should not allow past the 100 % discount'
);
select *
from finish();
rollback;

95
test/edit_contact.sql Normal file
View File

@ -0,0 +1,95 @@
-- Test edit_contact
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(15);
set search_path to auth, camper, public;
select has_function('camper', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code']);
select function_lang_is('camper', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'plpgsql');
select function_returns('camper', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'uuid');
select isnt_definer('camper', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code']);
select volatility_is('camper', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'volatile');
select function_privs_are('camper', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'guest', array []::text[]);
select function_privs_are('camper', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'employee', array ['EXECUTE']);
select function_privs_are('camper', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'admin', array ['EXECUTE']);
select function_privs_are('camper', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'text', 'country_code'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate contact_email cascade;
truncate contact_phone cascade;
truncate contact cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
;
insert into contact (contact_id, company_id, slug, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (12, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 1', 'C', 'XX555', '', '', '', '', 'FR')
, (13, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2', 'C', 'XX666', '', '', '', '', 'DE')
, (14, 1, '12fd031b-8f4d-4ac1-9dde-7df336dc6d52', 'Contact 3', 'C', 'XX777', '', '', '', '', 'JP')
;
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')
;
select lives_ok(
$$ select edit_contact('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 2.1', 'D', '40404040D', '999-999-999', 'c1@c1', 'Fake St., 123', 'City 2.1', 'Province 2.1', '19486', 'ES') $$,
'Should be able to edit the first contact'
);
select lives_ok(
$$ select edit_contact('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2.2', 'C', 'XXX666', '', '', '', '', '', '', 'GB') $$,
'Should be able to edit the second contact'
);
select lives_ok(
$$ select edit_contact('12fd031b-8f4d-4ac1-9dde-7df336dc6d52', 'Contact 2.3', 'D', '41414141L', '111-111-111', 'd2@d2', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17417', 'ES') $$,
'Should be able to edit the third contact'
);
select bag_eq(
$$ select company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code from contact $$,
$$ values (1, 'Contact 2.1', 'D', '40404040D', 'Fake St., 123', 'City 2.1', 'Province 2.1', '19486', 'ES')
, (1, 'Contact 2.2', 'C', 'XXX666', '', '', '', '', 'GB')
, (1, 'Contact 2.3', 'D', '41414141L', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17417', 'ES')
$$,
'Should have updated all contacts'
);
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 *
from finish();
rollback;

149
test/edit_invoice.sql Normal file
View File

@ -0,0 +1,149 @@
-- Test edit_invoice
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(15);
set search_path to auth, camper, public;
select has_function('camper', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'edited_invoice_product[]']);
select function_lang_is('camper', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'edited_invoice_product[]'], 'plpgsql');
select function_returns('camper', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'edited_invoice_product[]'], 'uuid');
select isnt_definer('camper', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'edited_invoice_product[]']);
select volatility_is('camper', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'edited_invoice_product[]'], 'volatile');
select function_privs_are('camper', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'edited_invoice_product[]'], 'guest', array []::text[]);
select function_privs_are('camper', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'edited_invoice_product[]'], 'employee', array ['EXECUTE']);
select function_privs_are('camper', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'edited_invoice_product[]'], 'admin', array ['EXECUTE']);
select function_privs_are('camper', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'edited_invoice_product[]'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate invoice_product_tax cascade;
truncate invoice_product cascade;
truncate invoice cascade;
truncate contact cascade;
truncate product cascade;
truncate tax cascade;
truncate tax_class cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
, (112, 1, 'bank', 'send money to my bank account')
;
insert into tax_class (tax_class_id, company_id, name)
values (11, 1, 'tax')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (3, 1, 11, 'IRPF -15 %', -0.15)
, (4, 1, 11, 'IVA 21 %', 0.21)
;
insert into product (product_id, company_id, name, price)
values ( 7, 1, 'Product 1.1', 1212)
, ( 8, 1, 'Product 2.2', 2424)
, ( 9, 1, 'Product 3.3', 3636)
;
insert into contact (contact_id, company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (12, 1, 'Contact 2.1', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (13, 1, 'Contact 2.2', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
;
insert into invoice (invoice_id, company_id, slug, invoice_number, invoice_date, contact_id, payment_method_id, currency_code)
values (15, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'INV1', '2023-03-10', 12, 111, 'EUR')
, (16, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'INV2', '2023-03-09', 13, 111, 'EUR')
;
insert into invoice_product (invoice_product_id, invoice_id, name, price)
values (19, 15, 'P1.0', 1100)
, (20, 15, 'P2.0', 2200)
, (21, 16, 'P1.1', 1111)
, (22, 16, 'P2.1', 2211)
;
insert into invoice_product_product (invoice_product_id, product_id)
values (19, 7)
, (20, 8)
, (21, 7)
, (22, 8)
;
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
values (19, 4, 0.21)
, (20, 4, 0.21)
, (21, 3, -0.07)
, (21, 4, 0.21)
, (22, 3, -0.15)
;
select lives_ok(
$$ select edit_invoice('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'paid', 13, 'Notes 1', 112, '{"(20,,p1.0,D1,11.01,2,0.50,{4})","(,,p1.3,D3,33.33,3,0.05,{3})"}') $$,
'Should be able to edit the first invoice'
);
select lives_ok(
$$ select edit_invoice('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'sent', 12, 'Notes 2', 111, '{"(21,7,P1.1,,11.11,1,0.0,{3})","(22,8,p2.1,D2,24.00,3,0.75,\"{3,4}\")","(,9,p3.3,,36.36,2,0.05,{4})"}') $$,
'Should be able to edit the second invoice'
);
select bag_eq(
$$ select invoice_number, invoice_date, contact_id, invoice_status, notes, payment_method_id from invoice $$,
$$ values ('INV1', '2023-03-10'::date, 13, 'paid', 'Notes 1', 112)
, ('INV2', '2023-03-09'::date, 12, 'sent', 'Notes 2', 111)
$$,
'Should have updated all invoices'
);
select bag_eq(
$$ select invoice_number, name, description, price, quantity, discount_rate from invoice_product join invoice using (invoice_id) $$,
$$ values ('INV1', 'p1.0', 'D1', 1101, 2, 0.50)
, ('INV1', 'p1.3', 'D3', 3333, 3, 0.05)
, ('INV2', 'P1.1', '', 1111, 1, 0.00)
, ('INV2', 'p2.1', 'D2', 2400, 3, 0.75)
, ('INV2', 'p3.3', '', 3636, 2, 0.05)
$$,
'Should have updated all existing invoice products, added new ones, and removed the ones not give to the function'
);
select bag_eq(
$$ select invoice_number, product_id, name from invoice_product left join invoice_product_product using (invoice_product_id) join invoice using (invoice_id) $$,
$$ values ('INV1', NULL, 'p1.0')
, ('INV1', NULL, 'p1.3')
, ('INV2', 7, 'P1.1')
, ('INV2', 8, 'p2.1')
, ('INV2', 9, 'p3.3')
$$,
'Should have updated all existing invoice products id, added new ones, and removed the ones not give to the function'
);
select bag_eq(
$$ select invoice_number, name, tax_id, tax_rate from invoice_product_tax join invoice_product using (invoice_product_id) join invoice using (invoice_id) $$,
$$ values ('INV1', 'p1.0', 4, 0.21)
, ('INV1', 'p1.3', 3, -0.15)
, ('INV2', 'P1.1', 3, -0.15)
, ('INV2', 'p2.1', 3, -0.15)
, ('INV2', 'p2.1', 4, 0.21)
, ('INV2', 'p3.3', 4, 0.21)
$$,
'Should have updated all invoice product taxes, added new ones, and removed the ones not given to the function'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,27 @@
-- Test edited_invoice_product
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(10);
set search_path to camper, public;
select has_composite('camper', 'edited_invoice_product', 'Composite type camper.edited_invoice_product should exist');
select columns_are('camper', 'edited_invoice_product', array['invoice_product_id', 'product_id', 'name', 'description', 'price', 'quantity', 'discount_rate', 'tax']);
select col_type_is('camper'::name, 'edited_invoice_product'::name, 'invoice_product_id'::name, 'integer');
select col_type_is('camper'::name, 'edited_invoice_product'::name, 'product_id'::name, 'integer');
select col_type_is('camper'::name, 'edited_invoice_product'::name, 'name'::name, 'text');
select col_type_is('camper'::name, 'edited_invoice_product'::name, 'description'::name, 'text');
select col_type_is('camper'::name, 'edited_invoice_product'::name, 'price'::name, 'text');
select col_type_is('camper'::name, 'edited_invoice_product'::name, 'quantity'::name, 'integer');
select col_type_is('camper'::name, 'edited_invoice_product'::name, 'discount_rate'::name, 'discount_rate');
select col_type_is('camper'::name, 'edited_invoice_product'::name, 'tax'::name, 'integer[]');
select *
from finish();
rollback;

188
test/invoice.sql Normal file
View File

@ -0,0 +1,188 @@
-- Test invoice
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(72);
set search_path to camper, auth, public;
select has_table('invoice');
select has_pk('invoice' );
select table_privs_are('invoice', 'guest', array []::text[]);
select table_privs_are('invoice', 'employee', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('invoice', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('invoice', 'authenticator', array []::text[]);
select has_column('invoice', 'invoice_id');
select col_is_pk('invoice', 'invoice_id');
select col_type_is('invoice', 'invoice_id', 'integer');
select col_not_null('invoice', 'invoice_id');
select col_hasnt_default('invoice', 'invoice_id');
select has_column('invoice', 'company_id');
select col_is_fk('invoice', 'company_id');
select fk_ok('invoice', 'company_id', 'company', 'company_id');
select col_type_is('invoice', 'company_id', 'integer');
select col_not_null('invoice', 'company_id');
select col_hasnt_default('invoice', 'company_id');
select has_column('invoice', 'slug');
select col_is_unique('invoice', 'slug');
select col_type_is('invoice', 'slug', 'uuid');
select col_not_null('invoice', 'slug');
select col_has_default('invoice', 'slug');
select col_default_is('invoice', 'slug', 'gen_random_uuid()');
select has_column('invoice', 'invoice_number');
select col_type_is('invoice', 'invoice_number', 'text');
select col_not_null('invoice', 'invoice_number');
select col_hasnt_default('invoice', 'invoice_number');
select has_column('invoice', 'invoice_date');
select col_type_is('invoice', 'invoice_date', 'date');
select col_not_null('invoice', 'invoice_date');
select col_has_default('invoice', 'invoice_date');
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 col_type_is('invoice', 'contact_id', 'integer');
select col_not_null('invoice', 'contact_id');
select col_hasnt_default('invoice', 'contact_id');
select has_column('invoice', 'invoice_status');
select col_is_fk('invoice', 'invoice_status');
select fk_ok('invoice', 'invoice_status', 'invoice_status', 'invoice_status');
select col_type_is('invoice', 'invoice_status', 'text');
select col_not_null('invoice', 'invoice_status');
select col_has_default('invoice', 'invoice_status');
select col_default_is('invoice', 'invoice_status', 'created');
select has_column('invoice', 'notes');
select col_type_is('invoice', 'notes', 'text');
select col_not_null('invoice', 'notes');
select col_has_default('invoice', 'notes');
select col_default_is('invoice', 'notes', '');
select has_column('invoice', 'payment_method_id');
select col_is_fk('invoice', 'payment_method_id');
select fk_ok('invoice', 'payment_method_id', 'payment_method', 'payment_method_id');
select col_type_is('invoice', 'payment_method_id', 'integer');
select col_not_null('invoice', 'payment_method_id');
select col_hasnt_default('invoice', 'payment_method_id');
select has_column('invoice', 'currency_code');
select col_is_fk('invoice', 'currency_code');
select fk_ok('invoice', 'currency_code', 'currency', 'currency_code');
select col_type_is('invoice', 'currency_code', 'text');
select col_not_null('invoice', 'currency_code');
select col_hasnt_default('invoice', 'currency_code');
select has_column('invoice', 'created_at');
select col_type_is('invoice', 'created_at', 'timestamp with time zone');
select col_not_null('invoice', 'created_at');
select col_has_default('invoice', 'created_at');
select col_default_is('invoice', 'created_at', 'CURRENT_TIMESTAMP');
set client_min_messages to warning;
truncate invoice cascade;
truncate contact cascade;
truncate payment_method cascade;
truncate company_host cascade;
truncate company_user cascade;
truncate company cascade;
truncate payment_method cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 7, 'FR', 'USD', 'ca')
;
insert into company_user (company_id, user_id, role)
values (2, 1, 'admin')
, (4, 5, 'admin')
;
insert into company_host (company_id, host)
values (2, 'co2')
, (4, 'co4')
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
insert into contact (contact_id, company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (6, 2, 'Contact 1', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (8, 4, 'Contact 2', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
;
insert into invoice (company_id, invoice_number, contact_id, currency_code, payment_method_id)
values (2, 'INV020001', 6, 'EUR', 222)
, (4, 'INV040001', 8, 'EUR', 444)
;
prepare invoice_data as
select company_id, invoice_number
from invoice
order by company_id, invoice_number;
set role employee;
select is_empty('invoice_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
select bag_eq(
'invoice_data',
$$ values (2, 'INV020001')
$$,
'Should only list invoices of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog', 'co4');
select bag_eq(
'invoice_data',
$$ values (4, 'INV040001')
$$,
'Should only list invoices of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie', 'co4');
select throws_ok(
'invoice_data',
'42501', 'permission denied for table invoice',
'Should not allow select to guest users'
);
reset role;
select throws_ok( $$
insert into invoice (company_id, invoice_number, contact_id, currency_code, payment_method_id)
values (2, ' ', 6, 'EUR', 222)
$$,
'23514', 'new row for relation "invoice" violates check constraint "invoice_number_not_empty"',
'Should not allow invoice with blank number'
);
select *
from finish();
rollback;

107
test/invoice_amount.sql Normal file
View File

@ -0,0 +1,107 @@
-- Test invoice_amount
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(12);
set search_path to camper, auth, public;
select has_view('invoice_amount');
select table_privs_are('invoice_amount', 'guest', array[]::text[]);
select table_privs_are('invoice_amount', 'employee', array['SELECT']);
select table_privs_are('invoice_amount', 'admin', array['SELECT']);
select table_privs_are('invoice_amount', 'authenticator', array[]::text[]);
select has_column('invoice_amount', 'invoice_id');
select col_type_is('invoice_amount', 'invoice_id', 'integer');
select has_column('invoice_amount', 'subtotal');
select col_type_is('invoice_amount', 'subtotal', 'integer');
select has_column('invoice_amount', 'total');
select col_type_is('invoice_amount', 'total', 'integer');
set client_min_messages to warning;
truncate invoice_product_tax cascade;
truncate invoice_product cascade;
truncate invoice cascade;
truncate contact cascade;
truncate tax cascade;
truncate tax_class cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
;
insert into tax_class (tax_class_id, company_id, name)
values (11, 1, 'tax')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (2, 1, 11, 'IRPF -15 %', -0.15)
, (3, 1, 11, 'IVA 4 %', 0.04)
, (4, 1, 11, 'IVA 10 %', 0.10)
, (5, 1, 11, 'IVA 21 %', 0.21)
;
insert into contact (contact_id, company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (7, 1, 'Contact', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code, payment_method_id)
values ( 8, 1, 'I1', current_date, 7, 'EUR', 111)
, ( 9, 1, 'I2', current_date, 7, 'EUR', 111)
, (10, 1, 'I3', current_date, 7, 'EUR', 111)
, (11, 1, 'I4', current_date, 7, 'EUR', 111)
;
insert into invoice_product (invoice_product_id, invoice_id, name, price, quantity, discount_rate)
values (12, 8, 'P', 100, 1, 0.0)
, (13, 8, 'P', 200, 2, 0.1)
, (14, 9, 'P', 222, 3, 0.0)
, (15, 9, 'P', 333, 4, 0.2)
, (16, 10, 'P', 444, 5, 0.0)
, (17, 10, 'P', 555, 6, 0.1)
, (18, 11, 'P', 777, 8, 0.0)
;
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
values (12, 2, -0.15)
, (12, 5, 0.21)
, (13, 3, 0.04)
, (14, 4, 0.10)
, (14, 5, 0.21)
, (14, 2, -0.07)
, (15, 4, 0.10)
, (16, 4, 0.10)
, (16, 5, 0.21)
, (17, 5, 0.21)
, (17, 3, 0.04)
;
select bag_eq(
$$ select invoice_id, subtotal, total from invoice_amount $$,
$$ values ( 8, 460, 480)
, ( 9, 1732, 1999)
, (10, 5217, 6654)
, (11, 6216, 6216)
$$,
'Should compute the amount for all taxes in the invoiced products.'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,141 @@
-- Test invoice_number_counter
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(28);
set search_path to camper, auth, public;
select has_table('invoice_number_counter');
select has_pk('invoice_number_counter' );
select col_is_pk('invoice_number_counter', array['company_id', 'year']);
select table_privs_are('invoice_number_counter', 'guest', array []::text[]);
select table_privs_are('invoice_number_counter', 'employee', array ['SELECT', 'INSERT', 'UPDATE']);
select table_privs_are('invoice_number_counter', 'admin', array ['SELECT', 'INSERT', 'UPDATE']);
select table_privs_are('invoice_number_counter', 'authenticator', array []::text[]);
select has_column('invoice_number_counter', 'company_id');
select col_is_fk('invoice_number_counter', 'company_id');
select fk_ok('invoice_number_counter', 'company_id', 'company', 'company_id');
select col_type_is('invoice_number_counter', 'company_id', 'integer');
select col_not_null('invoice_number_counter', 'company_id');
select col_hasnt_default('invoice_number_counter', 'company_id');
select has_column('invoice_number_counter', 'year');
select col_type_is('invoice_number_counter', 'year', 'integer');
select col_not_null('invoice_number_counter', 'year');
select col_hasnt_default('invoice_number_counter', 'year');
select has_column('invoice_number_counter', 'currval');
select col_type_is('invoice_number_counter', 'currval', 'integer');
select col_not_null('invoice_number_counter', 'currval');
select col_hasnt_default('invoice_number_counter', 'currval');
set client_min_messages to warning;
truncate invoice_number_counter cascade;
truncate company_host cascade;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 7, 'FR', 'USD', 'ca')
;
insert into company_user (company_id, user_id, role)
values (2, 1, 'admin')
, (4, 5, 'admin')
;
insert into company_host (company_id, host)
values (2, 'co2')
, (4, 'co4')
;
insert into invoice_number_counter (company_id, year, currval)
values (2, 2010, 6)
, (2, 2011, 8)
, (4, 2010, 8)
, (4, 2012, 10)
;
prepare counter_data as
select company_id, year, currval
from invoice_number_counter
;
set role employee;
select is_empty('counter_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
select bag_eq(
'counter_data',
$$ values (2, 2010, 6)
, (2, 2011, 8)
$$,
'Should only list invoices of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog', 'co4');
select bag_eq(
'counter_data',
$$ values (4, 2010, 8)
, (4, 2012, 10)
$$,
'Should only list invoices of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie', 'co4');
select throws_ok(
'counter_data',
'42501', 'permission denied for table invoice_number_counter',
'Should not allow select to guest users'
);
reset role;
select lives_ok( $$
insert into invoice_number_counter (company_id, year, currval)
values (2, 2009, 0)
$$,
'Should allow starting a counter from zero'
);
select throws_ok( $$
insert into invoice_number_counter (company_id, year, currval)
values (2, 2008, -1)
$$,
'23514', 'new row for relation "invoice_number_counter" violates check constraint "counter_zero_or_positive"',
'Should not allow starting a counter from a negative value'
);
select throws_ok( $$
insert into invoice_number_counter (company_id, year, currval)
values (2, -2008, 1)
$$,
'23514', 'new row for relation "invoice_number_counter" violates check constraint "year_always_positive"',
'Should not allow counters for invoices issued before Jesus Christ was born'
);
select *
from finish();
rollback;

162
test/invoice_product.sql Normal file
View File

@ -0,0 +1,162 @@
-- Test invoice_product
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(45);
set search_path to camper, auth, public;
select has_table('invoice_product');
select has_pk('invoice_product' );
select table_privs_are('invoice_product', 'guest', array []::text[]);
select table_privs_are('invoice_product', 'employee', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('invoice_product', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('invoice_product', 'authenticator', array []::text[]);
select has_column('invoice_product', 'invoice_product_id');
select col_is_pk('invoice_product', 'invoice_product_id');
select col_type_is('invoice_product', 'invoice_product_id', 'integer');
select col_not_null('invoice_product', 'invoice_product_id');
select col_hasnt_default('invoice_product', 'invoice_product_id');
select has_column('invoice_product', 'invoice_id');
select col_is_fk('invoice_product', 'invoice_id');
select fk_ok('invoice_product', 'invoice_id', 'invoice', 'invoice_id');
select col_type_is('invoice_product', 'invoice_id', 'integer');
select col_not_null('invoice_product', 'invoice_id');
select col_hasnt_default('invoice_product', 'invoice_id');
select has_column('invoice_product', 'name');
select col_type_is('invoice_product', 'name', 'text');
select col_not_null('invoice_product', 'name');
select col_hasnt_default('invoice_product', 'name');
select has_column('invoice_product', 'description');
select col_type_is('invoice_product', 'description', 'text');
select col_not_null('invoice_product', 'description');
select col_has_default('invoice_product', 'description');
select col_default_is('invoice_product', 'description', '');
select has_column('invoice_product', 'price');
select col_type_is('invoice_product', 'price', 'integer');
select col_not_null('invoice_product', 'price');
select col_hasnt_default('invoice_product', 'price');
select has_column('invoice_product', 'quantity');
select col_type_is('invoice_product', 'quantity', 'integer');
select col_not_null('invoice_product', 'quantity');
select col_has_default('invoice_product', 'quantity');
select col_default_is('invoice_product', 'quantity', 1);
select has_column('invoice_product', 'discount_rate');
select col_type_is('invoice_product', 'discount_rate', 'discount_rate');
select col_not_null('invoice_product', 'discount_rate');
select col_has_default('invoice_product', 'discount_rate');
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 cascade;
truncate payment_method cascade;
truncate company_host cascade;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, tourist_tax_max_days, country_code, currency_code, default_lang_tag)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 7, 'ES', 'EUR', 'ca')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 7, 'FR', 'USD', 'ca')
;
insert into company_user (company_id, user_id, role)
values (2, 1, 'admin')
, (4, 5, 'admin')
;
insert into company_host (company_id, host)
values (2, 'co2')
, (4, 'co4')
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
insert into contact (contact_id, company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
values (6, 2, 'Contact 1', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
, (8, 4, 'Contact 2', 'D', '41440443Q', 'Fake St', 'City', 'Province', '17600', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, contact_id, currency_code, payment_method_id)
values (10, 2, 'INV020001', 6, 'EUR', 222)
, (12, 4, 'INV040001', 8, 'EUR', 444)
;
insert into invoice_product (invoice_id, name, description, price, quantity)
values (10, 'product 1', 'description 1', 1212, 1)
, (12, 'product 2', 'description 2', 2424, 2)
;
prepare invoice_product_data as
select invoice_id, name, price, quantity
from invoice_product
order by invoice_id;
set role employee;
select is_empty('invoice_product_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
select bag_eq(
'invoice_product_data',
$$ values (10, 'product 1', 1212, 1)
$$,
'Should only list products of invoices of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog', 'co4');
select bag_eq(
'invoice_product_data',
$$ values (12, 'product 2', 2424, 2)
$$,
'Should only list products of invoices of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie', 'co4');
select throws_ok(
'invoice_product_data',
'42501', 'permission denied for table invoice_product',
'Should not allow select to guest users'
);
reset role;
select throws_ok( $$
insert into invoice_product (invoice_id, name, description, price, quantity)
values (10, ' ', '', 1212, 1)
$$,
'23514', 'new row for relation "invoice_product" violates check constraint "name_not_empty"',
'Should not allow invoice products with blank name'
);
select *
from finish();
rollback;

Some files were not shown because too many files have changed in this diff Show More