Add the payments section

This actually should be the “payments and receivables” section, however
this is quite a mouthful; a “receivable” is a payment made **to** you,
therefore “payments” is ok.

In fact, there is still no receivables in there, as they should be in
a separate relation, to constraint them to invoices instead of expenses.
It will be done in a separate commit.

Since this section will be, in a sense, sort of simplified accounting,
i needed to introduce the “payment account” concept.  There is no way,
yet, for users to add them, because i have to revamp the “tax details”
section, but this commit started to grow too big already.

The same reasoning for the attachment payment slips as PDF to payment:
something i have to add, but not yet in this commit.
This commit is contained in:
jordi fita mas 2024-08-10 04:34:07 +02:00
parent f546632a89
commit ad5bc271b6
107 changed files with 4373 additions and 112 deletions

67
deploy/add_payment.sql Normal file
View File

@ -0,0 +1,67 @@
-- Deploy numerus:add_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment
-- requires: expense_payment
-- requires: company
-- requires: currency
-- requires: parse_price
-- requires: tag_name
-- requires: update_expense_payment_status
begin;
set search_path to numerus, public;
create or replace function add_payment(company integer, expense_id integer, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
$$
declare
pslug uuid;
pid integer;
amount_cents integer;
begin
insert into payment
( company_id
, payment_account_id
, description
, payment_date
, amount
, currency_code
, payment_status
, tags
)
select company_id
, payment_account_id
, description
, payment_date
, parse_price(amount, currency.decimal_digits)
, currency_code
, 'complete'
, tags
from company
join currency using (currency_code)
where company.company_id = add_payment.company
returning payment_id, slug, payment.amount
into pid, pslug, amount_cents
;
if expense_id is not null then
-- must be inserted before updating statuses, so that it can see this
-- payments amount too.
insert into expense_payment (expense_id, payment_id)
values (expense_id, pid);
perform update_expense_payment_status(pid, expense_id, amount_cents);
end if;
return pslug;
end
$$
language plpgsql
;
revoke execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) from public;
grant execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) to invoicer;
grant execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) to admin;
commit;

View File

@ -0,0 +1,35 @@
-- Deploy numerus:add_payment_account_bank to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: payment_account_bank
begin;
set search_path to numerus, public;
create or replace function add_payment_account_bank(company integer, name text, iban iban) returns uuid as
$$
declare
account_id integer;
account_slug uuid;
begin
insert into payment_account (company_id, payment_account_type, name)
select company, 'bank', add_payment_account_bank.name
returning payment_account_id, slug into account_id, account_slug;
insert into payment_account_bank (payment_account_id, iban)
values (account_id, iban)
;
return account_slug;
end;
$$
language plpgsql
;
revoke execute on function add_payment_account_bank(integer, text, iban) from public;
grant execute on function add_payment_account_bank(integer, text, iban) to invoicer;
grant execute on function add_payment_account_bank(integer, text, iban) to admin;
commit;

View File

@ -0,0 +1,34 @@
-- Deploy numerus:add_payment_account_card to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: payment_account_card
begin;
set search_path to numerus, public;
create or replace function add_payment_account_card(company integer, name text, four_digits text, exp_date date) returns uuid as
$$
declare
account_id integer;
account_slug uuid;
begin
insert into payment_account (company_id, payment_account_type, name)
select company, 'card', add_payment_account_card.name
returning payment_account_id, slug into account_id, account_slug;
insert into payment_account_card (payment_account_id, last_four_digits, expiration_date)
values (account_id, four_digits, exp_date);
return account_slug;
end
$$
language plpgsql
;
revoke execute on function add_payment_account_card(integer, text, text, date) from public;
grant execute on function add_payment_account_card(integer, text, text, date) to invoicer;
grant execute on function add_payment_account_card(integer, text, text, date) to admin;
commit;

View File

@ -0,0 +1,23 @@
-- Deploy numerus:add_payment_account_cash to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
begin;
set search_path to numerus, public;
create or replace function add_payment_account_cash(company integer, name text) returns uuid as
$$
insert into payment_account (company_id, payment_account_type, name)
values (company, 'cash', name)
returning slug;
$$
language sql
;
revoke execute on function add_payment_account_cash(integer, text) from public;
grant execute on function add_payment_account_cash(integer, text) to invoicer;
grant execute on function add_payment_account_cash(integer, text) to admin;
commit;

View File

@ -0,0 +1,23 @@
-- Deploy numerus:add_payment_account_other to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
begin;
set search_path to numerus, public;
create or replace function add_payment_account_other(company integer, name text) returns uuid as
$$
insert into payment_account (company_id, payment_account_type, name)
values (company, 'other', name)
returning slug;
$$
language sql
;
revoke execute on function add_payment_account_other(integer, text) from public;
grant execute on function add_payment_account_other(integer, text) to invoicer;
grant execute on function add_payment_account_other(integer, text) to admin;
commit;

View File

@ -9,14 +9,19 @@ set search_path to numerus;
insert into expense_status (expense_status, name)
values ('pending', 'Pending')
, ('partial', 'Partial')
, ('paid', 'Paid')
on conflict (expense_status) do nothing
;
insert into expense_status_i18n (expense_status, lang_tag, name)
values ('pending', 'ca', 'Pendent')
, ('partial', 'ca', 'Parcial')
, ('paid', 'ca', 'Pagada')
, ('pending', 'es', 'Pendiente')
, ('partial', 'es', 'Parcial')
, ('paid', 'es', 'Pagada')
on conflict (expense_status, lang_tag) do nothing
;
commit;

View File

@ -0,0 +1,22 @@
-- Deploy numerus:available_expense_status to pg
-- requires: schema_numerus
-- requires: expense_status
-- requires: expense_status_i18n
begin;
set search_path to numerus;
insert into expense_status (expense_status, name)
values ('pending', 'Pending')
, ('paid', 'Paid')
;
insert into expense_status_i18n (expense_status, lang_tag, name)
values ('pending', 'ca', 'Pendent')
, ('paid', 'ca', 'Pagada')
, ('pending', 'es', 'Pendiente')
, ('paid', 'es', 'Pagada')
;
commit;

View File

@ -0,0 +1,28 @@
-- Deploy numerus:available_payment_account_types to pg
-- requires: schema_numerus
-- requires: payment_account_type
-- requires: payment_account_type_i18n
begin;
set search_path to numerus;
insert into payment_account_type (payment_account_type, name)
values ('bank', 'Bank')
, ('card', 'Credit Card')
, ('cash', 'Cash')
, ('other', 'Other')
;
insert into payment_account_type_i18n (payment_account_type, lang_tag, name)
values ('bank', 'ca', 'Banc')
, ('card', 'ca', 'Targeta de crèdit')
, ('cash', 'ca', 'Efectiu')
, ('other', 'ca', 'Altres')
, ('bank', 'es', 'Banco')
, ('card', 'es', 'Tarjeta de crédito')
, ('cash', 'es', 'Efectivo')
, ('other', 'es', 'Otros')
;
commit;

View File

@ -0,0 +1,22 @@
-- Deploy numerus:available_payment_status to pg
-- requires: schema_numerus
-- requires: payment_status
-- requires: payment_status_i18n
begin;
set search_path to numerus, public;
insert into payment_status (payment_status, name)
values ('partial', 'Partial')
, ('complete', 'Complete')
;
insert into payment_status_i18n (payment_status, lang_tag, name)
values ('partial', 'ca', 'Parcial')
, ('partial', 'es', 'Parcial')
, ('complete', 'ca', 'Complet')
, ('complete', 'es', 'Completo')
;
commit;

53
deploy/edit_payment.sql Normal file
View File

@ -0,0 +1,53 @@
-- Deploy numerus:edit_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment
-- requires: expense_payment
-- requires: currency
-- requires: parse_price
-- requires: tag_name
-- requires: update_expense_payment_status
begin;
set search_path to numerus, public;
create or replace function edit_payment(payment_slug uuid, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
$$
declare
pid integer;
eid integer;
amount_cents integer;
begin
update payment
set payment_date = edit_payment.payment_date
, payment_account_id = edit_payment.payment_account_id
, description = edit_payment.description
, amount = parse_price(edit_payment.amount, decimal_digits)
, tags = edit_payment.tags
from currency
where slug = payment_slug
and currency.currency_code = payment.currency_code
returning payment_id, payment.amount
into pid, amount_cents
;
select expense_id into eid
from expense_payment
where payment_id = pid;
if eid is not null then
perform update_expense_payment_status(pid, eid, amount_cents);
end if;
return payment_slug;
end
$$
language plpgsql
;
revoke execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) from public;
grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to invoicer;
grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to admin;
commit;

View File

@ -0,0 +1,44 @@
-- Deploy numerus:edit_payment_account_bank to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: payment_account_bank
-- requires: extension_pgcrypto
-- requires: extension_iban
begin;
set search_path to numerus, public;
create or replace function edit_payment_account_bank(account_slug uuid, new_name text, new_iban iban) returns uuid as
$$
declare
account_id int;
begin
update payment_account
set name = new_name
where slug = account_slug
and payment_account_type = 'bank'
returning payment_account_id into account_id
;
if account_id is null then
return null;
end if;
update payment_account_bank
set iban = new_iban
where payment_account_id = account_id
;
return account_slug;
end
$$
language plpgsql
;
revoke execute on function edit_payment_account_bank(uuid, text, iban) from public;
grant execute on function edit_payment_account_bank(uuid, text, iban) to invoicer;
grant execute on function edit_payment_account_bank(uuid, text, iban) to admin;
commit;

View File

@ -0,0 +1,44 @@
-- Deploy numerus:edit_payment_account_card to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: payment_account_card
-- requires: extension_pgcrypto
begin;
set search_path to numerus, public;
create or replace function edit_payment_account_card(account_slug uuid, new_name text, new_last_digits text, new_exp_date date) returns uuid as
$$
declare
account_id integer;
begin
update payment_account
set name = new_name
where slug = account_slug
and payment_account_type = 'card'
returning payment_account_id into account_id
;
if account_id is null then
return null;
end if;
update payment_account_card
set last_four_digits = new_last_digits
, expiration_date = new_exp_date
where payment_account_id = account_id
;
return account_slug;
end
$$
language plpgsql
;
revoke execute on function edit_payment_account_card(uuid, text, text, date) from public;
grant execute on function edit_payment_account_card(uuid, text, text, date) to invoicer;
grant execute on function edit_payment_account_card(uuid, text, text, date) to admin;
commit;

View File

@ -0,0 +1,27 @@
-- Deploy numerus:edit_payment_account_cash to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: extension_pgcrypto
begin;
set search_path to numerus, public;
create or replace function edit_payment_account_cash(account_slug uuid, new_name text) returns uuid as
$$
update payment_account
set name = new_name
where slug = account_slug
and payment_account_type = 'cash'
returning slug
;
$$
language sql
;
revoke execute on function edit_payment_account_cash(uuid, text) from public;
grant execute on function edit_payment_account_cash(uuid, text) to invoicer;
grant execute on function edit_payment_account_cash(uuid, text) to admin;
commit;

View File

@ -0,0 +1,27 @@
-- Deploy numerus:edit_payment_account_other to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: extension_pgcrypto
begin;
set search_path to numerus, public;
create or replace function edit_payment_account_other(account_slug uuid, new_name text) returns uuid as
$$
update payment_account
set name = new_name
where slug = account_slug
and payment_account_type = 'other'
returning slug
;
$$
language sql
;
revoke execute on function edit_payment_account_other(uuid, text) from public;
grant execute on function edit_payment_account_other(uuid, text) to invoicer;
grant execute on function edit_payment_account_other(uuid, text) to admin;
commit;

View File

@ -0,0 +1,32 @@
-- Deploy numerus:expense_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: expense
-- requires: payment
begin;
set search_path to numerus, public;
create table expense_payment (
expense_id integer not null references expense,
payment_id integer not null references payment,
primary key (expense_id, payment_id)
);
grant select, insert, update, delete on table expense_payment to invoicer;
grant select, insert, update, delete on table expense_payment to admin;
alter table expense_payment enable row level security;
create policy company_policy
on expense_payment
using (
exists(
select 1
from expense
where expense.expense_id = expense_payment.expense_id
)
);
commit;

47
deploy/payment.sql Normal file
View File

@ -0,0 +1,47 @@
-- Deploy numerus:payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: company
-- requires: payment_account
-- requires: currency
-- requires: tag_name
-- requires: payment_status
-- requires: extension_pgcrypto
begin;
set search_path to numerus, public;
create table payment (
payment_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(),
description text not null,
payment_date date not null default current_date,
payment_account_id integer not null references payment_account,
amount integer not null,
currency_code text not null references currency,
tags tag_name[] not null default '{}',
payment_status text not null default 'complete' references payment_status,
created_at timestamptz not null default current_timestamp
);
create index on payment using gin (tags);
grant select, insert, update, delete on table payment to invoicer;
grant select, insert, update, delete on table payment to admin;
alter table payment enable row level security;
create policy company_policy
on payment
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = payment.company_id
)
);
commit;

View File

@ -0,0 +1,38 @@
-- Deploy numerus:payment_account to pg
-- requires: roles
-- requires: schema_numerus
-- requires: company
-- requires: payment_account_type
-- requires: extension_pgcrypto
begin;
set search_path to numerus, public;
create table payment_account (
payment_account_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(),
payment_account_type text not null references payment_account_type,
name text not null constraint payment_account_name_not_empty check(length(trim(name)) > 0),
unique (payment_account_id, payment_account_type)
);
grant select, insert, update, delete on table payment_account to invoicer;
grant select, insert, update, delete on table payment_account to admin;
alter table payment_account enable row level security;
create policy company_policy
on payment_account
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = payment_account.company_id
)
);
commit;

View File

@ -0,0 +1,33 @@
-- Deploy numerus:payment_account_bank to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: extension_iban
begin;
set search_path to numerus, public;
create table payment_account_bank (
payment_account_id integer primary key,
payment_account_type text not null default 'bank' constraint payment_account_type_is_bank check (payment_account_type = 'bank'),
iban iban not null,
foreign key (payment_account_id, payment_account_type) references payment_account (payment_account_id, payment_account_type)
);
grant select, insert, update, delete on table payment_account_bank to invoicer;
grant select, insert, update, delete on table payment_account_bank to admin;
alter table payment_account_bank enable row level security;
create policy company_policy
on payment_account_bank
using (
exists(
select 1
from payment_account
where payment_account.payment_account_id = payment_account_bank.payment_account_id
)
);
commit;

View File

@ -0,0 +1,33 @@
-- Deploy numerus:payment_account_card to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
begin;
set search_path to numerus, public;
create table payment_account_card (
payment_account_id integer primary key,
payment_account_type text not null default 'card' constraint payment_account_type_is_card check (payment_account_type = 'card'),
last_four_digits text not null constraint last_four_digits_are_digits check ( last_four_digits ~ '^\d{4}$'),
expiration_date date not null,
foreign key (payment_account_id, payment_account_type) references payment_account (payment_account_id, payment_account_type)
);
grant select, insert, update, delete on table payment_account_card to invoicer;
grant select, insert, update, delete on table payment_account_card to admin;
alter table payment_account_card enable row level security;
create policy company_policy
on payment_account_card
using (
exists(
select 1
from payment_account
where payment_account.payment_account_id = payment_account_card.payment_account_id
)
);
commit;

