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-github-jackc-pgx-v4-dev,
golang-github-leonelquinteros-gotext-dev,
golang-github-rainycape-unidecode-dev,
golang-golang-x-text-dev,
postgresql-all (>= 217~),
sqitch,

View File

@ -24,6 +24,21 @@ values (52, 42, 'employee')
, (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_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 (
github.com/jackc/pgconn v1.11.0
github.com/jackc/pgio v1.0.0
github.com/jackc/pgtype v1.10.0
github.com/jackc/pgx/v4 v4.15.0
github.com/leonelquinteros/gotext v1.5.0
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8
golang.org/x/text v0.7.0
)
require (
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/pgproto3/v2 v2.3.2 // 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
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/campsite"
"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/home"
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/location"
"dev.tandem.ws/tandem/camper/pkg/media"
@ -32,7 +34,9 @@ type adminHandler struct {
booking *booking.AdminHandler
campsite *campsite.AdminHandler
company *company.AdminHandler
customer *customer.AdminHandler
home *home.AdminHandler
invoice *invoice.AdminHandler
legal *legal.AdminHandler
location *location.AdminHandler
media *media.AdminHandler
@ -49,7 +53,9 @@ func newAdminHandler(mediaDir string) *adminHandler {
booking: booking.NewAdminHandler(),
campsite: campsite.NewAdminHandler(),
company: company.NewAdminHandler(),
customer: customer.NewAdminHandler(),
home: home.NewAdminHandler(),
invoice: invoice.NewAdminHandler(),
legal: legal.NewAdminHandler(),
location: location.NewAdminHandler(),
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)
case "company":
h.company.Handler(user, company, conn).ServeHTTP(w, r)
case "customers":
h.customer.Handler(user, company, conn).ServeHTTP(w, r)
case "home":
h.home.Handler(user, company, conn).ServeHTTP(w, r)
case "legal":
h.legal.Handler(user, company, conn).ServeHTTP(w, r)
case "invoices":
h.invoice.Handler(user, company, conn).ServeHTTP(w, r)
case "location":
h.location.Handler(user, company, conn).ServeHTTP(w, r)
case "media":

View File

@ -7,6 +7,7 @@ package booking
import (
"context"
"dev.tandem.ws/tandem/camper/pkg/ods"
"errors"
"net/http"
"strconv"
@ -197,16 +198,16 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user
"Holder Name",
"Status",
}
ods, err := writeTableOds(page, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error {
if err := writeCellString(sb, entry.Reference); err != nil {
table, err := ods.WriteTable(page, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error {
if err := ods.WriteCellString(sb, entry.Reference); err != nil {
return err
}
writeCellDate(sb, entry.ArrivalDate)
writeCellDate(sb, entry.DepartureDate)
if err := writeCellString(sb, entry.HolderName); err != nil {
ods.WriteCellDate(sb, entry.ArrivalDate)
ods.WriteCellDate(sb, entry.DepartureDate)
if err := ods.WriteCellString(sb, entry.HolderName); err != nil {
return err
}
if err := writeCellString(sb, entry.StatusLabel); err != nil {
if err := ods.WriteCellString(sb, entry.StatusLabel); err != nil {
return err
}
return nil
@ -214,7 +215,7 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user
if err != nil {
panic(err)
}
mustWriteOdsResponse(w, ods, user.Locale.Pgettext("bookings.ods", "filename"))
ods.MustWriteResponse(w, table, user.Locale.Pgettext("bookings.ods", "filename"))
default:
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 {
documentTypes := mustGetDocumentTypeOptions(ctx, conn, l)
documentTypes := form.MustGetDocumentTypeOptions(ctx, conn, l)
sexes := mustGetSexOptions(ctx, conn, l)
countries := mustGetCountryOptions(ctx, conn, l)
countries := form.MustGetCountryOptions(ctx, conn, l)
rows, err := conn.Query(ctx, `
select array[id_document_type_id]
@ -118,10 +118,6 @@ func (f *checkInForm) FillFromDatabase(ctx context.Context, conn *database.Conn,
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 {
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
}
documentTypes := mustGetDocumentTypeOptions(r.Context(), conn, user.Locale)
documentTypes := form.MustGetDocumentTypeOptions(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)
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) {
documentTypes := mustGetDocumentTypeOptions(ctx, conn, l)
documentTypes := form.MustGetDocumentTypeOptions(ctx, conn, l)
sexes := mustGetSexOptions(ctx, conn, l)
countries := mustGetCountryOptions(ctx, conn, l)
countries := form.MustGetCountryOptions(ctx, conn, l)
var country []string
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{
Name: "country",
Options: mustGetCountryOptions(ctx, conn, l),
Options: form.MustGetCountryOptions(ctx, conn, l),
},
Email: &form.Input{
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) {
f.FullName.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))
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
}
discountRateOID, err := registerType(ctx, conn, &pgtype.Numeric{}, "discount_rate")
if err != nil {
return err
}
redsysRequestType, err := pgtype.NewCompositeType(
RedsysRequestTypeName,
[]pgtype.CompositeTypeField{
@ -150,6 +155,47 @@ func registerConnectionTypes(ctx context.Context, conn *pgx.Conn) error {
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
}

View File

@ -8,6 +8,7 @@ package form
import (
"context"
"database/sql/driver"
"dev.tandem.ws/tandem/camper/pkg/locale"
"net/http"
"strconv"
@ -113,3 +114,11 @@ func MustGetOptions(ctx context.Context, conn *database.Conn, sql string, args .
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

@ -14,6 +14,7 @@ const (
HxLocation = "HX-Location"
HxRedirect = "HX-Redirect"
HxRequest = "HX-Request"
HxTriggerAfterSettle = "HX-Trigger-After-Settle"
)
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 {
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 (
"archive/zip"
"bytes"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/template"
"encoding/xml"
"fmt"
"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
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">
`)
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
}
}
@ -148,7 +150,7 @@ func writeOdsFile(ods *zip.Writer, name string, content string, method uint16) e
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>`)
if err := xml.EscapeText(sb, []byte(s)); err != nil {
return err
@ -157,11 +159,15 @@ func writeCellString(sb *strings.Builder, s string) error {
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")))
}
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-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
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 {
return time.Format(database.ISODateFormat)
},
"formatPercent": func(value int) string {
return fmt.Sprintf("%d %%", value)
},
"today": func() string {
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 {
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 {
switch v := v.(type) {
case int:
return v
case bool:
if v {
return 1
} else {
return 0
}
case time.Weekday:
return int(v)
case time.Month:
return int(v)
default:
panic(fmt.Errorf("Could not convert to integer"))
panic(fmt.Errorf("could not convert to integer"))
}
},
"hexToDec": func(s string) int {
num, _ := strconv.ParseInt(s, 16, 0)
return int(num)
},
"slugify": Slugify,
})
templates = append(templates, "form.gohtml")
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 ""
"Project-Id-Version: camper\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"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\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/public/booking/fields.gohtml:146
#: web/templates/admin/payment/details.gohtml:140
#: web/templates/admin/customer/form.gohtml:28
#: web/templates/admin/booking/fields.gohtml:188
msgctxt "title"
msgid "Customer Details"
@ -247,6 +248,7 @@ msgstr "Detalls del client"
#: web/templates/mail/payment/details.gotxt:41
#: web/templates/public/booking/fields.gohtml:149
#: web/templates/admin/payment/details.gohtml:143
#: web/templates/admin/customer/form.gohtml:31
#: web/templates/admin/booking/fields.gohtml:191
msgctxt "input"
msgid "Full name"
@ -255,6 +257,7 @@ msgstr "Nom i cognoms"
#: web/templates/mail/payment/details.gotxt:42
#: web/templates/public/booking/fields.gohtml:158
#: web/templates/admin/payment/details.gohtml:147
#: web/templates/admin/customer/form.gohtml:69
#: web/templates/admin/taxDetails.gohtml:69
msgctxt "input"
msgid "Address"
@ -263,6 +266,7 @@ msgstr "Adreça"
#: web/templates/mail/payment/details.gotxt:43
#: web/templates/public/booking/fields.gohtml:167
#: web/templates/admin/payment/details.gohtml:151
#: web/templates/admin/customer/form.gohtml:105
#: web/templates/admin/taxDetails.gohtml:93
msgctxt "input"
msgid "Postcode"
@ -278,6 +282,7 @@ msgstr "Població"
#: web/templates/mail/payment/details.gotxt:45
#: web/templates/public/booking/fields.gohtml:187
#: web/templates/admin/payment/details.gohtml:159
#: web/templates/admin/customer/form.gohtml:117
#: web/templates/admin/taxDetails.gohtml:101
msgctxt "input"
msgid "Country"
@ -411,11 +416,16 @@ msgid "Order Number"
msgstr "Número de comanda"
#: web/templates/public/payment/details.gohtml:8
#: web/templates/admin/invoice/index.gohtml:103
#: web/templates/admin/invoice/view.gohtml:26
msgctxt "title"
msgid "Date"
msgstr "Data"
#: 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"
msgid "Total"
msgstr "Total"
@ -579,6 +589,11 @@ msgctxt "input"
msgid "Year"
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/booking/fields.gohtml:278
msgctxt "action"
@ -929,7 +944,7 @@ msgstr "Menú"
#: web/templates/admin/campsite/type/option/form.gohtml:16
#: web/templates/admin/campsite/type/option/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
msgctxt "title"
msgid "Campsites"
@ -1004,13 +1019,15 @@ msgid "Campground map"
msgstr "Mapa del càmping"
#: web/templates/public/booking/fields.gohtml:176
#: web/templates/admin/customer/form.gohtml:81
msgctxt "input"
msgid "Town or village"
msgstr "Població"
#: 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/guest.gohtml:109
#: web/templates/admin/booking/guest.gohtml:111
msgid "Choose a country"
msgstr "Esculli un país"
@ -1164,6 +1181,7 @@ msgstr "Àlies"
#: web/templates/admin/campsite/type/form.gohtml:51
#: web/templates/admin/campsite/type/option/form.gohtml:41
#: 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/profile.gohtml:29
#: 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/option/form.gohtml:98
#: 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/surroundings/form.gohtml:69
#: 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/option/form.gohtml:100
#: web/templates/admin/season/form.gohtml:75
#: web/templates/admin/customer/form.gohtml:155
#: web/templates/admin/services/form.gohtml:83
#: web/templates/admin/surroundings/form.gohtml:71
#: 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/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/surroundings/index.gohtml:83
#: web/templates/admin/amenity/feature/index.gohtml:30
@ -1725,6 +1747,7 @@ msgid "Per night"
msgstr "Per nit"
#: web/templates/admin/campsite/type/option/form.gohtml:84
#: web/templates/admin/invoice/product-form.gohtml:29
msgctxt "input"
msgid "Price"
msgstr "Preu"
@ -1824,12 +1847,316 @@ msgctxt "action"
msgid "Cancel"
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:13 web/templates/admin/layout.gohtml:89
msgctxt "title"
msgid "Dashboard"
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
msgctxt "title"
msgid "Login"
@ -1928,12 +2255,6 @@ msgctxt "title"
msgid "Users"
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
msgctxt "header"
msgid "IP Address"
@ -1985,11 +2306,6 @@ msgctxt "input"
msgid "Trade Name"
msgstr "Nom comercial"
#: web/templates/admin/taxDetails.gohtml:85
msgctxt "input"
msgid "Province"
msgstr "Província"
#: web/templates/admin/taxDetails.gohtml:111
msgctxt "input"
msgid "Currency"
@ -2214,11 +2530,11 @@ msgctxt "title"
msgid "Bookings"
msgstr "Reserves"
#: web/templates/admin/layout.gohtml:101
#: web/templates/admin/layout.gohtml:107
msgid "Breadcrumb"
msgstr "Fil dAriadna"
#: web/templates/admin/layout.gohtml:113
#: web/templates/admin/layout.gohtml:119
msgid "Camper Version: %s"
msgstr "Camper versió: %s"
@ -2340,7 +2656,7 @@ msgid "Country (optional)"
msgstr "País (opcional)"
#: web/templates/admin/booking/fields.gohtml:212
#: web/templates/admin/booking/guest.gohtml:128
#: web/templates/admin/booking/guest.gohtml:130
msgctxt "input"
msgid "Address (optional)"
msgstr "Adreça (opcional)"
@ -2355,17 +2671,6 @@ msgctxt "input"
msgid "Town or village (optional)"
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
msgctxt "title"
msgid "Edit Booking"
@ -2430,60 +2735,41 @@ msgstr "Nom del titular"
msgid "No booking found."
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
msgctxt "input"
msgid "ID document issue date (if any)"
msgstr "Data dexpedició (si hi consta)"
#: web/templates/admin/booking/guest.gohtml:44
#: web/templates/admin/booking/guest.gohtml:45
msgctxt "input"
msgid "First surname"
msgstr "Primer cognom"
#: web/templates/admin/booking/guest.gohtml:56
#: web/templates/admin/booking/guest.gohtml:57
msgctxt "input"
msgid "Second surname (if has one)"
msgstr "Segon cognom (si en té)"
#: web/templates/admin/booking/guest.gohtml:67
#: web/templates/admin/booking/guest.gohtml:68
msgctxt "input"
msgid "Given name"
msgstr "Nom"
#: web/templates/admin/booking/guest.gohtml:79
#: web/templates/admin/booking/guest.gohtml:80
msgctxt "input"
msgid "Sex"
msgstr "Sexe"
#: web/templates/admin/booking/guest.gohtml:84
#: web/templates/admin/booking/guest.gohtml:85
msgid "Choose a sex"
msgstr "Esculli un sexe"
#: web/templates/admin/booking/guest.gohtml:92
#: web/templates/admin/booking/guest.gohtml:93
msgctxt "input"
msgid "Birthdate"
msgstr "Data de naixement"
#: web/templates/admin/booking/guest.gohtml:104
#: web/templates/admin/booking/guest.gohtml:106
msgctxt "input"
msgid "Nationality"
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/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
#: pkg/campsite/feature.go:269 pkg/season/admin.go:411
#: pkg/services/admin.go:316 pkg/surroundings/admin.go:340
#: pkg/amenity/feature.go:269 pkg/amenity/admin.go:283
#: pkg/invoice/admin.go:1092 pkg/services/admin.go:316
#: pkg/surroundings/admin.go:340 pkg/amenity/feature.go:269
#: pkg/amenity/admin.go:283
msgid "Name can not be empty."
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."
#: 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."
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/booking/admin.go:437 pkg/booking/public.go:597
#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/customer/admin.go:312
#: 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."
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."
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"
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."
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."
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."
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."
msgstr "El preu ha de ser com a mínim zero."
@ -2805,7 +3092,7 @@ msgctxt "header"
msgid "Children (aged 2 to 10)"
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
msgid "Selected campsite type is not valid."
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."
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
msgctxt "role"
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/."
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
msgid "Business name can not be empty."
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."
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."
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
msgid "City can not be empty."
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."
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
msgid "RTC number can not be empty."
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."
msgstr "No podeu deixar el nom del fitxer en blanc."
#: pkg/booking/checkin.go:285
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
#: pkg/booking/checkin.go:284
msgid "ID document issue date must be a valid date."
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."
msgstr "La data dexpedició del document didentitat ha de ser al passat."
#: pkg/booking/checkin.go:292 pkg/booking/checkin.go:293
#: 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
#: pkg/booking/checkin.go:290
msgid "Selected sex is not valid."
msgstr "El sexe escollit no és vàlid."
#: pkg/booking/checkin.go:295
#: pkg/booking/checkin.go:291
msgid "Birthdate can not be empty"
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."
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."
msgstr "La data de naixement ha de ser al passat."
@ -3057,28 +3439,24 @@ msgctxt "cart"
msgid "Dog"
msgstr "Gos"
#: pkg/booking/admin.go:217
#: pkg/booking/admin.go:218
msgctxt "filename"
msgid "bookings.ods"
msgstr "reserves.ods"
#: pkg/booking/admin.go:426 pkg/booking/public.go:586
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
#: pkg/booking/admin.go:432
msgid "Country can not be empty to validate the postcode."
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."
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."
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."
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."
msgstr "El valor de %s ha de ser com a màxim %d."
#: pkg/booking/public.go:590
msgid "Town or village can not be empty."
msgstr "No podeu deixar la població en blanc."
#: pkg/booking/public.go:605
#: pkg/booking/public.go:601
msgid "It is mandatory to agree to the reservation conditions."
msgstr "És obligatori acceptar les condicions de reserves."

614
po/es.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\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"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\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/public/booking/fields.gohtml:146
#: web/templates/admin/payment/details.gohtml:140
#: web/templates/admin/customer/form.gohtml:28
#: web/templates/admin/booking/fields.gohtml:188
msgctxt "title"
msgid "Customer Details"
@ -247,6 +248,7 @@ msgstr "Detalles del cliente"
#: web/templates/mail/payment/details.gotxt:41
#: web/templates/public/booking/fields.gohtml:149
#: web/templates/admin/payment/details.gohtml:143
#: web/templates/admin/customer/form.gohtml:31
#: web/templates/admin/booking/fields.gohtml:191
msgctxt "input"
msgid "Full name"
@ -255,6 +257,7 @@ msgstr "Nombre y apellidos"
#: web/templates/mail/payment/details.gotxt:42
#: web/templates/public/booking/fields.gohtml:158
#: web/templates/admin/payment/details.gohtml:147
#: web/templates/admin/customer/form.gohtml:69
#: web/templates/admin/taxDetails.gohtml:69
msgctxt "input"
msgid "Address"
@ -263,6 +266,7 @@ msgstr "Dirección"
#: web/templates/mail/payment/details.gotxt:43
#: web/templates/public/booking/fields.gohtml:167
#: web/templates/admin/payment/details.gohtml:151
#: web/templates/admin/customer/form.gohtml:105
#: web/templates/admin/taxDetails.gohtml:93
msgctxt "input"
msgid "Postcode"
@ -278,6 +282,7 @@ msgstr "Población"
#: web/templates/mail/payment/details.gotxt:45
#: web/templates/public/booking/fields.gohtml:187
#: web/templates/admin/payment/details.gohtml:159
#: web/templates/admin/customer/form.gohtml:117
#: web/templates/admin/taxDetails.gohtml:101
msgctxt "input"
msgid "Country"
@ -411,11 +416,16 @@ msgid "Order Number"
msgstr "Número de pedido"
#: web/templates/public/payment/details.gohtml:8
#: web/templates/admin/invoice/index.gohtml:103
#: web/templates/admin/invoice/view.gohtml:26
msgctxt "title"
msgid "Date"
msgstr "Fecha"
#: 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"
msgid "Total"
msgstr "Total"
@ -579,6 +589,11 @@ msgctxt "input"
msgid "Year"
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/booking/fields.gohtml:278
msgctxt "action"
@ -929,7 +944,7 @@ msgstr "Menú"
#: web/templates/admin/campsite/type/option/form.gohtml:16
#: web/templates/admin/campsite/type/option/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
msgctxt "title"
msgid "Campsites"
@ -1004,13 +1019,15 @@ msgid "Campground map"
msgstr "Mapa del camping"
#: web/templates/public/booking/fields.gohtml:176
#: web/templates/admin/customer/form.gohtml:81
msgctxt "input"
msgid "Town or village"
msgstr "Población"
#: 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/guest.gohtml:109
#: web/templates/admin/booking/guest.gohtml:111
msgid "Choose a country"
msgstr "Escoja un país"
@ -1164,6 +1181,7 @@ msgstr "Álias"
#: web/templates/admin/campsite/type/form.gohtml:51
#: web/templates/admin/campsite/type/option/form.gohtml:41
#: 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/profile.gohtml:29
#: 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/option/form.gohtml:98
#: 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/surroundings/form.gohtml:69
#: 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/option/form.gohtml:100
#: web/templates/admin/season/form.gohtml:75
#: web/templates/admin/customer/form.gohtml:155
#: web/templates/admin/services/form.gohtml:83
#: web/templates/admin/surroundings/form.gohtml:71
#: 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/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/surroundings/index.gohtml:83
#: web/templates/admin/amenity/feature/index.gohtml:30
@ -1725,6 +1747,7 @@ msgid "Per night"
msgstr "Por noche"
#: web/templates/admin/campsite/type/option/form.gohtml:84
#: web/templates/admin/invoice/product-form.gohtml:29
msgctxt "input"
msgid "Price"
msgstr "Precio"
@ -1824,12 +1847,316 @@ msgctxt "action"
msgid "Cancel"
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:13 web/templates/admin/layout.gohtml:89
msgctxt "title"
msgid "Dashboard"
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
msgctxt "title"
msgid "Login"
@ -1928,12 +2255,6 @@ msgctxt "title"
msgid "Users"
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
msgctxt "header"
msgid "IP Address"
@ -1985,11 +2306,6 @@ msgctxt "input"
msgid "Trade Name"
msgstr "Nombre comercial"
#: web/templates/admin/taxDetails.gohtml:85
msgctxt "input"
msgid "Province"
msgstr "Provincia"
#: web/templates/admin/taxDetails.gohtml:111
msgctxt "input"
msgid "Currency"
@ -2214,11 +2530,11 @@ msgctxt "title"
msgid "Bookings"
msgstr "Reservas"
#: web/templates/admin/layout.gohtml:101
#: web/templates/admin/layout.gohtml:107
msgid "Breadcrumb"
msgstr "Migas de pan"
#: web/templates/admin/layout.gohtml:113
#: web/templates/admin/layout.gohtml:119
msgid "Camper Version: %s"
msgstr "Camper versión: %s"
@ -2340,7 +2656,7 @@ msgid "Country (optional)"
msgstr "País (opcional)"
#: web/templates/admin/booking/fields.gohtml:212
#: web/templates/admin/booking/guest.gohtml:128
#: web/templates/admin/booking/guest.gohtml:130
msgctxt "input"
msgid "Address (optional)"
msgstr "Dirección (opcional)"
@ -2355,17 +2671,6 @@ msgctxt "input"
msgid "Town or village (optional)"
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
msgctxt "title"
msgid "Edit Booking"
@ -2430,60 +2735,41 @@ msgstr "Nombre del titular"
msgid "No booking found."
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
msgctxt "input"
msgid "ID document issue date (if any)"
msgstr "Fecha expedición del documento (si hay)"
#: web/templates/admin/booking/guest.gohtml:44
#: web/templates/admin/booking/guest.gohtml:45
msgctxt "input"
msgid "First surname"
msgstr "Primer apellido"
#: web/templates/admin/booking/guest.gohtml:56
#: web/templates/admin/booking/guest.gohtml:57
msgctxt "input"
msgid "Second surname (if has one)"
msgstr "Segundo apellido (si tiene)"
#: web/templates/admin/booking/guest.gohtml:67
#: web/templates/admin/booking/guest.gohtml:68
msgctxt "input"
msgid "Given name"
msgstr "Nombre"
#: web/templates/admin/booking/guest.gohtml:79
#: web/templates/admin/booking/guest.gohtml:80
msgctxt "input"
msgid "Sex"
msgstr "Sexo"
#: web/templates/admin/booking/guest.gohtml:84
#: web/templates/admin/booking/guest.gohtml:85
msgid "Choose a sex"
msgstr "Escoja un sexo"
#: web/templates/admin/booking/guest.gohtml:92
#: web/templates/admin/booking/guest.gohtml:93
msgctxt "input"
msgid "Birthdate"
msgstr "Fecha de nacimiento"
#: web/templates/admin/booking/guest.gohtml:104
#: web/templates/admin/booking/guest.gohtml:106
msgctxt "input"
msgid "Nationality"
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/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
#: pkg/campsite/feature.go:269 pkg/season/admin.go:411
#: pkg/services/admin.go:316 pkg/surroundings/admin.go:340
#: pkg/amenity/feature.go:269 pkg/amenity/admin.go:283
#: pkg/invoice/admin.go:1092 pkg/services/admin.go:316
#: pkg/surroundings/admin.go:340 pkg/amenity/feature.go:269
#: pkg/amenity/admin.go:283
msgid "Name can not be empty."
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."
#: 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."
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/booking/admin.go:437 pkg/booking/public.go:597
#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/customer/admin.go:312
#: 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."
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."
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"
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."
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."
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."
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."
msgstr "El precio tiene que ser como mínimo cero."
@ -2805,7 +3092,7 @@ msgctxt "header"
msgid "Children (aged 2 to 10)"
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
msgid "Selected campsite type is not valid."
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."
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
msgctxt "role"
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/."
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
msgid "Business name can not be empty."
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."
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."
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
msgid "City can not be empty."
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."
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
msgid "RTC number can not be empty."
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."
msgstr "No podéis dejar el nombre del archivo en blanco."
#: pkg/booking/checkin.go:285
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
#: pkg/booking/checkin.go:284
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."
#: pkg/booking/checkin.go:289
#: pkg/booking/checkin.go:285
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."
#: pkg/booking/checkin.go:292 pkg/booking/checkin.go:293
#: 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
#: pkg/booking/checkin.go:290
msgid "Selected sex is not valid."
msgstr "El sexo escogido no es válido."
#: pkg/booking/checkin.go:295
#: pkg/booking/checkin.go:291
msgid "Birthdate can not be empty"
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."
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."
msgstr "La fecha de nacimiento tiene que ser del pasado."
@ -3057,28 +3439,24 @@ msgctxt "cart"
msgid "Dog"
msgstr "Perro"
#: pkg/booking/admin.go:217
#: pkg/booking/admin.go:218
msgctxt "filename"
msgid "bookings.ods"
msgstr "reservas.ods"
#: pkg/booking/admin.go:426 pkg/booking/public.go:586
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
#: pkg/booking/admin.go:432
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."
#: pkg/booking/admin.go:441
#: pkg/booking/admin.go:442
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."
#: pkg/booking/admin.go:448
#: pkg/booking/admin.go:449
msgid "You must select at least one accommodation."
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."
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."
msgstr "%s tiene que ser como máximo %d"
#: pkg/booking/public.go:590
msgid "Town or village can not be empty."
msgstr "No podéis dejar la población en blanco."
#: pkg/booking/public.go:605
#: pkg/booking/public.go:601
msgid "It is mandatory to agree to the reservation conditions."
msgstr "Es obligatorio aceptar las condiciones de reserva."

614
po/fr.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\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"
"Last-Translator: Oriol Carbonell <info@oriolcarbonell.cat>\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/public/booking/fields.gohtml:146
#: web/templates/admin/payment/details.gohtml:140
#: web/templates/admin/customer/form.gohtml:28
#: web/templates/admin/booking/fields.gohtml:188
msgctxt "title"
msgid "Customer Details"
@ -247,6 +248,7 @@ msgstr "Détails du client"
#: web/templates/mail/payment/details.gotxt:41
#: web/templates/public/booking/fields.gohtml:149
#: web/templates/admin/payment/details.gohtml:143
#: web/templates/admin/customer/form.gohtml:31
#: web/templates/admin/booking/fields.gohtml:191
msgctxt "input"
msgid "Full name"
@ -255,6 +257,7 @@ msgstr "Nom et prénom"
#: web/templates/mail/payment/details.gotxt:42
#: web/templates/public/booking/fields.gohtml:158
#: web/templates/admin/payment/details.gohtml:147
#: web/templates/admin/customer/form.gohtml:69
#: web/templates/admin/taxDetails.gohtml:69
msgctxt "input"
msgid "Address"
@ -263,6 +266,7 @@ msgstr "Adresse"
#: web/templates/mail/payment/details.gotxt:43
#: web/templates/public/booking/fields.gohtml:167
#: web/templates/admin/payment/details.gohtml:151
#: web/templates/admin/customer/form.gohtml:105
#: web/templates/admin/taxDetails.gohtml:93
msgctxt "input"
msgid "Postcode"
@ -278,6 +282,7 @@ msgstr "Ville"
#: web/templates/mail/payment/details.gotxt:45
#: web/templates/public/booking/fields.gohtml:187
#: web/templates/admin/payment/details.gohtml:159
#: web/templates/admin/customer/form.gohtml:117
#: web/templates/admin/taxDetails.gohtml:101
msgctxt "input"
msgid "Country"
@ -411,11 +416,16 @@ msgid "Order Number"
msgstr "Numéro de commande"
#: web/templates/public/payment/details.gohtml:8
#: web/templates/admin/invoice/index.gohtml:103
#: web/templates/admin/invoice/view.gohtml:26
msgctxt "title"
msgid "Date"
msgstr "Date"
#: 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"
msgid "Total"
msgstr "Totale"
@ -579,6 +589,11 @@ msgctxt "input"
msgid "Year"
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/booking/fields.gohtml:278
msgctxt "action"
@ -929,7 +944,7 @@ msgstr "Menu"
#: web/templates/admin/campsite/type/option/form.gohtml:16
#: web/templates/admin/campsite/type/option/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
msgctxt "title"
msgid "Campsites"
@ -1004,13 +1019,15 @@ msgid "Campground map"
msgstr "Plan du camping"
#: web/templates/public/booking/fields.gohtml:176
#: web/templates/admin/customer/form.gohtml:81
msgctxt "input"
msgid "Town or village"
msgstr "Ville"
#: 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/guest.gohtml:109
#: web/templates/admin/booking/guest.gohtml:111
msgid "Choose a country"
msgstr "Choisissez un pays"
@ -1164,6 +1181,7 @@ msgstr "Slug"
#: web/templates/admin/campsite/type/form.gohtml:51
#: web/templates/admin/campsite/type/option/form.gohtml:41
#: 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/profile.gohtml:29
#: 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/option/form.gohtml:98
#: 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/surroundings/form.gohtml:69
#: 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/option/form.gohtml:100
#: web/templates/admin/season/form.gohtml:75
#: web/templates/admin/customer/form.gohtml:155
#: web/templates/admin/services/form.gohtml:83
#: web/templates/admin/surroundings/form.gohtml:71
#: 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/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/surroundings/index.gohtml:83
#: web/templates/admin/amenity/feature/index.gohtml:30
@ -1725,6 +1747,7 @@ msgid "Per night"
msgstr "Par nuit"
#: web/templates/admin/campsite/type/option/form.gohtml:84
#: web/templates/admin/invoice/product-form.gohtml:29
msgctxt "input"
msgid "Price"
msgstr "Prix"
@ -1824,12 +1847,316 @@ msgctxt "action"
msgid "Cancel"
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:13 web/templates/admin/layout.gohtml:89
msgctxt "title"
msgid "Dashboard"
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
msgctxt "title"
msgid "Login"
@ -1928,12 +2255,6 @@ msgctxt "title"
msgid "Users"
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
msgctxt "header"
msgid "IP Address"
@ -1985,11 +2306,6 @@ msgctxt "input"
msgid "Trade Name"
msgstr "Nom commercial"
#: web/templates/admin/taxDetails.gohtml:85
msgctxt "input"
msgid "Province"
msgstr "Province"
#: web/templates/admin/taxDetails.gohtml:111
msgctxt "input"
msgid "Currency"
@ -2214,11 +2530,11 @@ msgctxt "title"
msgid "Bookings"
msgstr "Réservations"
#: web/templates/admin/layout.gohtml:101
#: web/templates/admin/layout.gohtml:107
msgid "Breadcrumb"
msgstr "Fil dAriane"
#: web/templates/admin/layout.gohtml:113
#: web/templates/admin/layout.gohtml:119
msgid "Camper Version: %s"
msgstr "Camper version: %s"
@ -2340,7 +2656,7 @@ msgid "Country (optional)"
msgstr "Pays (facultatif)"
#: web/templates/admin/booking/fields.gohtml:212
#: web/templates/admin/booking/guest.gohtml:128
#: web/templates/admin/booking/guest.gohtml:130
msgctxt "input"
msgid "Address (optional)"
msgstr "Adresse (facultatif)"
@ -2355,17 +2671,6 @@ msgctxt "input"
msgid "Town or village (optional)"
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
msgctxt "title"
msgid "Edit Booking"
@ -2430,60 +2735,41 @@ msgstr "Nom du titulaire"
msgid "No booking found."
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
msgctxt "input"
msgid "ID document issue date (if any)"
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"
msgid "First surname"
msgstr "Premier nom"
#: web/templates/admin/booking/guest.gohtml:56
#: web/templates/admin/booking/guest.gohtml:57
msgctxt "input"
msgid "Second surname (if has one)"
msgstr "Deuxième nom"
#: web/templates/admin/booking/guest.gohtml:67
#: web/templates/admin/booking/guest.gohtml:68
msgctxt "input"
msgid "Given name"
msgstr "Prénom"
#: web/templates/admin/booking/guest.gohtml:79
#: web/templates/admin/booking/guest.gohtml:80
msgctxt "input"
msgid "Sex"
msgstr "Sexe"
#: web/templates/admin/booking/guest.gohtml:84
#: web/templates/admin/booking/guest.gohtml:85
msgid "Choose a sex"
msgstr "Choisissez un sexe"
#: web/templates/admin/booking/guest.gohtml:92
#: web/templates/admin/booking/guest.gohtml:93
msgctxt "input"
msgid "Birthdate"
msgstr "Date de naissance"
#: web/templates/admin/booking/guest.gohtml:104
#: web/templates/admin/booking/guest.gohtml:106
msgctxt "input"
msgid "Nationality"
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/campsite/types/feature.go:272 pkg/campsite/types/admin.go:577
#: pkg/campsite/feature.go:269 pkg/season/admin.go:411
#: pkg/services/admin.go:316 pkg/surroundings/admin.go:340
#: pkg/amenity/feature.go:269 pkg/amenity/admin.go:283
#: pkg/invoice/admin.go:1092 pkg/services/admin.go:316
#: pkg/surroundings/admin.go:340 pkg/amenity/feature.go:269
#: pkg/amenity/admin.go:283
msgid "Name can not be empty."
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."
#: 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."
msgstr "Le-mail ne peut pas être vide."
#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:225
#: pkg/booking/admin.go:437 pkg/booking/public.go:597
#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/customer/admin.go:312
#: 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."
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."
msgstr "Le fichier doit être une image PNG ou JPEG valide."
#: pkg/app/admin.go:73
#: pkg/app/admin.go:79
msgid "Access forbidden"
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."
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."
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."
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."
msgstr "Le prix doit être égal ou supérieur à zéro."
@ -2805,7 +3092,7 @@ msgctxt "header"
msgid "Children (aged 2 to 10)"
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
msgid "Selected campsite type is not valid."
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."
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
msgctxt "role"
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/."
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
msgid "Business name can not be empty."
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."
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."
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
msgid "City can not be empty."
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."
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
msgid "RTC number can not be empty."
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."
msgstr "Le nom de fichier ne peut pas être vide."
#: pkg/booking/checkin.go:285
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
#: pkg/booking/checkin.go:284
msgid "ID document issue date must be a valid date."
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."
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/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
#: pkg/booking/checkin.go:290
msgid "Selected sex is not valid."
msgstr "Le sexe sélectionné nest pas valide."
#: pkg/booking/checkin.go:295
#: pkg/booking/checkin.go:291
msgid "Birthdate can not be empty"
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."
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."
msgstr "La date de naissance doit être du passé."
@ -3057,28 +3439,24 @@ msgctxt "cart"
msgid "Dog"
msgstr "Chien"
#: pkg/booking/admin.go:217
#: pkg/booking/admin.go:218
msgctxt "filename"
msgid "bookings.ods"
msgstr "reservations.ods"
#: pkg/booking/admin.go:426 pkg/booking/public.go:586
msgid "Full name must have at least one letter."
msgstr "Le nom complet doit comporter au moins une lettre."
#: pkg/booking/admin.go:431
#: pkg/booking/admin.go:432
msgid "Country can not be empty to validate the postcode."
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."
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."
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."
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."
msgstr "%s doit être tout au plus %d."
#: pkg/booking/public.go:590
msgid "Town or village can not be empty."
msgstr "La ville ne peut pas être vide."
#: pkg/booking/public.go:605
#: pkg/booking/public.go:601
msgid "It is mandatory to agree to the reservation conditions."
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;

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