View File

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

View File

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

17
deploy/payment_status.sql Normal file
View File

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

View File

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

View File

@ -0,0 +1,43 @@
-- Deploy numerus:update_expense_payment_status to pg
-- requires: roles
-- requires: schema_numerus
-- requires: expense
-- requires: payment
-- requires: expense_payment
-- requires: available_expense_status
-- requires: available_payment_status
begin;
set search_path to numerus, public;
create or replace function update_expense_payment_status(pid integer, eid integer, amount_cents integer) returns void as
$$
update payment
set payment_status = case when expense.amount > amount_cents or exists (select 1 from expense_payment as ep where ep.expense_id = expense.expense_id and payment_id <> pid) then 'partial' else 'complete' end
from expense
where expense.expense_id = eid
and payment_id = pid
;
update expense
set expense_status = case when paid_amount >= expense.amount then 'paid' else 'partial' end
from (
select expense_payment.expense_id
, sum(payment.amount) as paid_amount
from expense_payment
join payment using (payment_id)
group by expense_payment.expense_id
) as payment
where payment.expense_id = expense.expense_id
and expense.expense_id = eid
;
$$
language sql
;
revoke execute on function update_expense_payment_status(integer, integer, integer) from public;
grant execute on function update_expense_payment_status(integer, integer, integer) to invoicer;
grant execute on function update_expense_payment_status(integer, integer, integer) to admin;
commit;

340
pkg/accounts.go Normal file
View File

@ -0,0 +1,340 @@
package pkg
import (
"context"
"github.com/jackc/pgtype"
"github.com/julienschmidt/httprouter"
"html/template"
"net/http"
"time"
)
const (
ExpirationDateFormat = "01/06"
AccountTypeBank = "bank"
AccountTypeCard = "card"
AccountTypeCash = "cash"
AccountTypeOther = "other"
)
func servePaymentAccountIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
page := NewPaymentAccountIndexPage(r.Context(), conn, locale)
page.MustRender(w, r)
}
type PaymentAccountIndexPage struct {
Accounts []*PaymentAccountEntry
}
func NewPaymentAccountIndexPage(ctx context.Context, conn *Conn, locale *Locale) *PaymentAccountIndexPage {
return &PaymentAccountIndexPage{
Accounts: mustCollectPaymentAccountEntries(ctx, conn, locale),
}
}
func (page *PaymentAccountIndexPage) MustRender(w http.ResponseWriter, r *http.Request) {
mustRenderMainTemplate(w, r, "payments/accounts/index.gohtml", page)
}
type PaymentAccountEntry struct {
ID int
Slug string
Type string
TypeLabel string
Name string
IBAN string
LastFourDigits string
ExpirationDate string
}
func mustCollectPaymentAccountEntries(ctx context.Context, conn *Conn, locale *Locale) []*PaymentAccountEntry {
rows := conn.MustQuery(ctx, `
select payment_account_id
, slug
, payment_account.payment_account_type
, coalesce(i18n.name, payment_account_type.name)
, payment_account.name
, coalesce(iban::text, '') as iban
, coalesce(last_four_digits, '') as last_four_digits
, expiration_date
from payment_account
left join payment_account_bank using (payment_account_id, payment_account_type)
left join payment_account_card using (payment_account_id, payment_account_type)
join payment_account_type using (payment_account_type)
left join payment_account_type_i18n as i18n on payment_account_type.payment_account_type = i18n.payment_account_type and i18n.lang_tag = $1
order by payment_account_id
`, locale.Language.String())
defer rows.Close()
var entries []*PaymentAccountEntry
for rows.Next() {
entry := &PaymentAccountEntry{}
var expirationDate pgtype.Date
if err := rows.Scan(&entry.ID, &entry.Slug, &entry.Type, &entry.TypeLabel, &entry.Name, &entry.IBAN, &entry.LastFourDigits, &expirationDate); err != nil {
panic(err)
}
if expirationDate.Status == pgtype.Present {
entry.ExpirationDate = expirationDate.Time.Format(ExpirationDateFormat)
}
entries = append(entries, entry)
}
if rows.Err() != nil {
panic(rows.Err())
}
return entries
}
func servePaymentAccountForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
form := newPaymentAccountForm(r.Context(), conn, locale, company)
slug := params[0].Value
if slug == "new" {
form.MustRender(w, r)
return
}
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
http.NotFound(w, r)
return
}
form.MustRender(w, r)
}
type PaymentAccountForm struct {
locale *Locale
company *Company
Slug string
Type *RadioField
Name *InputField
IBAN *InputField
LastFourDigits *InputField
ExpirationMonthYear *InputField
}
func newPaymentAccountForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *PaymentAccountForm {
return &PaymentAccountForm{
locale: locale,
company: company,
Type: &RadioField{
Name: "payment_account_type",
Label: pgettext("input", "Type", locale),
Required: true,
Options: MustGetRadioOptions(ctx, conn, "select payment_account_type, i18n.name from payment_account_type join payment_account_type_i18n as i18n using (payment_account_type) where i18n.lang_tag = $1 order by payment_account_type", locale.Language.String()),
Attributes: []template.HTMLAttr{
`x-model="type"`,
},
},
Name: &InputField{
Name: "name",
Label: pgettext("input", "Name", locale),
Required: true,
Type: "text",
},
IBAN: &InputField{
Name: "iban",
Label: pgettext("input", "IBAN", locale),
Required: true,
Type: "text",
},
LastFourDigits: &InputField{
Name: "last_four_digits",
Label: pgettext("input", "Cards last four digits", locale),
Required: true,
Type: "text",
Attributes: []template.HTMLAttr{
`maxlength="4"`,
`minlength="4"`,
`pattern="[0-9]{4}"`,
},
},
ExpirationMonthYear: &InputField{
Name: "expiration_date",
Label: pgettext("input", "Expiration date", locale),
Required: true,
Type: "text",
Attributes: []template.HTMLAttr{
`maxlength="5"`,
`minlength="5"`,
`pattern="[0-9]{2}/[0-9]{2}"`,
},
},
}
}
func (f *PaymentAccountForm) MustRender(w http.ResponseWriter, r *http.Request) {
if f.Slug == "" {
mustRenderMainTemplate(w, r, "payments/accounts/new.gohtml", f)
} else {
mustRenderMainTemplate(w, r, "payments/accounts/edit.gohtml", f)
}
}
func (f *PaymentAccountForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
selectedType := f.Type.Selected
var expirationDate pgtype.Date
if notFoundErrorOrPanic(conn.QueryRow(ctx, `
select payment_account_type
, name
, coalesce(iban::text, '') as iban
, coalesce(last_four_digits, '') as last_four_digits
, expiration_date
from payment_account
left join payment_account_bank using (payment_account_id, payment_account_type)
left join payment_account_card using (payment_account_id, payment_account_type)
where slug = $1
`, slug).Scan(
f.Type,
f.Name,
f.IBAN,
f.LastFourDigits,
&expirationDate)) {
f.Type.Selected = selectedType
return false
}
f.Slug = slug
if expirationDate.Status == pgtype.Present {
f.ExpirationMonthYear.Val = expirationDate.Time.Format(ExpirationDateFormat)
} else {
f.ExpirationMonthYear.Val = ""
}
return true
}
func (f *PaymentAccountForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
f.Type.FillValue(r)
f.Name.FillValue(r)
f.IBAN.FillValue(r)
f.LastFourDigits.FillValue(r)
f.ExpirationMonthYear.FillValue(r)
return nil
}
func (f *PaymentAccountForm) Validate(ctx context.Context, conn *Conn) bool {
validator := newFormValidator()
if validator.CheckValidRadioOption(f.Type, gettext("Selected payment account type is not valid.", f.locale)) {
switch f.Type.Selected {
case AccountTypeBank:
if validator.CheckRequiredInput(f.IBAN, gettext("IBAN can not be empty.", f.locale)) {
validator.CheckValidIBANInput(ctx, conn, f.IBAN, gettext("This value is not a valid IBAN.", f.locale))
}
case AccountTypeCard:
if validator.CheckRequiredInput(f.LastFourDigits, gettext("Last four digits can not be empty.", f.locale)) {
if validator.CheckInputLength(f.LastFourDigits, 4, gettext("You must enter the cards last four digits", f.locale)) {
validator.CheckValidInteger(f.LastFourDigits, 0, 9999, gettext("Last four digits must be a number.", f.locale))
}
}
if validator.CheckRequiredInput(f.ExpirationMonthYear, gettext("Expiration date can not be empty.", f.locale)) {
_, err := time.Parse(ExpirationDateFormat, f.ExpirationMonthYear.Val)
validator.checkInput(f.ExpirationMonthYear, err == nil, gettext("Expiration date should be a valid date in format MM/YY (e.g., 08/24).", f.locale))
}
}
}
validator.CheckRequiredInput(f.Name, gettext("Payment account name can not be empty.", f.locale))
return validator.AllOK()
}
func (f *PaymentAccountForm) ExpirationDate() (time.Time, error) {
date, err := time.Parse(ExpirationDateFormat, f.ExpirationMonthYear.Val)
if err != nil {
return date, err
}
return date.AddDate(0, 1, -1), nil
}
func handleAddPaymentAccount(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
form := newPaymentAccountForm(r.Context(), conn, locale, company)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !form.Validate(r.Context(), conn) {
if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
form.MustRender(w, r)
return
}
switch form.Type.Selected {
case AccountTypeBank:
conn.MustExec(r.Context(), "select add_payment_account_bank($1, $2, $3)", company.Id, form.Name, form.IBAN)
case AccountTypeCard:
date, err := form.ExpirationDate()
if err != nil {
panic(err)
}
conn.MustExec(r.Context(), "select add_payment_account_card($1, $2, $3, $4)", company.Id, form.Name, form.LastFourDigits, date)
case AccountTypeCash:
conn.MustExec(r.Context(), "select add_payment_account_cash($1, $2)", company.Id, form.Name)
case AccountTypeOther:
conn.MustExec(r.Context(), "select add_payment_account_other($1, $2)", company.Id, form.Name)
}
htmxRedirect(w, r, companyURI(company, "/payment-accounts"))
}
func handleEditPaymentAccount(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
company := mustGetCompany(r)
form := newPaymentAccountForm(r.Context(), conn, locale, company)
form.Slug = params[0].Value
if !ValidUuid(form.Slug) {
http.NotFound(w, r)
return
}
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !form.Validate(r.Context(), conn) {
if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
form.MustRender(w, r)
return
}
var found string
switch form.Type.Selected {
case AccountTypeBank:
found = conn.MustGetText(r.Context(), "", "select edit_payment_account_bank($1, $2, $3)", form.Slug, form.Name, form.IBAN)
case AccountTypeCard:
date, err := form.ExpirationDate()
if err != nil {
panic(err)
}
found = conn.MustGetText(r.Context(), "", "select edit_payment_account_card($1, $2, $3, $4)", form.Slug, form.Name, form.LastFourDigits, date)
case AccountTypeCash:
found = conn.MustGetText(r.Context(), "", "select edit_payment_account_cash($1, $2)", form.Slug, form.Name)
case AccountTypeOther:
found = conn.MustGetText(r.Context(), "", "select edit_payment_account_other($1, $2)", form.Slug, form.Name)
}
if found == "" {
http.NotFound(w, r)
return
}
htmxRedirect(w, r, companyURI(company, "/payment-accounts"))
}

View File

@ -295,6 +295,33 @@ func (field *RadioField) isValidOption(selected string) bool {
return field.FindOption(selected) != nil
}
func (field *RadioField) HasValidOption() bool {
return field.isValidOption(field.Selected)
}
func MustGetRadioOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*RadioOption {
rows, err := conn.Query(ctx, sql, args...)
if err != nil {
panic(err)
}
defer rows.Close()
var options []*RadioOption
for rows.Next() {
option := &RadioOption{}
err = rows.Scan(&option.Value, &option.Label)
if err != nil {
panic(err)
}
options = append(options, option)
}
if rows.Err() != nil {
panic(rows.Err())
}
return options
}
type CheckField struct {
Name string
Label string
@ -452,6 +479,10 @@ func (v *FormValidator) CheckInputMinLength(field *InputField, min int, message
return v.checkInput(field, len(field.Val) >= min, message)
}
func (v *FormValidator) CheckInputLength(field *InputField, length int, message string) bool {
return v.checkInput(field, len(field.Val) == length, message)
}
func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool {
_, err := mail.ParseAddress(field.Val)
return v.checkInput(field, err == nil, message)
@ -481,6 +512,10 @@ func (v *FormValidator) CheckValidSelectOption(field *SelectField, message strin
return v.checkSelect(field, field.HasValidOptions(), message)
}
func (v *FormValidator) CheckValidRadioOption(field *RadioField, message string) bool {
return v.checkRadio(field, field.HasValidOption(), message)
}
func (v *FormValidator) CheckAtMostOneOfEachGroup(field *SelectField, message string) bool {
repeated := false
groups := map[string]bool{}
@ -539,3 +574,11 @@ func (v *FormValidator) checkSelect(field *SelectField, ok bool, message string)
}
return ok
}
func (v *FormValidator) checkRadio(field *RadioField, ok bool, message string) bool {
if !ok {
field.Errors = append(field.Errors, errors.New(message))
v.Valid = false
}
return ok
}

261
pkg/payments.go Normal file
View File

@ -0,0 +1,261 @@
package pkg
import (
"context"
"fmt"
"github.com/julienschmidt/httprouter"
"html/template"
"math"
"net/http"
"time"
)
func servePaymentIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
page := NewPaymentIndexPage(r.Context(), conn, locale)
page.MustRender(w, r)
}
type PaymentIndexPage struct {
Payments []*PaymentEntry
}
func NewPaymentIndexPage(ctx context.Context, conn *Conn, locale *Locale) *PaymentIndexPage {
return &PaymentIndexPage{
Payments: mustCollectPaymentEntries(ctx, conn, locale),
}
}
func (page *PaymentIndexPage) MustRender(w http.ResponseWriter, r *http.Request) {
mustRenderMainTemplate(w, r, "payments/index.gohtml", page)
}
type PaymentEntry struct {
ID int
Slug string
PaymentDate time.Time
Description string
Total string
Tags []string
Status string
StatusLabel string
}
func mustCollectPaymentEntries(ctx context.Context, conn *Conn, locale *Locale) []*PaymentEntry {
rows := conn.MustQuery(ctx, `
select payment_id
, payment.slug
, payment_date
, description
, to_price(payment.amount, decimal_digits) as total
, payment.tags
, payment.payment_status
, psi18n.name
from payment
join payment_status_i18n psi18n on payment.payment_status = psi18n.payment_status and psi18n.lang_tag = $1
join currency using (currency_code)
order by payment_date desc, total desc
`, locale.Language)
defer rows.Close()
var entries []*PaymentEntry
for rows.Next() {
entry := &PaymentEntry{}
if err := rows.Scan(&entry.ID, &entry.Slug, &entry.PaymentDate, &entry.Description, &entry.Total, &entry.Tags, &entry.Status, &entry.StatusLabel); err != nil {
panic(err)
}
entries = append(entries, entry)
}
if rows.Err() != nil {
panic(rows.Err())
}
return entries
}
func servePaymentForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
form := newPaymentForm(r.Context(), conn, locale, company)
slug := params[0].Value
if slug == "new" {
form.PaymentDate.Val = time.Now().Format("2006-01-02")
form.MustRender(w, r)
return
}
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
http.NotFound(w, r)
return
}
form.MustRender(w, r)
}
type PaymentForm struct {
locale *Locale
company *Company
Slug string
Description *InputField
PaymentDate *InputField
PaymentAccount *SelectField
Amount *InputField
Tags *TagsField
}
func newPaymentForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *PaymentForm {
return &PaymentForm{
locale: locale,
company: company,
Description: &InputField{
Name: "description",
Label: pgettext("input", "Description", locale),
Required: true,
Type: "text",
},
PaymentDate: &InputField{
Name: "payment_date",
Label: pgettext("input", "Invoice Date", locale),
Required: true,
Type: "date",
},
PaymentAccount: &SelectField{
Name: "payment_account",
Label: pgettext("input", "Account", locale),
Required: true,
Options: MustGetOptions(ctx, conn, "select payment_account_id::text, name from payment_account order by name"),
},
Amount: &InputField{
Name: "amount",
Label: pgettext("input", "Amount", locale),
Type: "number",
Required: true,
Attributes: []template.HTMLAttr{
`min="0"`,
template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
},
},
Tags: &TagsField{
Name: "tags",
Label: pgettext("input", "Tags", locale),
},
}
}
func (f *PaymentForm) MustRender(w http.ResponseWriter, r *http.Request) {
if f.Slug == "" {
f.PaymentAccount.EmptyLabel = gettext("Select an account.", f.locale)
mustRenderMainTemplate(w, r, "payments/new.gohtml", f)
} else {
mustRenderMainTemplate(w, r, "payments/edit.gohtml", f)
}
}
func (f *PaymentForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
selectedPaymentAccount := f.PaymentAccount.Selected
f.PaymentAccount.Clear()
if notFoundErrorOrPanic(conn.QueryRow(ctx, `
select description
, payment_date
, payment_account_id::text
, to_price(amount, decimal_digits)
, tags
from payment
join currency using (currency_code)
where payment.slug = $1
`, slug).Scan(
f.Description,
f.PaymentDate,
f.PaymentAccount,
f.Amount,
f.Tags)) {
f.PaymentAccount.Selected = selectedPaymentAccount
return false
}
f.Slug = slug
return true
}
func (f *PaymentForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
f.Description.FillValue(r)
f.PaymentDate.FillValue(r)
f.PaymentAccount.FillValue(r)
f.Amount.FillValue(r)
f.Tags.FillValue(r)
return nil
}
func (f *PaymentForm) Validate() bool {
validator := newFormValidator()
validator.CheckRequiredInput(f.Description, gettext("Description can not be empty.", f.locale))
validator.CheckValidSelectOption(f.PaymentAccount, gettext("Selected payment account is not valid.", f.locale))
validator.CheckValidDate(f.PaymentDate, gettext("Payment date must be a valid date.", f.locale))
if validator.CheckRequiredInput(f.Amount, gettext("Amount can not be empty.", f.locale)) {
validator.CheckValidDecimal(f.Amount, f.company.MinCents(), math.MaxFloat64, gettext("Amount must be a number greater than zero.", f.locale))
}
return validator.AllOK()
}
func handleAddPayment(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
form := newPaymentForm(r.Context(), conn, locale, company)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !form.Validate() {
if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
form.MustRender(w, r)
return
}
conn.MustExec(r.Context(), "select add_payment($1, $2, $3, $4, $5, $6, $7)", company.Id, nil, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags)
htmxRedirect(w, r, companyURI(company, "/payments"))
}
func handleEditPayment(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
company := mustGetCompany(r)
form := newPaymentForm(r.Context(), conn, locale, company)
form.Slug = params[0].Value
if !ValidUuid(form.Slug) {
http.NotFound(w, r)
return
}
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !form.Validate() {
if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
form.MustRender(w, r)
return
}
if found := conn.MustGetText(r.Context(), "", "select edit_payment($1, $2, $3, $4, $5, $6)", form.Slug, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags); found == "" {
http.NotFound(w, r)
return
}
htmxRedirect(w, r, companyURI(company, "/payments"))
}

View File

@ -58,6 +58,14 @@ func NewRouter(db *Db, demo bool) http.Handler {
companyRouter.PUT("/expenses/:slug/tags", HandleUpdateExpenseTags)
companyRouter.GET("/expenses/:slug/tags/edit", ServeEditExpenseTags)
companyRouter.GET("/expenses/:slug/download/:filename", ServeExpenseAttachment)
companyRouter.GET("/payments", servePaymentIndex)
companyRouter.POST("/payments", handleAddPayment)
companyRouter.GET("/payments/:slug", servePaymentForm)
companyRouter.PUT("/payments/:slug", handleEditPayment)
companyRouter.GET("/payment-accounts", servePaymentAccountIndex)
companyRouter.POST("/payment-accounts", handleAddPaymentAccount)
companyRouter.GET("/payment-accounts/:slug", servePaymentAccountForm)
companyRouter.PUT("/payment-accounts/:slug", handleEditPaymentAccount)
companyRouter.GET("/", ServeDashboard)
router := httprouter.New()

270
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-07-20 22:51+0200\n"
"POT-Creation-Date: 2024-08-10 04:08+0200\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n"
@ -35,6 +35,11 @@ msgstr "Afegeix productes a la factura"
#: web/template/expenses/index.gohtml:10 web/template/expenses/edit.gohtml:10
#: web/template/tax-details.gohtml:9 web/template/products/new.gohtml:9
#: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10
#: web/template/payments/new.gohtml:10 web/template/payments/index.gohtml:10
#: web/template/payments/edit.gohtml:10
#: web/template/payments/accounts/new.gohtml:10
#: web/template/payments/accounts/index.gohtml:10
#: web/template/payments/accounts/edit.gohtml:10
msgctxt "title"
msgid "Home"
msgstr "Inici"
@ -61,6 +66,7 @@ msgstr "Tots"
#: web/template/invoices/products.gohtml:49
#: web/template/switch-company.gohtml:22 web/template/quotes/products.gohtml:49
#: web/template/products/index.gohtml:45
#: web/template/payments/accounts/index.gohtml:25
msgctxt "title"
msgid "Name"
msgstr "Nom"
@ -107,7 +113,7 @@ msgstr "Subtotal"
#: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82
#: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75
#: web/template/expenses/new.gohtml:47 web/template/expenses/index.gohtml:74
#: web/template/expenses/edit.gohtml:49
#: web/template/expenses/edit.gohtml:49 web/template/payments/index.gohtml:29
msgctxt "title"
msgid "Total"
msgstr "Total"
@ -115,6 +121,8 @@ msgstr "Total"
#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93
#: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93
#: web/template/expenses/new.gohtml:57 web/template/expenses/edit.gohtml:59
#: web/template/payments/edit.gohtml:35
#: web/template/payments/accounts/edit.gohtml:38
msgctxt "action"
msgid "Update"
msgstr "Actualitza"
@ -124,6 +132,8 @@ msgstr "Actualitza"
#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53
#: web/template/expenses/new.gohtml:60 web/template/expenses/edit.gohtml:62
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36
#: web/template/payments/new.gohtml:33
#: web/template/payments/accounts/new.gohtml:41
msgctxt "action"
msgid "Save"
msgstr "Desa"
@ -180,14 +190,14 @@ msgid "Customer"
msgstr "Client"
#: web/template/invoices/index.gohtml:70 web/template/quotes/index.gohtml:70
#: web/template/expenses/index.gohtml:68
#: web/template/expenses/index.gohtml:68 web/template/payments/index.gohtml:27
msgctxt "title"
msgid "Status"
msgstr "Estat"
#: web/template/invoices/index.gohtml:71 web/template/quotes/index.gohtml:71
#: web/template/contacts/index.gohtml:50 web/template/expenses/index.gohtml:69
#: web/template/products/index.gohtml:46
#: web/template/products/index.gohtml:46 web/template/payments/index.gohtml:28
msgctxt "title"
msgid "Tags"
msgstr "Etiquetes"
@ -326,7 +336,7 @@ msgctxt "input"
msgid "(Max. %s)"
msgstr "(Màx. %s)"
#: web/template/form.gohtml:200
#: web/template/form.gohtml:202
msgctxt "action"
msgid "Filters"
msgstr "Filtra"
@ -489,15 +499,20 @@ msgstr "Despeses"
#: web/template/app.gohtml:57
msgctxt "nav"
msgid "Payments"
msgstr "Pagaments"
#: web/template/app.gohtml:58
msgctxt "nav"
msgid "Products"
msgstr "Productes"
#: web/template/app.gohtml:58
#: web/template/app.gohtml:59
msgctxt "nav"
msgid "Contacts"
msgstr "Contactes"
#: web/template/app.gohtml:66
#: web/template/app.gohtml:67
msgid "<a href=\"https://numerus.cat/\">Numerus</a> Version: %s"
msgstr "<a href=\"https://numerus.cat/\">Numerus</a> versió: %s"
@ -731,6 +746,84 @@ msgctxt "title"
msgid "Edit Product “%s”"
msgstr "Edició del producte «%s»"
#: web/template/payments/new.gohtml:3 web/template/payments/new.gohtml:12
msgctxt "title"
msgid "New Payment"
msgstr "Nou pagament"
#: web/template/payments/new.gohtml:11 web/template/payments/index.gohtml:3
#: web/template/payments/index.gohtml:11 web/template/payments/edit.gohtml:11
msgctxt "title"
msgid "Payments"
msgstr "Pagaments"
#: web/template/payments/index.gohtml:16
msgctxt "action"
msgid "New payment"
msgstr "Nou pagament"
#: web/template/payments/index.gohtml:25
msgctxt "title"
msgid "Payment Date"
msgstr "Data del pagament"
#: web/template/payments/index.gohtml:26
msgctxt "title"
msgid "Description"
msgstr "Descripció"
#: web/template/payments/index.gohtml:54
msgid "No payments added yet."
msgstr "No hi ha cap pagament."
#: web/template/payments/edit.gohtml:3
msgctxt "title"
msgid "Edit Payment “%s”"
msgstr "Edició del pagament «%s»"
#: web/template/payments/accounts/new.gohtml:3
#: web/template/payments/accounts/new.gohtml:12
msgctxt "title"
msgid "New Payment Account"
msgstr "Nou compte de pagament"
#: web/template/payments/accounts/new.gohtml:11
#: web/template/payments/accounts/index.gohtml:3
#: web/template/payments/accounts/index.gohtml:11
#: web/template/payments/accounts/edit.gohtml:11
msgctxt "title"
msgid "Payment Accounts"
msgstr "Comptes de pagament"
#: web/template/payments/accounts/index.gohtml:16
msgctxt "action"
msgid "New payment account"
msgstr "Nou compte de pagament"
#: web/template/payments/accounts/index.gohtml:26
msgctxt "title"
msgid "Type"
msgstr "Tipus"
#: web/template/payments/accounts/index.gohtml:27
msgctxt "title"
msgid "Number"
msgstr "Número"
#: web/template/payments/accounts/index.gohtml:28
msgctxt "title"
msgid "Expiration Date"
msgstr "Data de caducitat"
#: web/template/payments/accounts/index.gohtml:51
msgid "No payment accounts added yet."
msgstr "No hi ha cap compte de pagament."
#: web/template/payments/accounts/edit.gohtml:3
msgctxt "title"
msgid "Edit Payment Account “%s”"
msgstr "Edició del compte de pagament «%s»"
#: pkg/ods.go:62 pkg/ods.go:106
msgctxt "title"
msgid "VAT number"
@ -762,49 +855,50 @@ msgstr "No podeu deixar la contrasenya en blanc."
msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901
#: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 pkg/accounts.go:138
#: pkg/invoices.go:1147 pkg/contacts.go:149 pkg/contacts.go:262
msgctxt "input"
msgid "Name"
msgstr "Nom"
#: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708
#: pkg/expenses.go:343 pkg/expenses.go:511 pkg/invoices.go:177
#: pkg/invoices.go:877 pkg/invoices.go:1462 pkg/contacts.go:154
#: pkg/contacts.go:362
#: pkg/payments.go:145 pkg/expenses.go:343 pkg/expenses.go:510
#: pkg/invoices.go:177 pkg/invoices.go:877 pkg/invoices.go:1462
#: pkg/contacts.go:154 pkg/contacts.go:362
msgctxt "input"
msgid "Tags"
msgstr "Etiquetes"
#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:521 pkg/invoices.go:181
#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:520 pkg/invoices.go:181
#: pkg/contacts.go:158
msgctxt "input"
msgid "Tags Condition"
msgstr "Condició de les etiquetes"
#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:525 pkg/invoices.go:185
#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:524 pkg/invoices.go:185
#: pkg/contacts.go:162
msgctxt "tag condition"
msgid "All"
msgstr "Totes"
#: pkg/products.go:186 pkg/expenses.go:526 pkg/invoices.go:186
#: pkg/products.go:186 pkg/expenses.go:525 pkg/invoices.go:186
#: pkg/contacts.go:163
msgid "Invoices must have all the specified labels."
msgstr "Les factures han de tenir totes les etiquetes."
#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:530 pkg/invoices.go:190
#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:529 pkg/invoices.go:190
#: pkg/contacts.go:167
msgctxt "tag condition"
msgid "Any"
msgstr "Qualsevol"
#: pkg/products.go:191 pkg/expenses.go:531 pkg/invoices.go:191
#: pkg/products.go:191 pkg/expenses.go:530 pkg/invoices.go:191
#: pkg/contacts.go:168
msgid "Invoices must have at least one of the specified labels."
msgstr "Les factures han de tenir com a mínim una de les etiquetes."
#: pkg/products.go:282 pkg/quote.go:915 pkg/invoices.go:1161
#: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:117
#: pkg/invoices.go:1161
msgctxt "input"
msgid "Description"
msgstr "Descripció"
@ -1062,7 +1156,7 @@ msgctxt "input"
msgid "Quotation Status"
msgstr "Estat del pressupost"
#: pkg/quote.go:154 pkg/expenses.go:516 pkg/invoices.go:157
#: pkg/quote.go:154 pkg/expenses.go:515 pkg/invoices.go:157
msgid "All status"
msgstr "Tots els estats"
@ -1071,12 +1165,12 @@ msgctxt "input"
msgid "Quotation Number"
msgstr "Número de pressupost"
#: pkg/quote.go:164 pkg/expenses.go:501 pkg/invoices.go:167
#: pkg/quote.go:164 pkg/expenses.go:500 pkg/invoices.go:167
msgctxt "input"
msgid "From Date"
msgstr "A partir de la data"
#: pkg/quote.go:169 pkg/expenses.go:506 pkg/invoices.go:172
#: pkg/quote.go:169 pkg/expenses.go:505 pkg/invoices.go:172
msgctxt "input"
msgid "To Date"
msgstr "Fins la data"
@ -1097,8 +1191,8 @@ msgstr "pressuposts.zip"
msgid "quotations.ods"
msgstr "pressuposts.ods"
#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:720
#: pkg/expenses.go:750 pkg/invoices.go:684 pkg/invoices.go:1437
#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:719
#: pkg/expenses.go:749 pkg/invoices.go:684 pkg/invoices.go:1437
#: pkg/invoices.go:1445
msgid "Invalid action"
msgstr "Acció invàlida."
@ -1218,6 +1312,101 @@ msgstr "La confirmació no és igual a la contrasenya."
msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid."
#: pkg/payments.go:123 pkg/expenses.go:305 pkg/invoices.go:866
msgctxt "input"
msgid "Invoice Date"
msgstr "Data de factura"
#: pkg/payments.go:129
msgctxt "input"
msgid "Account"
msgstr "Compte"
#: pkg/payments.go:135 pkg/expenses.go:320
msgctxt "input"
msgid "Amount"
msgstr "Import"
#: pkg/payments.go:152
msgid "Select an account."
msgstr "Escolliu un compte."
#: pkg/payments.go:198
msgid "Description can not be empty."
msgstr "No podeu deixar la descripció en blanc."
#: pkg/payments.go:199
msgid "Selected payment account is not valid."
msgstr "Heu seleccionat un compte de pagament que no és vàlid."
#: pkg/payments.go:200
msgid "Payment date must be a valid date."
msgstr "La data de pagament ha de ser vàlida."
#: pkg/payments.go:201 pkg/expenses.go:381
msgid "Amount can not be empty."
msgstr "No podeu deixar limport en blanc."
#: pkg/payments.go:202 pkg/expenses.go:382
msgid "Amount must be a number greater than zero."
msgstr "Limport ha de ser un número major a zero."
#: pkg/accounts.go:129
msgctxt "input"
msgid "Type"
msgstr "Tipus"
#: pkg/accounts.go:144 pkg/contacts.go:352
msgctxt "input"
msgid "IBAN"
msgstr "IBAN"
#: pkg/accounts.go:150
msgctxt "input"
msgid "Cards last four digits"
msgstr "Els quatre darrers dígits de la targeta"
#: pkg/accounts.go:161
msgctxt "input"
msgid "Expiration date"
msgstr "Data de caducitat"
#: pkg/accounts.go:227
msgid "Selected payment account type is not valid."
msgstr "Heu seleccionat un tipus de compte de pagament que no és vàlid."
#: pkg/accounts.go:230
msgid "IBAN can not be empty."
msgstr "No podeu deixar lIBAN en blanc."
#: pkg/accounts.go:231
msgid "This value is not a valid IBAN."
msgstr "Aquest valor no és un IBAN vàlid."
#: pkg/accounts.go:234
msgid "Last four digits can not be empty."
msgstr "No podeu deixar el quatre darrers dígits en blanc."
#: pkg/accounts.go:235
msgid "You must enter the cards last four digits"
msgstr "Heu dentrar els quatre darrers dígits de la targeta"
#: pkg/accounts.go:236
msgid "Last four digits must be a number."
msgstr "El quatre darrera dígits han de ser un número."
#: pkg/accounts.go:239
msgid "Expiration date can not be empty."
msgstr "No podeu deixar la data de pagament en blanc."
#: pkg/accounts.go:241
msgid "Expiration date should be a valid date in format MM/YY (e.g., 08/24)."
msgstr "La data de caducitat has de ser vàlida i en format MM/AA (p. ex., 08/24)."
#: pkg/accounts.go:245
msgid "Payment account name can not be empty."
msgstr "No podeu deixar el nom del compte de pagament en blanc."
#: pkg/dashboard.go:138
msgctxt "input"
msgid "Period"
@ -1257,7 +1446,7 @@ msgstr "Any anterior"
msgid "Select a contact."
msgstr "Escolliu un contacte."
#: pkg/expenses.go:294 pkg/expenses.go:490
#: pkg/expenses.go:294 pkg/expenses.go:489
msgctxt "input"
msgid "Contact"
msgstr "Contacte"
@ -1267,22 +1456,12 @@ msgctxt "input"
msgid "Invoice number"
msgstr "Número de factura"
#: pkg/expenses.go:305 pkg/invoices.go:866
msgctxt "input"
msgid "Invoice Date"
msgstr "Data de factura"
#: pkg/expenses.go:320
msgctxt "input"
msgid "Amount"
msgstr "Import"
#: pkg/expenses.go:331 pkg/invoices.go:888
msgctxt "input"
msgid "File"
msgstr "Fitxer"
#: pkg/expenses.go:337 pkg/expenses.go:515
#: pkg/expenses.go:337 pkg/expenses.go:514
msgctxt "input"
msgid "Expense Status"
msgstr "Estat de la despesa"
@ -1295,28 +1474,20 @@ msgstr "Heu seleccionat un contacte que no és vàlid."
msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida."
#: pkg/expenses.go:381
msgid "Amount can not be empty."
msgstr "No podeu deixar limport en blanc."
#: pkg/expenses.go:382
msgid "Amount must be a number greater than zero."
msgstr "Limport ha de ser un número major a zero."
#: pkg/expenses.go:384
msgid "Selected expense status is not valid."
msgstr "Heu seleccionat un estat de despesa que no és vàlid."
#: pkg/expenses.go:491
#: pkg/expenses.go:490
msgid "All contacts"
msgstr "Tots els contactes"
#: pkg/expenses.go:496 pkg/invoices.go:162
#: pkg/expenses.go:495 pkg/invoices.go:162
msgctxt "input"
msgid "Invoice Number"
msgstr "Número de factura"
#: pkg/expenses.go:748
#: pkg/expenses.go:747
msgid "expenses.ods"
msgstr "despeses.ods"
@ -1364,11 +1535,6 @@ msgctxt "input"
msgid "Need to input tax details"
msgstr "Necessito poder facturar aquest contacte"
#: pkg/contacts.go:352
msgctxt "input"
msgid "IBAN"
msgstr "IBAN"
#: pkg/contacts.go:357
msgctxt "bic"
msgid "BIC"
@ -1445,10 +1611,6 @@ msgstr "Fitxer Excel del Holded"
#~ msgid "Product ID can not be empty."
#~ msgstr "No podeu deixar lidentificador del producte en blanc."
#~ msgctxt "input"
#~ msgid "Number"
#~ msgstr "Número"
#~ msgctxt "title"
#~ msgid "Label"
#~ msgstr "Etiqueta"

270
po/es.po
View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-07-20 22:51+0200\n"
"POT-Creation-Date: 2024-08-10 04:08+0200\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
@ -35,6 +35,11 @@ msgstr "Añadir productos a la factura"
#: web/template/expenses/index.gohtml:10 web/template/expenses/edit.gohtml:10
#: web/template/tax-details.gohtml:9 web/template/products/new.gohtml:9
#: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10
#: web/template/payments/new.gohtml:10 web/template/payments/index.gohtml:10
#: web/template/payments/edit.gohtml:10
#: web/template/payments/accounts/new.gohtml:10
#: web/template/payments/accounts/index.gohtml:10
#: web/template/payments/accounts/edit.gohtml:10
msgctxt "title"
msgid "Home"
msgstr "Inicio"
@ -61,6 +66,7 @@ msgstr "Todos"
#: web/template/invoices/products.gohtml:49
#: web/template/switch-company.gohtml:22 web/template/quotes/products.gohtml:49
#: web/template/products/index.gohtml:45
#: web/template/payments/accounts/index.gohtml:25
msgctxt "title"
msgid "Name"
msgstr "Nombre"
@ -107,7 +113,7 @@ msgstr "Subtotal"
#: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82
#: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75
#: web/template/expenses/new.gohtml:47 web/template/expenses/index.gohtml:74
#: web/template/expenses/edit.gohtml:49
#: web/template/expenses/edit.gohtml:49 web/template/payments/index.gohtml:29
msgctxt "title"
msgid "Total"
msgstr "Total"
@ -115,6 +121,8 @@ msgstr "Total"
#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93
#: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93
#: web/template/expenses/new.gohtml:57 web/template/expenses/edit.gohtml:59
#: web/template/payments/edit.gohtml:35
#: web/template/payments/accounts/edit.gohtml:38
msgctxt "action"
msgid "Update"
msgstr "Actualizar"
@ -124,6 +132,8 @@ msgstr "Actualizar"
#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53
#: web/template/expenses/new.gohtml:60 web/template/expenses/edit.gohtml:62
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36
#: web/template/payments/new.gohtml:33
#: web/template/payments/accounts/new.gohtml:41
msgctxt "action"
msgid "Save"
msgstr "Guardad"
@ -180,14 +190,14 @@ msgid "Customer"
msgstr "Cliente"
#: web/template/invoices/index.gohtml:70 web/template/quotes/index.gohtml:70
#: web/template/expenses/index.gohtml:68
#: web/template/expenses/index.gohtml:68 web/template/payments/index.gohtml:27
msgctxt "title"
msgid "Status"
msgstr "Estado"
#: web/template/invoices/index.gohtml:71 web/template/quotes/index.gohtml:71
#: web/template/contacts/index.gohtml:50 web/template/expenses/index.gohtml:69
#: web/template/products/index.gohtml:46
#: web/template/products/index.gohtml:46 web/template/payments/index.gohtml:28
msgctxt "title"
msgid "Tags"
msgstr "Etiquetes"
@ -326,7 +336,7 @@ msgctxt "input"
msgid "(Max. %s)"
msgstr "(Máx. %s)"
#: web/template/form.gohtml:200
#: web/template/form.gohtml:202
msgctxt "action"
msgid "Filters"
msgstr "Filtrar"
@ -489,15 +499,20 @@ msgstr "Gastos"
#: web/template/app.gohtml:57
msgctxt "nav"
msgid "Payments"
msgstr "Pagos"
#: web/template/app.gohtml:58
msgctxt "nav"
msgid "Products"
msgstr "Productos"
#: web/template/app.gohtml:58
#: web/template/app.gohtml:59
msgctxt "nav"
msgid "Contacts"
msgstr "Contactos"
#: web/template/app.gohtml:66
#: web/template/app.gohtml:67
msgid "<a href=\"https://numerus.cat/\">Numerus</a> Version: %s"
msgstr "<a href=\"https://numerus.cat/\">Numerus</a> versión: %s"
@ -731,6 +746,84 @@ msgctxt "title"
msgid "Edit Product “%s”"
msgstr "Edición del producto «%s»"
#: web/template/payments/new.gohtml:3 web/template/payments/new.gohtml:12
msgctxt "title"
msgid "New Payment"
msgstr "Nuevo pago"
#: web/template/payments/new.gohtml:11 web/template/payments/index.gohtml:3
#: web/template/payments/index.gohtml:11 web/template/payments/edit.gohtml:11
msgctxt "title"
msgid "Payments"
msgstr "Pagos"
#: web/template/payments/index.gohtml:16
msgctxt "action"
msgid "New payment"
msgstr "Nuevo pago"
#: web/template/payments/index.gohtml:25
msgctxt "title"
msgid "Payment Date"
msgstr "Fecha del pago"
#: web/template/payments/index.gohtml:26
msgctxt "title"
msgid "Description"
msgstr "Descripción"
#: web/template/payments/index.gohtml:54
msgid "No payments added yet."
msgstr "No hay pagos."
#: web/template/payments/edit.gohtml:3
msgctxt "title"
msgid "Edit Payment “%s”"
msgstr "Edición del pago «%s»"
#: web/template/payments/accounts/new.gohtml:3
#: web/template/payments/accounts/new.gohtml:12
msgctxt "title"
msgid "New Payment Account"
msgstr "Nueva cuenta de pago"
#: web/template/payments/accounts/new.gohtml:11
#: web/template/payments/accounts/index.gohtml:3
#: web/template/payments/accounts/index.gohtml:11
#: web/template/payments/accounts/edit.gohtml:11
msgctxt "title"
msgid "Payment Accounts"
msgstr "Cuenta de pago"
#: web/template/payments/accounts/index.gohtml:16
msgctxt "action"
msgid "New payment account"
msgstr "Nuevo cuenta de pago"
#: web/template/payments/accounts/index.gohtml:26
msgctxt "title"
msgid "Type"
msgstr "Tipo"
#: web/template/payments/accounts/index.gohtml:27
msgctxt "title"
msgid "Number"
msgstr "Número"
#: web/template/payments/accounts/index.gohtml:28
msgctxt "title"
msgid "Expiration Date"
msgstr "Fecha de caducidad"
#: web/template/payments/accounts/index.gohtml:51
msgid "No payment accounts added yet."
msgstr "No hay cuentas de pago."
#: web/template/payments/accounts/edit.gohtml:3
msgctxt "title"
msgid "Edit Payment Account “%s”"
msgstr "Edición de la cuenta de pago «%s»"
#: pkg/ods.go:62 pkg/ods.go:106
msgctxt "title"
msgid "VAT number"
@ -762,49 +855,50 @@ msgstr "No podéis dejar la contraseña en blanco."
msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido."
#: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901
#: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 pkg/accounts.go:138
#: pkg/invoices.go:1147 pkg/contacts.go:149 pkg/contacts.go:262
msgctxt "input"
msgid "Name"
msgstr "Nombre"
#: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708
#: pkg/expenses.go:343 pkg/expenses.go:511 pkg/invoices.go:177
#: pkg/invoices.go:877 pkg/invoices.go:1462 pkg/contacts.go:154
#: pkg/contacts.go:362
#: pkg/payments.go:145 pkg/expenses.go:343 pkg/expenses.go:510
#: pkg/invoices.go:177 pkg/invoices.go:877 pkg/invoices.go:1462
#: pkg/contacts.go:154 pkg/contacts.go:362
msgctxt "input"
msgid "Tags"
msgstr "Etiquetes"
#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:521 pkg/invoices.go:181
#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:520 pkg/invoices.go:181
#: pkg/contacts.go:158
msgctxt "input"
msgid "Tags Condition"
msgstr "Condición de las etiquetas"
#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:525 pkg/invoices.go:185
#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:524 pkg/invoices.go:185
#: pkg/contacts.go:162
msgctxt "tag condition"
msgid "All"
msgstr "Todas"
#: pkg/products.go:186 pkg/expenses.go:526 pkg/invoices.go:186
#: pkg/products.go:186 pkg/expenses.go:525 pkg/invoices.go:186
#: pkg/contacts.go:163
msgid "Invoices must have all the specified labels."
msgstr "Las facturas deben tener todas las etiquetas."
#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:530 pkg/invoices.go:190
#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:529 pkg/invoices.go:190
#: pkg/contacts.go:167
msgctxt "tag condition"
msgid "Any"
msgstr "Cualquiera"
#: pkg/products.go:191 pkg/expenses.go:531 pkg/invoices.go:191
#: pkg/products.go:191 pkg/expenses.go:530 pkg/invoices.go:191
#: pkg/contacts.go:168
msgid "Invoices must have at least one of the specified labels."
msgstr "Las facturas deben tener como mínimo una de las etiquetas."
#: pkg/products.go:282 pkg/quote.go:915 pkg/invoices.go:1161
#: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:117
#: pkg/invoices.go:1161
msgctxt "input"
msgid "Description"
msgstr "Descripción"
@ -1062,7 +1156,7 @@ msgctxt "input"
msgid "Quotation Status"
msgstr "Estado del presupuesto"
#: pkg/quote.go:154 pkg/expenses.go:516 pkg/invoices.go:157
#: pkg/quote.go:154 pkg/expenses.go:515 pkg/invoices.go:157
msgid "All status"
msgstr "Todos los estados"
@ -1071,12 +1165,12 @@ msgctxt "input"
msgid "Quotation Number"
msgstr "Número de presupuesto"
#: pkg/quote.go:164 pkg/expenses.go:501 pkg/invoices.go:167
#: pkg/quote.go:164 pkg/expenses.go:500 pkg/invoices.go:167
msgctxt "input"
msgid "From Date"
msgstr "A partir de la fecha"
#: pkg/quote.go:169 pkg/expenses.go:506 pkg/invoices.go:172
#: pkg/quote.go:169 pkg/expenses.go:505 pkg/invoices.go:172
msgctxt "input"
msgid "To Date"
msgstr "Hasta la fecha"
@ -1097,8 +1191,8 @@ msgstr "presupuestos.zip"
msgid "quotations.ods"
msgstr "presupuestos.ods"
#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:720
#: pkg/expenses.go:750 pkg/invoices.go:684 pkg/invoices.go:1437
#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:719
#: pkg/expenses.go:749 pkg/invoices.go:684 pkg/invoices.go:1437
#: pkg/invoices.go:1445
msgid "Invalid action"
msgstr "Acción inválida."
@ -1218,6 +1312,101 @@ msgstr "La confirmación no corresponde con la contraseña."
msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido."
#: pkg/payments.go:123 pkg/expenses.go:305 pkg/invoices.go:866
msgctxt "input"
msgid "Invoice Date"
msgstr "Fecha de factura"
#: pkg/payments.go:129
msgctxt "input"
msgid "Account"
msgstr "Cuenta"
#: pkg/payments.go:135 pkg/expenses.go:320
msgctxt "input"
msgid "Amount"
msgstr "Importe"
#: pkg/payments.go:152
msgid "Select an account."
msgstr "Escoged una cuenta."
#: pkg/payments.go:198
msgid "Description can not be empty."
msgstr "No podéis dejar la descripción en blanco."
#: pkg/payments.go:199
msgid "Selected payment account is not valid."
msgstr "Habéis escogido una cuenta de pago que no es válida."
#: pkg/payments.go:200
msgid "Payment date must be a valid date."
msgstr "La fecha de pago debe ser válida."
#: pkg/payments.go:201 pkg/expenses.go:381
msgid "Amount can not be empty."
msgstr "No podéis dejar el importe en blanco."
#: pkg/payments.go:202 pkg/expenses.go:382
msgid "Amount must be a number greater than zero."
msgstr "El importe tiene que ser un número mayor a cero."
#: pkg/accounts.go:129
msgctxt "input"
msgid "Type"
msgstr "Tipo"
#: pkg/accounts.go:144 pkg/contacts.go:352
msgctxt "input"
msgid "IBAN"
msgstr "IBAN"
#: pkg/accounts.go:150
msgctxt "input"
msgid "Cards last four digits"
msgstr "Últimos cuatro dígitos de la tarjeta"
#: pkg/accounts.go:161
msgctxt "input"
msgid "Expiration date"
msgstr "Fecha de caducidad"
#: pkg/accounts.go:227
msgid "Selected payment account type is not valid."
msgstr "Habéis escogido un tipo de cuenta de pago que no es válido."
#: pkg/accounts.go:230
msgid "IBAN can not be empty."
msgstr "No podéis dejar el IBAN en blanco."
#: pkg/accounts.go:231
msgid "This value is not a valid IBAN."
msgstr "Este valor no es un IBAN válido."
#: pkg/accounts.go:234
msgid "Last four digits can not be empty."
msgstr "No podéis dejar los cuatro últimos dígitos en blanco."
#: pkg/accounts.go:235
msgid "You must enter the cards last four digits"
msgstr "Debéis entrar los cuatro últimos dígitos de la tarjeta"
#: pkg/accounts.go:236
msgid "Last four digits must be a number."
msgstr "Los cuatro últimos dígitos tienen que ser un número."
#: pkg/accounts.go:239
msgid "Expiration date can not be empty."
msgstr "No podéis dejar la fecha de caducidad en blanco."
#: pkg/accounts.go:241
msgid "Expiration date should be a valid date in format MM/YY (e.g., 08/24)."
msgstr "La fecha de caducidad tiene que ser válida y en formato MM/AA (p. ej., 08/24)."
#: pkg/accounts.go:245
msgid "Payment account name can not be empty."
msgstr "No podéis dejar el nombre de la cuenta de pago en blanco."
#: pkg/dashboard.go:138
msgctxt "input"
msgid "Period"
@ -1257,7 +1446,7 @@ msgstr "Año anterior"
msgid "Select a contact."
msgstr "Escoged un contacto"
#: pkg/expenses.go:294 pkg/expenses.go:490
#: pkg/expenses.go:294 pkg/expenses.go:489
msgctxt "input"
msgid "Contact"
msgstr "Contacto"
@ -1267,22 +1456,12 @@ msgctxt "input"
msgid "Invoice number"
msgstr "Número de factura"
#: pkg/expenses.go:305 pkg/invoices.go:866
msgctxt "input"
msgid "Invoice Date"
msgstr "Fecha de factura"
#: pkg/expenses.go:320
msgctxt "input"
msgid "Amount"
msgstr "Importe"
#: pkg/expenses.go:331 pkg/invoices.go:888
msgctxt "input"
msgid "File"
msgstr "Archivo"
#: pkg/expenses.go:337 pkg/expenses.go:515
#: pkg/expenses.go:337 pkg/expenses.go:514
msgctxt "input"
msgid "Expense Status"
msgstr "Estado del gasto"
@ -1295,28 +1474,20 @@ msgstr "Habéis escogido un contacto que no es válido."
msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida."
#: pkg/expenses.go:381
msgid "Amount can not be empty."
msgstr "No podéis dejar el importe en blanco."
#: pkg/expenses.go:382
msgid "Amount must be a number greater than zero."
msgstr "El importe tiene que ser un número mayor a cero."
#: pkg/expenses.go:384
msgid "Selected expense status is not valid."
msgstr "Habéis escogido un estado de gasto que no es válido."
#: pkg/expenses.go:491
#: pkg/expenses.go:490
msgid "All contacts"
msgstr "Todos los contactos"
#: pkg/expenses.go:496 pkg/invoices.go:162
#: pkg/expenses.go:495 pkg/invoices.go:162
msgctxt "input"
msgid "Invoice Number"
msgstr "Número de factura"
#: pkg/expenses.go:748
#: pkg/expenses.go:747
msgid "expenses.ods"
msgstr "gastos.ods"
@ -1364,11 +1535,6 @@ msgctxt "input"
msgid "Need to input tax details"
msgstr "Necesito facturar este contacto"
#: pkg/contacts.go:352
msgctxt "input"
msgid "IBAN"
msgstr "IBAN"
#: pkg/contacts.go:357
msgctxt "bic"
msgid "BIC"
@ -1441,10 +1607,6 @@ msgstr "Archivo Excel de Holded"
#~ msgid "Product ID can not be empty."
#~ msgstr "No podéis dejar el identificador de producto en blanco."
#~ msgctxt "input"
#~ msgid "Number"
#~ msgstr "Número"
#~ msgctxt "title"
#~ msgid "Label"
#~ msgstr "Etiqueta"

7
revert/add_payment.sql Normal file
View File

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

View File

@ -0,0 +1,7 @@
-- Revert numerus:add_payment_account_bank from pg
begin;
drop function if exists numerus.add_payment_account_bank(integer, text, iban);
commit;

View File

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

View File

@ -0,0 +1,7 @@
-- Revert numerus:add_payment_account_cash from pg
begin;
drop function if exists numerus.add_payment_account_cash(integer, text);
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:add_payment_account_other from pg
begin;
drop function if exists numerus.add_payment_account_other(integer, text);
commit;

View File

@ -1,10 +1,13 @@
-- Revert numerus:available_expense_status from pg
-- Deploy numerus:available_expense_status to pg
-- requires: schema_numerus
-- requires: expense_status
-- requires: expense_status_i18n
begin;
set search_path to numerus;
delete from expense_status_i18n;
delete from expense_status;
delete from expense_status_i18n where expense_status = 'partial';
delete from expense_status where expense_status = 'partial';
commit;

View File

@ -0,0 +1,10 @@
-- Revert numerus:available_expense_status from pg
begin;
set search_path to numerus;
delete from expense_status_i18n;
delete from expense_status;
commit;

View File

@ -0,0 +1,10 @@
-- Revert numerus:available_payment_account_types from pg
begin;
set search_path to numerus, public;
delete from payment_account_type_i18n;
delete from payment_account_type;
commit;

View File

@ -0,0 +1,10 @@
-- Revert numerus:available_payment_status from pg
begin;
set search_path to numerus;
delete from payment_status_i18n;
delete from payment_status;
commit;

7
revert/edit_payment.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert numerus:edit_payment from pg
begin;
drop function if exists numerus.edit_payment(uuid, date, integer, text, text, numerus.tag_name[]);
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:edit_payment_account_bank from pg
begin;
drop function if exists numerus.edit_payment_account_bank(uuid, text, iban);
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:edit_payment_account_card from pg
begin;
drop function if exists numerus.edit_payment_account_card(uuid, text, text, date);
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:edit_payment_account_cash from pg
begin;
drop function if exists numerus.edit_payment_account_cash(uuid, text);
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:edit_payment_account_other from pg
begin;
drop function if exists numerus.edit_payment_account_other(uuid, text);
commit;

View File

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

7
revert/payment.sql Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -127,3 +127,28 @@ attach_to_invoice [schema_numerus roles invoice invoice_attachment] 2023-07-12T1
new_expense_amount [schema_numerus] 2023-07-13T17:45:33Z jordi fita mas <jordi@tandem.blog> # Add type to return when computing new expense amounts
compute_new_expense_amount [schema_numerus roles company tax new_expense_amount] 2023-07-13T17:34:12Z jordi fita mas <jordi@tandem.blog> # Add function to compute the taxes and total for a new expense
parse_price [parse_price@v1] 2023-08-25T11:59:54Z jordi fita mas <jordi@tandem.blog> # Throw when subtotal is empty string
@v2 2024-08-04T05:21:18Z jordi fita mas <jordi@tandem.blog> # Tag version 2
payment_account_type [roles schema_numerus] 2024-08-01T23:00:18Z jordi fita mas <jordi@tandem.blog> # Add payment_account_type enum
payment_account_type_i18n [roles schema_numerus payment_account_type language] 2024-08-08T17:05:28Z jordi fita mas <jordi@tandem.blog> # Add relation for payment account types translations
available_payment_account_types [schema_numerus payment_account_type payment_account_type_i18n] 2024-08-08T17:09:24Z jordi fita mas <jordi@tandem.blog> # Add the list of available payment account types
payment_account [roles schema_numerus company payment_account_type extension_pgcrypto] 2024-08-01T23:11:54Z jordi fita mas <jordi@tandem.blog> # Add relation of payment account
payment_account_bank [roles schema_numerus payment_account extension_iban] 2024-08-01T23:40:34Z jordi fita mas <jordi@tandem.blog> # Add relation for payment account bank
payment_account_card [roles schema_numerus payment_account] 2024-08-02T00:10:54Z jordi fita mas <jordi@tandem.blog> # Add relation for payment account card
add_payment_account_bank [roles schema_numerus payment_account payment_account_bank] 2024-08-03T00:24:18Z jordi fita mas <jordi@tandem.blog> # Add function to insert bank payment accounts
edit_payment_account_bank [roles schema_numerus payment_account payment_account_bank extension_pgcrypto extension_iban] 2024-08-10T00:04:03Z jordi fita mas <jordi@tandem.blog> # Add function to update bank payment accounts
add_payment_account_card [roles schema_numerus payment_account payment_account_card] 2024-08-03T00:41:18Z jordi fita mas <jordi@tandem.blog> # Add function to insert card payment accounts
edit_payment_account_card [roles schema_numerus payment_account payment_account_card extension_pgcrypto] 2024-08-10T00:22:37Z jordi fita mas <jordi@tandem.blog> # Add function to update card payment accounts
add_payment_account_cash [roles schema_numerus payment_account] 2024-08-03T00:49:07Z jordi fita mas <jordi@tandem.blog> # Add function to insert cash payment accounts
edit_payment_account_cash [roles schema_numerus payment_account extension_pgcrypto] 2024-08-10T00:34:19Z jordi fita mas <jordi@tandem.blog> # Add function to update cash payment accounts
add_payment_account_other [roles schema_numerus payment_account] 2024-08-03T00:49:15Z jordi fita mas <jordi@tandem.blog> # Add function to insert other payment accounts
edit_payment_account_other [roles schema_numerus payment_account extension_pgcrypto] 2024-08-10T00:41:38Z jordi fita mas <jordi@tandem.blog> # Add function to update other payment accounts
payment_status [roles schema_numerus] 2024-08-04T03:02:06Z jordi fita mas <jordi@tandem.blog> # Add relation of payment status
payment_status_i18n [roles schema_numerus payment_status language] 2024-08-04T03:05:41Z jordi fita mas <jordi@tandem.blog> # Add relation of payment status translated name
available_payment_status [schema_numerus payment_status payment_status_i18n] 2024-08-04T03:08:42Z jordi fita mas <jordi@tandem.blog> # Add the list of available payment status
payment [roles schema_numerus company payment_account currency tag_name payment_status extension_pgcrypto] 2024-08-01T01:28:59Z jordi fita mas <jordi@tandem.blog> # Add relation for accounts payable
expense_payment [roles schema_numerus expense payment] 2024-08-04T03:44:30Z jordi fita mas <jordi@tandem.blog> # Add relation of expense payments
available_expense_status [available_expense_status@v2] 2024-08-04T05:24:08Z jordi fita mas <jordi@tandem.blog> # Add “partial” expense status
update_expense_payment_status [roles schema_numerus expense payment expense_payment available_expense_status available_payment_status] 2024-08-04T06:36:00Z jordi fita mas <jordi@tandem.blog> # Add function to update payment and expense status
add_payment [roles schema_numerus payment expense_payment company currency parse_price tag_name update_expense_payment_status] 2024-08-04T03:16:55Z jordi fita mas <jordi@tandem.blog> # Add function to insert new payments
edit_payment [roles schema_numerus payment expense_payment currency parse_price tag_name update_expense_payment_status] 2024-08-04T03:31:45Z jordi fita mas <jordi@tandem.blog> # Add function to update payments

125
test/add_payment.sql Normal file
View File

@ -0,0 +1,125 @@
-- Test add_payment
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(17);
set search_path to numerus, auth, public;
select has_function('numerus', 'add_payment', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]']);
select function_lang_is('numerus', 'add_payment', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'plpgsql');
select function_returns('numerus', 'add_payment', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'uuid');
select isnt_definer('numerus', 'add_payment', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]']);
select volatility_is('numerus', 'add_payment', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'volatile');
select function_privs_are('numerus', 'add_payment', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_payment', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_payment', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_payment', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate expense_payment cascade;
truncate payment cascade;
truncate payment_account cascade;
truncate expense cascade;
truncate contact cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into contact (contact_id, company_id, name)
values ( 9, 1, 'Customer 1')
, (10, 2, 'Customer 2')
;
insert into expense (expense_id, company_id, invoice_number, contact_id, invoice_date, amount, currency_code)
values (12, 1, 'REF123', 9, '2011-01-11', 111, 'EUR')
, (13, 2, 'INV001', 10, '2011-01-11', 111, 'USD')
, (14, 2, 'INV002', 10, '2022-02-22', 222, 'USD')
, (15, 2, 'INV003', 10, '2022-02-22', 222, 'USD')
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values (11, 1, 'other', 'Other 1')
, (22, 2, 'cash', 'Cash 2')
;
select lives_ok(
$$ select add_payment(1, null, '2023-05-02', 11, '“Protection”', '11.11', array['tag1', 'tag2']) $$,
'Should be able to insert a payment, unrelated to any expense, for the first company'
);
select lives_ok(
$$ select add_payment(2, 13, '2023-05-03', 22, 'Payment of INV001', '1.11', array[]::tag_name[]) $$,
'Should be able to insert a complete payment for the first expense'
);
select lives_ok(
$$ select add_payment(2, 14, '2023-05-04', 22, 'First payment of INV002', '1.00', array[]::tag_name[]) $$,
'Should be able to insert a partial payment for the second expense'
);
select lives_ok(
$$ select add_payment(2, 14, '2023-05-05', 22, 'Second payment of INV002', '1.22', array[]::tag_name[]) $$,
'Should be able to insert a partial, and final, payment for the second expense'
);
select lives_ok(
$$ select add_payment(2, 15, '2023-05-06', 22, 'Partial payment of INV003', '1.11', array[]::tag_name[]) $$,
'Should be able to insert a partial payment for the third expense'
);
select bag_eq(
$$ select company_id, description, payment_date::text, payment_account_id, amount, currency_code, payment_status, tags::text, created_at from payment $$,
$$ values (1, '“Protection”', '2023-05-02', 11, 1111, 'EUR', 'complete', '{tag1,tag2}', current_timestamp)
, (2, 'Payment of INV001', '2023-05-03', 22, 111, 'USD', 'complete', '{}', current_timestamp)
, (2, 'First payment of INV002', '2023-05-04', 22, 100, 'USD', 'partial', '{}', current_timestamp)
, (2, 'Second payment of INV002', '2023-05-05', 22, 122, 'USD', 'partial', '{}', current_timestamp)
, (2, 'Partial payment of INV003', '2023-05-06', 22, 111, 'USD', 'partial', '{}', current_timestamp)
$$,
'Should have created all payments'
);
select bag_eq(
$$ select expense_id, description from expense_payment join payment using (payment_id) $$,
$$ values (13, 'Payment of INV001')
, (14, 'First payment of INV002')
, (14, 'Second payment of INV002')
, (15, 'Partial payment of INV003')
$$,
'Should have linked all expenses to payments'
);
select bag_eq(
$$ select expense_id, expense_status from expense $$,
$$ values (12, 'pending')
, (13, 'paid')
, (14, 'paid')
, (15, 'partial')
$$,
'Should have updated the status of expenses'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,67 @@
-- Test add_payment_account_bank
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(12);
set search_path to numerus, auth, public;
select has_function('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban']);
select function_lang_is('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'plpgsql');
select function_returns('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'uuid');
select isnt_definer('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban']);
select volatility_is('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'volatile');
select function_privs_are('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate payment_account_bank cascade;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
select lives_ok(
$$ select add_payment_account_bank(1, 'Bank A', 'ES2820958297603648596978') $$,
'Should be able to insert a first bank payment account'
);
select lives_ok (
$$ select add_payment_account_bank(1, 'Bank 2', 'DE68500105178297336485') $$,
'Should be able to insert a second bank payment account'
);
select bag_eq(
$$ select company_id, payment_account_type::text, name, iban::text from payment_account join payment_account_bank using (payment_account_id, payment_account_type) $$,
$$ values (1, 'bank', 'Bank A', 'ES2820958297603648596978')
, (1, 'bank', 'Bank 2', 'DE68500105178297336485')
$$,
'Should have created all payment accounts'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,66 @@
-- Test add_payment_account_card
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(12);
set search_path to numerus, public;
select has_function('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date']);
select function_lang_is('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'plpgsql');
select function_returns('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'uuid');
select isnt_definer('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date']);
select volatility_is('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'volatile');
select function_privs_are('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate payment_account_card cascade;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
select lives_ok(
$$ select add_payment_account_card(1, 'Card A', '1234', '2024-04-04') $$,
'Should be able to insert a first card payment account'
);
select lives_ok (
$$ select add_payment_account_card(1, 'Card 2', '2345', '2025-05-05') $$,
'Should be able to insert a second card payment account'
);
select bag_eq(
$$ select company_id, payment_account_type::text, name, last_four_digits, expiration_date::text from payment_account join payment_account_card using (payment_account_id, payment_account_type) $$,
$$ values (1, 'card', 'Card A', '1234', '2024-04-04')
, (1, 'card', 'Card 2', '2345', '2025-05-05')
$$,
'Should have created all payment accounts'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,65 @@
-- Test add_payment_account_cash
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(12);
set search_path to numerus, public;
select has_function('numerus', 'add_payment_account_cash', array['integer', 'text']);
select function_lang_is('numerus', 'add_payment_account_cash', array['integer', 'text'], 'sql');
select function_returns('numerus', 'add_payment_account_cash', array['integer', 'text'], 'uuid');
select isnt_definer('numerus', 'add_payment_account_cash', array['integer', 'text']);
select volatility_is('numerus', 'add_payment_account_cash', array['integer', 'text'], 'volatile');
select function_privs_are('numerus', 'add_payment_account_cash', array['integer', 'text'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_payment_account_cash', array['integer', 'text'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_payment_account_cash', array['integer', 'text'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_payment_account_cash', array['integer', 'text'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
select lives_ok(
$$ select add_payment_account_cash(1, 'Cash A') $$,
'Should be able to insert a first cash payment account'
);
select lives_ok (
$$ select add_payment_account_cash(1, 'Cash 2') $$,
'Should be able to insert a second cash payment account'
);
select bag_eq(
$$ select company_id, payment_account_type::text, name from payment_account $$,
$$ values (1, 'cash', 'Cash A')
, (1, 'cash', 'Cash 2')
$$,
'Should have created all payment accounts'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,65 @@
-- Test add_payment_account_other
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(12);
set search_path to numerus, public;
select has_function('numerus', 'add_payment_account_other', array['integer', 'text']);
select function_lang_is('numerus', 'add_payment_account_other', array['integer', 'text'], 'sql');
select function_returns('numerus', 'add_payment_account_other', array['integer', 'text'], 'uuid');
select isnt_definer('numerus', 'add_payment_account_other', array['integer', 'text']);
select volatility_is('numerus', 'add_payment_account_other', array['integer', 'text'], 'volatile');
select function_privs_are('numerus', 'add_payment_account_other', array['integer', 'text'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_payment_account_other', array['integer', 'text'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_payment_account_other', array['integer', 'text'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_payment_account_other', array['integer', 'text'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
select lives_ok(
$$ select add_payment_account_other(1, 'Other A') $$,
'Should be able to insert a first payment account'
);
select lives_ok (
$$ select add_payment_account_other(1, 'Other 2') $$,
'Should be able to insert a second payment account'
);
select bag_eq(
$$ select company_id, payment_account_type::text, name from payment_account $$,
$$ values (1, 'other', 'Other A')
, (1, 'other', 'Other 2')
$$,
'Should have created all payment accounts'
);
select *
from finish();
rollback;

115
test/edit_payment.sql Normal file
View File

@ -0,0 +1,115 @@
-- Test edit_payment
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(14);
set search_path to numerus, public;
select has_function('numerus', 'edit_payment', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]']);
select function_lang_is('numerus', 'edit_payment', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'plpgsql');
select function_returns('numerus', 'edit_payment', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'uuid');
select isnt_definer('numerus', 'edit_payment', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]']);
select volatility_is('numerus', 'edit_payment', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'volatile');
select function_privs_are('numerus', 'edit_payment', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'edit_payment', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_payment', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_payment', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate expense_payment cascade;
truncate payment cascade;
truncate expense cascade;
truncate contact cascade;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into contact (contact_id, company_id, name)
values ( 9, 1, 'Customer 1')
;
insert into expense (expense_id, company_id, invoice_number, contact_id, invoice_date, amount, currency_code, expense_status)
values (13, 1, 'INV001', 9, '2011-01-11', 111, 'EUR', 'paid')
, (14, 1, 'INV002', 9, '2022-02-22', 222, 'EUR', 'paid')
, (15, 1, 'INV003', 9, '2022-02-22', 333, 'EUR', 'partial')
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values (11, 1, 'cash', 'Cash 1')
, (12, 1, 'cash', 'Cash 2')
, (13, 1, 'other', 'Other')
;
insert into payment (payment_id, company_id, slug, description, payment_date, payment_account_id, amount, currency_code, payment_status, tags)
values (16, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Payment INV001', '2023-05-04', 12, 111, 'EUR', 'complete', '{tag1}')
, (17, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'First INV002', '2023-05-05', 13, 100, 'EUR', 'partial', '{tag2}')
, (18, 1, '3bdad7a8-4a1e-4ae0-b5c6-015e51ee0502', 'Second INV002', '2023-05-06', 13, 122, 'EUR', 'partial', '{tag1,tag3}')
, (19, 1, '5a524bee-8311-4d13-9adf-ef6310b26990', 'Partial INV003', '2023-05-07', 11, 123, 'EUR', 'partial', '{}')
;
insert into expense_payment (expense_id, payment_id)
values (13, 16)
, (14, 17)
, (14, 18)
, (15, 19)
;
select lives_ok(
$$ select edit_payment('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', '2023-05-06', 13, 'Partial INV001', '1.00', array['tag1']) $$,
'Should be able to change a complete payment to partial'
);
select lives_ok(
$$ select edit_payment('b57b980b-247b-4be4-a0b7-03a7819c53ae', '2023-05-07', 12, 'First INV002', '0.50', array['tag1', 'tag3']) $$,
'Should be able to adjust a partial payment, that is still partial, and the expense now becomes partial'
);
select lives_ok(
$$ select edit_payment('5a524bee-8311-4d13-9adf-ef6310b26990', '2023-05-01', 11, 'Complete INV003', '3.33', array[]::tag_name[]) $$,
'Should be able to complete a previously partial payment'
);
select bag_eq(
$$ select description, payment_date::text, payment_account_id, amount, payment_status, tags::text from payment $$,
$$ values ('Partial INV001', '2023-05-06', 13, 100, 'partial', '{tag1}')
, ('First INV002', '2023-05-07', 12, 50, 'partial', '{tag1,tag3}')
, ('Second INV002', '2023-05-06', 13, 122, 'partial', '{tag1,tag3}')
, ('Complete INV003', '2023-05-01', 11, 333, 'complete', '{}')
$$,
'Should have updated all payments'
);
select bag_eq(
$$ select expense_id, expense_status from expense $$,
$$ values (13, 'partial')
, (14, 'partial')
, (15, 'paid')
$$,
'Should have updated expenses too'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,84 @@
-- Test edit_payment_account_bank
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to numerus, public;
select has_function('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban']);
select function_lang_is('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'plpgsql');
select function_returns('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'uuid');
select isnt_definer('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban']);
select volatility_is('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'volatile');
select function_privs_are('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'guest', array []::text[]);
select function_privs_are('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate payment_account_bank cascade;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into payment_account (payment_account_id, company_id, slug, payment_account_type, name)
values (11, 1, '81c478ab-395c-44aa-a5b4-3489e5349a0b', 'bank', 'Bank 1')
, (12, 1, '84e84788-54f1-4796-b93d-dfa2c9adc072', 'bank', 'Bank 2')
, (13, 1, 'afb774a5-0c38-480c-bef2-fe03465071cd', 'other', 'Other')
;
insert into payment_account_bank (payment_account_id, iban)
values (11, 'MR1765838885255712383711884')
, (12, 'UA709147419869457296562612646')
;
select lives_ok(
$$ select edit_payment_account_bank('81c478ab-395c-44aa-a5b4-3489e5349a0b', 'Piggy Bank', 'ES2820958297603648596978') $$,
'Should be able to edit the first bank payment account'
);
select lives_ok (
$$ select edit_payment_account_bank('84e84788-54f1-4796-b93d-dfa2c9adc072', 'Totally Secure', 'DE68500105178297336485') $$,
'Should be able to edit the second bank payment account'
);
select results_eq (
$$ select edit_payment_account_bank('afb774a5-0c38-480c-bef2-fe03465071cd', 'Huh?', 'ES3720951423184941926287')::text $$,
$$ values (null::text) $$,
'Should do nothing to an account that is not of type bank'
);
select bag_eq(
$$ select payment_account_id, payment_account_type::text, name, iban::text from payment_account left join payment_account_bank using (payment_account_id, payment_account_type) $$,
$$ values (11, 'bank', 'Piggy Bank', 'ES2820958297603648596978')
, (12, 'bank', 'Totally Secure', 'DE68500105178297336485')
, (13, 'other', 'Other', null)
$$,
'Should have update all bank payment accounts'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,84 @@
-- Test edit_payment_account_card
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to numerus, public;
select has_function('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date']);
select function_lang_is('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'plpgsql');
select function_returns('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'uuid');
select isnt_definer('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date']);
select volatility_is('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'volatile');
select function_privs_are('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'guest', array []::text[]);
select function_privs_are('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate payment_account_card cascade;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into payment_account (payment_account_id, company_id, slug, payment_account_type, name)
values (11, 1, '81c478ab-395c-44aa-a5b4-3489e5349a0b', 'card', 'Card 1')
, (12, 1, '84e84788-54f1-4796-b93d-dfa2c9adc072', 'card', 'Card 2')
, (13, 1, 'afb774a5-0c38-480c-bef2-fe03465071cd', 'other', 'Other')
;
insert into payment_account_card (payment_account_id, last_four_digits, expiration_date)
values (11, '1234', '2022-04-02')
, (12, '2345', '2024-05-02')
;
select lives_ok(
$$ select edit_payment_account_card('81c478ab-395c-44aa-a5b4-3489e5349a0b', 'Gold Card', '9999', '2022-12-22') $$,
'Should be able to edit the first card payment account'
);
select lives_ok (
$$ select edit_payment_account_card('84e84788-54f1-4796-b93d-dfa2c9adc072', 'Scurvy Card', '7777', '2025-08-25') $$,
'Should be able to edit the second card payment account'
);
select results_eq (
$$ select edit_payment_account_card('afb774a5-0c38-480c-bef2-fe03465071cd', 'Huh?', '1234', '2006-01-02')::text $$,
$$ values (null::text) $$,
'Should do nothing to an account that is not of type card'
);
select bag_eq(
$$ select payment_account_id, payment_account_type::text, name, last_four_digits, expiration_date::text from payment_account left join payment_account_card using (payment_account_id, payment_account_type) $$,
$$ values (11, 'card', 'Gold Card', '9999', '2022-12-22')
, (12, 'card', 'Scurvy Card', '7777', '2025-08-25')
, (13, 'other', 'Other', null::text, null::text)
$$,
'Should have update all card payment accounts'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,77 @@
-- Test edit_payment_account_cash
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to numerus, public;
select has_function('numerus', 'edit_payment_account_cash', array['uuid', 'text']);
select function_lang_is('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'sql');
select function_returns('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'uuid');
select isnt_definer('numerus', 'edit_payment_account_cash', array['uuid', 'text']);
select volatility_is('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'volatile');
select function_privs_are('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'guest', array []::text[]);
select function_privs_are('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into payment_account (payment_account_id, company_id, slug, payment_account_type, name)
values (11, 1, '81c478ab-395c-44aa-a5b4-3489e5349a0b', 'cash', 'Cash 1')
, (12, 1, '84e84788-54f1-4796-b93d-dfa2c9adc072', 'cash', 'Cash 2')
, (13, 1, 'afb774a5-0c38-480c-bef2-fe03465071cd', 'other', 'Other')
;
select lives_ok(
$$ select edit_payment_account_cash('81c478ab-395c-44aa-a5b4-3489e5349a0b', 'My stash') $$,
'Should be able to edit the first cash payment account'
);
select lives_ok (
$$ select edit_payment_account_cash('84e84788-54f1-4796-b93d-dfa2c9adc072', 'Under mattress') $$,
'Should be able to edit the second cash payment account'
);
select results_eq (
$$ select edit_payment_account_cash('afb774a5-0c38-480c-bef2-fe03465071cd', 'Huh?')::text $$,
$$ values (null::text) $$,
'Should do nothing to an account that is not of type cash'
);
select bag_eq(
$$ select payment_account_id, payment_account_type::text, name from payment_account $$,
$$ values (11, 'cash', 'My stash')
, (12, 'cash', 'Under mattress')
, (13, 'other', 'Other')
$$,
'Should have update all cash payment accounts'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,77 @@
-- Test edit_payment_account_other
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to numerus, public;
select has_function('numerus', 'edit_payment_account_other', array['uuid', 'text']);
select function_lang_is('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'sql');
select function_returns('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'uuid');
select isnt_definer('numerus', 'edit_payment_account_other', array['uuid', 'text']);
select volatility_is('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'volatile');
select function_privs_are('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'guest', array []::text[]);
select function_privs_are('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into payment_account (payment_account_id, company_id, slug, payment_account_type, name)
values (11, 1, '81c478ab-395c-44aa-a5b4-3489e5349a0b', 'other', 'Other 1')
, (12, 1, '84e84788-54f1-4796-b93d-dfa2c9adc072', 'other', 'Other 2')
, (13, 1, 'afb774a5-0c38-480c-bef2-fe03465071cd', 'cash', 'Cash')
;
select lives_ok(
$$ select edit_payment_account_other('81c478ab-395c-44aa-a5b4-3489e5349a0b', 'Something') $$,
'Should be able to edit the first other payment account'
);
select lives_ok (
$$ select edit_payment_account_other('84e84788-54f1-4796-b93d-dfa2c9adc072', 'And something else') $$,
'Should be able to edit the second other payment account'
);
select results_eq (
$$ select edit_payment_account_other('afb774a5-0c38-480c-bef2-fe03465071cd', 'Huh?')::text $$,
$$ values (null::text) $$,
'Should do nothing to an account that is not of type other'
);
select bag_eq(
$$ select payment_account_id, payment_account_type::text, name from payment_account $$,
$$ values (11, 'other', 'Something')
, (12, 'other', 'And something else')
, (13, 'cash', 'Cash')
$$,
'Should have update all other payment accounts'
);
select *
from finish();
rollback;

136
test/expense_payment.sql Normal file
View File

@ -0,0 +1,136 @@
-- Test expense_payment
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(23);
set search_path to numerus, auth, public;
select has_table('expense_payment');
select has_pk('expense_payment');
select col_is_pk('expense_payment', array['expense_id', 'payment_id']);
select table_privs_are('expense_payment', 'guest', array []::text[]);
select table_privs_are('expense_payment', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('expense_payment', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('expense_payment', 'authenticator', array []::text[]);
select has_column('expense_payment', 'expense_id');
select col_is_fk('expense_payment', 'expense_id');
select fk_ok('expense_payment', 'expense_id', 'expense', 'expense_id');
select col_type_is('expense_payment', 'expense_id', 'integer');
select col_not_null('expense_payment', 'expense_id');
select col_hasnt_default('expense_payment', 'expense_id');
select has_column('expense_payment', 'payment_id');
select col_is_fk('expense_payment', 'payment_id');
select fk_ok('expense_payment', 'payment_id', 'payment', 'payment_id');
select col_type_is('expense_payment', 'payment_id', 'integer');
select col_not_null('expense_payment', 'payment_id');
select col_hasnt_default('expense_payment', 'payment_id');
set client_min_messages to warning;
truncate expense_payment cascade;
truncate payment cascade;
truncate payment_account cascade;
truncate expense cascade;
truncate contact cascade;
truncate company_user cascade;
truncate payment_method cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into contact (contact_id, company_id, name)
values ( 9, 2, 'Customer 1')
, (10, 4, 'Customer 2')
;
insert into expense (expense_id, company_id, invoice_number, contact_id, invoice_date, amount, currency_code)
values (13, 2, 'INV001', 9, '2011-01-11', 111, 'EUR')
, (14, 4, 'INV002', 10, '2022-02-22', 222, 'EUR')
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values (17, 2, 'cash', 'Cash 2')
, (18, 4, 'cash', 'Cash 4')
;
insert into payment (payment_id, company_id, description, payment_date, payment_account_id, amount, currency_code)
values (21, 2, 'Payment INV001', '2022-01-11', 17, 111, 'EUR')
, (22, 4, 'Payment INV002', '2022-02-23', 18, 222, 'EUR')
;
insert into expense_payment (expense_id, payment_id)
values (13, 21)
, (14, 22)
;
prepare expense_payment_data as
select expense_id, payment_id
from expense_payment
order by expense_id, payment_id;
set role invoicer;
select is_empty('expense_payment_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'expense_payment_data',
$$ values (13, 21)
$$,
'Should only list tax of products of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'expense_payment_data',
$$ values (14, 22)
$$,
'Should only list tax of products of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'expense_payment_data',
'42501', 'permission denied for table expense_payment',
'Should not allow select to guest users'
);
reset role;
select *
from finish();
rollback;

173
test/payment.sql Normal file
View File

@ -0,0 +1,173 @@
-- Test payment
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(68);
set search_path to numerus, auth, public;
select has_table('payment');
select has_pk('payment');
select table_privs_are('payment', 'guest', array []::text[]);
select table_privs_are('payment', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('payment', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('payment', 'authenticator', array []::text[]);
select has_column('payment', 'payment_id');
select col_type_is('payment', 'payment_id', 'integer');
select col_not_null('payment', 'payment_id');
select col_hasnt_default('payment', 'payment_id');
select has_column('payment', 'company_id');
select col_is_fk('payment', 'company_id');
select fk_ok('payment', 'company_id', 'company', 'company_id');
select col_type_is('payment', 'company_id', 'integer');
select col_not_null('payment', 'company_id');
select col_hasnt_default('payment', 'company_id');
select has_column('payment', 'slug');
select col_is_unique('payment', 'slug');
select col_type_is('payment', 'slug', 'uuid');
select col_not_null('payment', 'slug');
select col_has_default('payment', 'slug');
select col_default_is('payment', 'slug', 'gen_random_uuid()');
select has_column('payment', 'description');
select col_type_is('payment', 'description', 'text');
select col_not_null('payment', 'description');
select col_hasnt_default('payment', 'description');
select has_column('payment', 'payment_date');
select col_type_is('payment', 'payment_date', 'date');
select col_not_null('payment', 'payment_date');
select col_has_default('payment', 'payment_date');
select col_default_is('payment', 'payment_date', 'CURRENT_DATE');
select has_column('payment', 'payment_account_id');
select col_is_fk('payment', 'payment_account_id');
select fk_ok('payment', 'payment_account_id', 'payment_account', 'payment_account_id');
select col_type_is('payment', 'payment_account_id', 'integer');
select col_not_null('payment', 'payment_account_id');
select col_hasnt_default('payment', 'payment_account_id');
select has_column('payment', 'amount');
select col_type_is('payment', 'amount', 'integer');
select col_not_null('payment', 'amount');
select col_hasnt_default('payment', 'amount');
select has_column('payment', 'currency_code');
select col_is_fk('payment', 'currency_code');
select fk_ok('payment', 'currency_code', 'currency', 'currency_code');
select col_type_is('payment', 'currency_code', 'text');
select col_not_null('payment', 'currency_code');
select col_hasnt_default('payment', 'currency_code');
select has_column('payment', 'tags');
select col_type_is('payment', 'tags', 'tag_name[]');
select col_not_null('payment', 'tags');
select col_has_default('payment', 'tags');
select col_default_is('payment', 'tags', '{}');
select has_column('payment', 'payment_status');
select col_is_fk('payment', 'payment_status');
select fk_ok('payment', 'payment_status', 'payment_status', 'payment_status');
select col_type_is('payment', 'payment_status', 'text');
select col_not_null('payment', 'payment_status');
select col_has_default('payment', 'payment_status');
select col_default_is('payment', 'payment_status', 'complete');
select has_column('payment', 'created_at');
select col_type_is('payment', 'created_at', 'timestamp with time zone');
select col_not_null('payment', 'created_at');
select col_has_default('payment', 'created_at');
select col_default_is('payment', 'created_at', 'CURRENT_TIMESTAMP');
set client_min_messages to warning;
truncate payment cascade;
truncate payment_account 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, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values (221, 2, 'other', 'Other 2')
, (441, 4, 'other', 'Other 4')
;
insert into payment (company_id, description, payment_account_id, amount, currency_code)
values (2, 'Payment 20001', 221, 333, 'EUR')
, (4, 'Payment 40001', 441, 555, 'EUR')
;
prepare payment_data as
select company_id, description
from payment
order by company_id, description;
set role invoicer;
select is_empty('payment_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'payment_data',
$$ values (2, 'Payment 20001')
$$,
'Should only list payments from the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'payment_data',
$$ values (4, 'Payment 40001')
$$,
'Should only list payments from the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'payment_data',
'42501', 'permission denied for table payment',
'Should not allow select to guest users'
);
reset role;
select *
from finish();
rollback;

139
test/payment_account.sql Normal file
View File

@ -0,0 +1,139 @@
-- Test payment_account
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(38);
set search_path to numerus, auth, public;
select has_table('payment_account');
select has_pk('payment_account');
select table_privs_are('payment_account', 'guest', array []::text[]);
select table_privs_are('payment_account', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('payment_account', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('payment_account', 'authenticator', array []::text[]);
select has_column('payment_account', 'payment_account_id');
select col_is_pk('payment_account', 'payment_account_id');
select col_type_is('payment_account', 'payment_account_id', 'integer');
select col_not_null('payment_account', 'payment_account_id');
select col_hasnt_default('payment_account', 'payment_account_id');
select has_column('payment_account', 'company_id');
select col_is_fk('payment_account', 'company_id');
select fk_ok('payment_account', 'company_id', 'company', 'company_id');
select col_type_is('payment_account', 'company_id', 'integer');
select col_not_null('payment_account', 'company_id');
select col_hasnt_default('payment_account', 'company_id');
select has_column('payment_account', 'slug');
select col_is_unique('payment_account', 'slug');
select col_type_is('payment_account', 'slug', 'uuid');
select col_not_null('payment_account', 'slug');
select col_has_default('payment_account', 'slug');
select col_default_is('payment_account', 'slug', 'gen_random_uuid()');
select has_column('payment_account', 'payment_account_type');
select col_type_is('payment_account', 'payment_account_type', 'text');
select col_is_fk('payment_account', 'payment_account_type');
select fk_ok('payment_account', 'payment_account_type', 'payment_account_type', 'payment_account_type');
select col_not_null('payment_account', 'payment_account_type');
select col_hasnt_default('payment_account', 'payment_account_type');
select has_column('payment_account', 'name');
select col_type_is('payment_account', 'name', 'text');
select col_not_null('payment_account', 'name');
select col_hasnt_default('payment_account', 'name');
set client_min_messages to warning;
truncate payment_account 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, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into payment_account (company_id, payment_account_type, name)
values (2, 'bank', 'Bank A')
, (4, 'cash', 'Cash')
;
prepare payment_account_data as
select company_id, name
from payment_account
order by company_id, name;
set role invoicer;
select is_empty('payment_account_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'payment_account_data',
$$ values (2, 'Bank A')
$$,
'Should only list payment_accounts from the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'payment_account_data',
$$ values (4, 'Cash')
$$,
'Should only list payment_accounts from the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'payment_account_data',
'42501', 'permission denied for table payment_account',
'Should not allow select to guest users'
);
reset role;
select throws_ok( $$
insert into payment_account (company_id, payment_account_type, name)
values (2, 'cash', ' ')
$$,
'23514', 'new row for relation "payment_account" violates check constraint "payment_account_name_not_empty"',
'Should not allow payment accounts with a blank name'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,150 @@
-- Test payment_account_bank
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(29);
set search_path to numerus, auth, public;
select has_table('payment_account_bank');
select has_pk('payment_account_bank');
select table_privs_are('payment_account_bank', 'guest', array []::text[]);
select table_privs_are('payment_account_bank', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('payment_account_bank', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('payment_account_bank', 'authenticator', array []::text[]);
select col_is_fk('payment_account_bank', array['payment_account_id', 'payment_account_type']);
select fk_ok('payment_account_bank', array['payment_account_id', 'payment_account_type'], 'payment_account', array['payment_account_id', 'payment_account_type']);
select has_column('payment_account_bank', 'payment_account_id');
select col_is_pk('payment_account_bank', 'payment_account_id');
select col_type_is('payment_account_bank', 'payment_account_id', 'integer');
select col_not_null('payment_account_bank', 'payment_account_id');
select col_hasnt_default('payment_account_bank', 'payment_account_id');
select has_column('payment_account_bank', 'payment_account_type');
select col_type_is('payment_account_bank', 'payment_account_type', 'text');
select col_not_null('payment_account_bank', 'payment_account_type');
select col_has_default('payment_account_bank', 'payment_account_type');
select col_default_is('payment_account_bank', 'payment_account_type', 'bank');
select has_column('payment_account_bank', 'iban');
select col_type_is('payment_account_bank', 'iban', 'iban');
select col_not_null('payment_account_bank', 'iban');
select col_hasnt_default('payment_account_bank', 'iban');
set client_min_messages to warning;
truncate payment_account_bank cascade;
truncate payment_account cascade;
truncate company_user cascade;
truncate payment_method cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values ( 6, 2, 'bank', 'Bank 2')
, ( 7, 2, 'card', 'Credit card 2')
, ( 8, 2, 'cash', 'Cash 2')
, ( 9, 2, 'other', 'Other 2')
, (10, 4, 'bank', 'Bank 4')
;
insert into payment_account_bank (payment_account_id, iban)
values ( 6, 'NL35INGB5262865534')
, (10, 'MT47JQRS54557143744629565915326')
;
prepare payment_account_data as
select company_id, iban::text
from payment_account
join payment_account_bank using (payment_account_id)
order by payment_account_id, iban;
set role invoicer;
select is_empty('payment_account_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'payment_account_data',
$$ values (2, 'NL35INGB5262865534')
$$,
'Should only list payment_accounts of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'payment_account_data',
$$ values (4, 'MT47JQRS54557143744629565915326')
$$,
'Should only list payment_accounts of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'payment_account_data',
'42501', 'permission denied for table payment_account',
'Should not allow select to guest users'
);
reset role;
select throws_ok( $$
insert into payment_account_bank (payment_account_id, payment_account_type, iban)
values (7, 'card', 'ES2820958297603648596978')
$$,
'23514', 'new row for relation "payment_account_bank" violates check constraint "payment_account_type_is_bank"',
'Should not allow payment accounts of type card'
);
select throws_ok( $$
insert into payment_account_bank (payment_account_id, payment_account_type, iban)
values (8, 'cash', 'ES2820958297603648596978')
$$,
'23514', 'new row for relation "payment_account_bank" violates check constraint "payment_account_type_is_bank"',
'Should not allow payment accounts of type cash'
);
select throws_ok( $$
insert into payment_account_bank (payment_account_id, payment_account_type, iban)
values (9, 'other', 'ES2820958297603648596978')
$$,
'23514', 'new row for relation "payment_account_bank" violates check constraint "payment_account_type_is_bank"',
'Should not allow payment accounts of type other'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,192 @@
-- Test payment_account_card
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(39);
set search_path to numerus, auth, public;
select has_table('payment_account_card');
select has_pk('payment_account_card');
select table_privs_are('payment_account_card', 'guest', array []::text[]);
select table_privs_are('payment_account_card', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('payment_account_card', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('payment_account_card', 'authenticator', array []::text[]);
select col_is_fk('payment_account_card', array['payment_account_id', 'payment_account_type']);
select fk_ok('payment_account_card', array['payment_account_id', 'payment_account_type'], 'payment_account', array['payment_account_id', 'payment_account_type']);
select has_column('payment_account_card', 'payment_account_id');
select col_is_pk('payment_account_card', 'payment_account_id');
select col_type_is('payment_account_card', 'payment_account_id', 'integer');
select col_not_null('payment_account_card', 'payment_account_id');
select col_hasnt_default('payment_account_card', 'payment_account_id');
select has_column('payment_account_card', 'payment_account_type');
select col_type_is('payment_account_card', 'payment_account_type', 'text');
select col_not_null('payment_account_card', 'payment_account_type');
select col_has_default('payment_account_card', 'payment_account_type');
select col_default_is('payment_account_card', 'payment_account_type', 'card');
select has_column('payment_account_card', 'last_four_digits');
select col_type_is('payment_account_card', 'last_four_digits', 'text');
select col_not_null('payment_account_card', 'last_four_digits');
select col_hasnt_default('payment_account_card', 'last_four_digits');
select has_column('payment_account_card', 'expiration_date');
select col_type_is('payment_account_card', 'expiration_date', 'date');
select col_not_null('payment_account_card', 'expiration_date');
select col_hasnt_default('payment_account_card', 'expiration_date');
set client_min_messages to warning;
truncate payment_account_card cascade;
truncate payment_account cascade;
truncate company_user cascade;
truncate payment_method cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values ( 6, 2, 'bank', 'Bank 2')
, ( 7, 2, 'card', 'Credit card 2')
, ( 8, 2, 'cash', 'Cash 2')
, ( 9, 2, 'other', 'Other 2')
, (10, 4, 'card', 'Card 4')
;
insert into payment_account_card (payment_account_id, last_four_digits, expiration_date)
values ( 7, '1234', '2024-07-09')
, (10, '4321', '2025-09-07')
;
prepare payment_account_data as
select company_id, last_four_digits
from payment_account
join payment_account_card using (payment_account_id)
order by payment_account_id, last_four_digits;
set role invoicer;
select is_empty('payment_account_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'payment_account_data',
$$ values (2, '1234')
$$,
'Should only list payment_accounts of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'payment_account_data',
$$ values (4, '4321')
$$,
'Should only list payment_accounts of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'payment_account_data',
'42501', 'permission denied for table payment_account',
'Should not allow select to guest users'
);
reset role;
select throws_ok( $$
insert into payment_account_card (payment_account_id, payment_account_type, last_four_digits, expiration_date)
values (6, 'bank', '1111', '2025-05-05')
$$,
'23514', 'new row for relation "payment_account_card" violates check constraint "payment_account_type_is_card"',
'Should not allow payment accounts of type bank'
);
select throws_ok( $$
insert into payment_account_card (payment_account_id, payment_account_type, last_four_digits, expiration_date)
values (8, 'cash', '1111', '2025-05-05')
$$,
'23514', 'new row for relation "payment_account_card" violates check constraint "payment_account_type_is_card"',
'Should not allow payment accounts of type cash'
);
select throws_ok( $$
insert into payment_account_card (payment_account_id, payment_account_type, last_four_digits, expiration_date)
values (9, 'other', '1111', '2025-05-05')
$$,
'23514', 'new row for relation "payment_account_card" violates check constraint "payment_account_type_is_card"',
'Should not allow payment accounts of type other'
);
select throws_ok (
$$ update payment_account_card set last_four_digits = 'a234' where payment_account_id = 7 $$,
'23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"',
'Last four digits of a credit card number should be all digits'
);
select throws_ok (
$$ update payment_account_card set last_four_digits = '1a34' where payment_account_id = 7 $$,
'23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"',
'Last four digits of a credit card number should be all digits'
);
select throws_ok (
$$ update payment_account_card set last_four_digits = '12a4' where payment_account_id = 7 $$,
'23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"',
'Last four digits of a credit card number should be all digits'
);
select throws_ok (
$$ update payment_account_card set last_four_digits = '123a' where payment_account_id = 7 $$,
'23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"',
'Last four digits of a credit card number should be all digits'
);
select throws_ok (
$$ update payment_account_card set last_four_digits = '12345' where payment_account_id = 7 $$,
'23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"',
'Last four digits of a credit card number should be not have more than four digits'
);
select throws_ok (
$$ update payment_account_card set last_four_digits = '123' where payment_account_id = 7 $$,
'23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"',
'Last four digits of a credit card number should be not have less than four digits'
);
select *
from finish();
rollback;

View File

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

View File

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

35
test/payment_status.sql Normal file
View File

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

View File

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

View File

@ -0,0 +1,26 @@
-- Test update_expense_payment_status
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(9);
set search_path to numerus, public;
select has_function('numerus', 'update_expense_payment_status', array['integer', 'integer', 'integer']);
select function_lang_is('numerus', 'update_expense_payment_status', array['integer', 'integer', 'integer'], 'sql');
select function_returns('numerus', 'update_expense_payment_status', array['integer', 'integer', 'integer'], 'void');
select isnt_definer('numerus', 'update_expense_payment_status', array['integer', 'integer', 'integer']);
select volatility_is('numerus', 'update_expense_payment_status', array['integer', 'integer', 'integer'], 'volatile');
select function_privs_are('numerus', 'update_expense_payment_status', array ['integer', 'integer', 'integer'], 'guest', array []::text[]);
select function_privs_are('numerus', 'update_expense_payment_status', array ['integer', 'integer', 'integer'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'update_expense_payment_status', array ['integer', 'integer', 'integer'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'update_expense_payment_status', array ['integer', 'integer', 'integer'], 'authenticator', array []::text[]);
select *
from finish();
rollback;

7
verify/add_payment.sql Normal file
View File

@ -0,0 +1,7 @@
-- Verify numerus:add_payment on pg
begin;
select has_function_privilege('numerus.add_payment(integer, integer, date, integer, text, text, numerus.tag_name[])', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:add_payment_account_bank on pg
begin;
select has_function_privilege('numerus.add_payment_account_bank(integer, text, iban)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:add_payment_account_card on pg
begin;
select has_function_privilege('numerus.add_payment_account_card(integer, text, text, date)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:add_payment_account_cash on pg
begin;
select has_function_privilege('numerus.add_payment_account_cash(integer, text)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:add_payment_account_other on pg
begin;
select has_function_privilege('numerus.add_payment_account_other(integer, text)', 'execute');
rollback;

View File

@ -5,10 +5,13 @@ begin;
set search_path to numerus;
select 1 / count(*) from expense_status where expense_status = 'pending' and name ='Pending';
select 1 / count(*) from expense_status where expense_status = 'partial' and name ='Partial';
select 1 / count(*) from expense_status where expense_status = 'paid' and name ='Paid';
select 1 / count(*) from expense_status_i18n where expense_status = 'pending' and name ='Pendent' and lang_tag = 'ca';
select 1 / count(*) from expense_status_i18n where expense_status = 'pending' and name ='Pendiente' and lang_tag = 'es';
select 1 / count(*) from expense_status_i18n where expense_status = 'partial' and name ='Parcial' and lang_tag = 'ca';
select 1 / count(*) from expense_status_i18n where expense_status = 'partial' and name ='Parcial' and lang_tag = 'es';
select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'ca';
select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'es';

View File

@ -0,0 +1,15 @@
-- Verify numerus:available_expense_status on pg
begin;
set search_path to numerus;
select 1 / count(*) from expense_status where expense_status = 'pending' and name ='Pending';
select 1 / count(*) from expense_status where expense_status = 'paid' and name ='Paid';
select 1 / count(*) from expense_status_i18n where expense_status = 'pending' and name ='Pendent' and lang_tag = 'ca';
select 1 / count(*) from expense_status_i18n where expense_status = 'pending' and name ='Pendiente' and lang_tag = 'es';
select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'ca';
select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'es';
rollback;

View File

@ -0,0 +1,21 @@
-- Verify numerus:available_payment_account_types on pg
begin;
set search_path to numerus;
select 1 / count(*) from payment_account_type where payment_account_type = 'bank' and name ='Bank';
select 1 / count(*) from payment_account_type where payment_account_type = 'card' and name ='Credit Card';
select 1 / count(*) from payment_account_type where payment_account_type = 'cash' and name ='Cash';
select 1 / count(*) from payment_account_type where payment_account_type = 'other' and name ='Other';
select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'bank' and name ='Banc' and lang_tag = 'ca';
select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'bank' and name ='Banco' and lang_tag = 'es';
select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'card' and name ='Targeta de crèdit' and lang_tag= 'ca';
select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'card' and name ='Tarjeta de crédito' and lang_tag= 'es';
select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'cash' and name ='Efectiu' and lang_tag= 'ca';
select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'cash' and name ='Efectivo' and lang_tag= 'es';
select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'other' and name ='Altres' and lang_tag= 'ca';
select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'other' and name ='Otros' and lang_tag= 'es';
rollback;

View File

@ -0,0 +1,15 @@
-- Verify numerus:available_payment_status on pg
BEGIN;
set search_path to numerus;
select 1 / count(*) from payment_status where payment_status = 'partial' and name ='Partial';
select 1 / count(*) from payment_status where payment_status = 'complete' and name ='Complete';
select 1 / count(*) from payment_status_i18n where payment_status = 'partial' and name ='Parcial' and lang_tag = 'ca';
select 1 / count(*) from payment_status_i18n where payment_status = 'partial' and name ='Parcial' and lang_tag = 'es';
select 1 / count(*) from payment_status_i18n where payment_status = 'complete' and name ='Complet' and lang_tag= 'ca';
select 1 / count(*) from payment_status_i18n where payment_status = 'complete' and name ='Completo' and lang_tag= 'es';
ROLLBACK;

7
verify/edit_payment.sql Normal file
View File

@ -0,0 +1,7 @@
-- Verify numerus:edit_payment on pg
begin;
select has_function_privilege('numerus.edit_payment(uuid, date, integer, text, text, numerus.tag_name[])', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:edit_payment_account_bank on pg
begin;
select has_function_privilege('numerus.edit_payment_account_bank(uuid, text, iban)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:edit_payment_account_card on pg
begin;
select has_function_privilege('numerus.edit_payment_account_card(uuid, text, text, date)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:edit_payment_account_cash on pg
begin;
select has_function_privilege('numerus.edit_payment_account_cash(uuid, text)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:edit_payment_account_other on pg
begin;
select has_function_privilege('numerus.edit_payment_account_other(uuid, text)', 'execute');
rollback;

View File

@ -0,0 +1,13 @@
-- Verify numerus:expense_payment on pg
begin;
select expense_id
, payment_id
from numerus.expense_payment
where false;
select 1 / count(*) from pg_class where oid = 'numerus.expense_payment'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.expense_payment'::regclass;
rollback;

22
verify/payment.sql Normal file
View File

@ -0,0 +1,22 @@
-- Verify numerus:payment on pg
begin;
select payment_id
, company_id
, slug
, description
, payment_date
, payment_account_id
, amount
, currency_code
, tags
, payment_status
, created_at
from numerus.payment
where false;
select 1 / count(*) from pg_class where oid = 'numerus.payment'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.payment'::regclass;
rollback;

View File

@ -0,0 +1,16 @@
-- Verify numerus:payment_account on pg
begin;
select payment_account_id
, company_id
, slug
, payment_account_type
, name
from numerus.payment_account
where false;
select 1 / count(*) from pg_class where oid = 'numerus.payment_account'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.payment_account'::regclass;
rollback;

View File

@ -0,0 +1,14 @@
-- Verify numerus:payment_account_bank on pg
begin;
select payment_account_id
, payment_account_type
, iban
from numerus.payment_account_bank
where false;
select 1 / count(*) from pg_class where oid = 'numerus.payment_account_bank'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.payment_account_bank'::regclass;
rollback;

View File

@ -0,0 +1,15 @@
-- Verify numerus:payment_account_card on pg
begin;
select payment_account_id
, payment_account_type
, last_four_digits
, expiration_date
from numerus.payment_account_card
where false;
select 1 / count(*) from pg_class where oid = 'numerus.payment_account_card'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.payment_account_card'::regclass;
rollback;

View File

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

View File

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

10
verify/payment_status.sql Normal file
View File

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

View File

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

View File

@ -0,0 +1,7 @@
-- Verify numerus:update_expense_payment_status on pg
begin;
select has_function_privilege('numerus.update_expense_payment_status(integer, integer, integer)', 'execute');
rollback;

View File

@ -54,6 +54,7 @@
<li><a{{if requestURIHasPrefix (companyURI "/quotes") }} aria-current="page"{{ end }} href="{{ companyURI "/quotes" }}">{{( pgettext "Quotations" "nav" )}}</a></li>
<li><a{{if requestURIHasPrefix (companyURI "/invoices") }} aria-current="page"{{ end }} href="{{ companyURI "/invoices" }}">{{( pgettext "Invoices" "nav" )}}</a></li>
<li><a{{if requestURIHasPrefix (companyURI "/expenses") }} aria-current="page"{{ end }} href="{{ companyURI "/expenses" }}">{{( pgettext "Expenses" "nav" )}}</a></li>
<li><a{{if requestURIHasPrefix (companyURI "/payments") }} aria-current="page"{{ end }} href="{{ companyURI "/payments" }}">{{( pgettext "Payments" "nav" )}}</a></li>
<li><a{{if requestURIHasPrefix (companyURI "/products") }} aria-current="page"{{ end }} href="{{ companyURI "/products" }}">{{( pgettext "Products" "nav" )}}</a></li>
<li><a{{if requestURIHasPrefix (companyURI "/contacts") }} aria-current="page"{{ end }} href="{{ companyURI "/contacts" }}">{{( pgettext "Contacts" "nav" )}}</a></li>
</ul>

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