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) insert into expense_status (expense_status, name)
values ('pending', 'Pending') values ('pending', 'Pending')
, ('partial', 'Partial')
, ('paid', 'Paid') , ('paid', 'Paid')
on conflict (expense_status) do nothing
; ;
insert into expense_status_i18n (expense_status, lang_tag, name) insert into expense_status_i18n (expense_status, lang_tag, name)
values ('pending', 'ca', 'Pendent') values ('pending', 'ca', 'Pendent')
, ('partial', 'ca', 'Parcial')
, ('paid', 'ca', 'Pagada') , ('paid', 'ca', 'Pagada')
, ('pending', 'es', 'Pendiente') , ('pending', 'es', 'Pendiente')
, ('partial', 'es', 'Parcial')
, ('paid', 'es', 'Pagada') , ('paid', 'es', 'Pagada')
on conflict (expense_status, lang_tag) do nothing
; ;
commit; 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 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 { type CheckField struct {
Name string Name string
Label 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) 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 { func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool {
_, err := mail.ParseAddress(field.Val) _, err := mail.ParseAddress(field.Val)
return v.checkInput(field, err == nil, message) 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) 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 { func (v *FormValidator) CheckAtMostOneOfEachGroup(field *SelectField, message string) bool {
repeated := false repeated := false
groups := map[string]bool{} groups := map[string]bool{}
@ -539,3 +574,11 @@ func (v *FormValidator) checkSelect(field *SelectField, ok bool, message string)
} }
return ok 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.PUT("/expenses/:slug/tags", HandleUpdateExpenseTags)
companyRouter.GET("/expenses/:slug/tags/edit", ServeEditExpenseTags) companyRouter.GET("/expenses/:slug/tags/edit", ServeEditExpenseTags)
companyRouter.GET("/expenses/:slug/download/:filename", ServeExpenseAttachment) 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) companyRouter.GET("/", ServeDashboard)
router := httprouter.New() router := httprouter.New()

270
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\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" "PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -35,6 +35,11 @@ msgstr "Afegeix productes a la factura"
#: web/template/expenses/index.gohtml:10 web/template/expenses/edit.gohtml:10 #: 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/tax-details.gohtml:9 web/template/products/new.gohtml:9
#: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10 #: 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" msgctxt "title"
msgid "Home" msgid "Home"
msgstr "Inici" msgstr "Inici"
@ -61,6 +66,7 @@ msgstr "Tots"
#: web/template/invoices/products.gohtml:49 #: web/template/invoices/products.gohtml:49
#: web/template/switch-company.gohtml:22 web/template/quotes/products.gohtml:49 #: web/template/switch-company.gohtml:22 web/template/quotes/products.gohtml:49
#: web/template/products/index.gohtml:45 #: web/template/products/index.gohtml:45
#: web/template/payments/accounts/index.gohtml:25
msgctxt "title" msgctxt "title"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
@ -107,7 +113,7 @@ msgstr "Subtotal"
#: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82 #: 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/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/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" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
@ -115,6 +121,8 @@ msgstr "Total"
#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: 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/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93
#: web/template/expenses/new.gohtml:57 web/template/expenses/edit.gohtml:59 #: 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" msgctxt "action"
msgid "Update" msgid "Update"
msgstr "Actualitza" msgstr "Actualitza"
@ -124,6 +132,8 @@ msgstr "Actualitza"
#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: 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/expenses/new.gohtml:60 web/template/expenses/edit.gohtml:62
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 #: 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" msgctxt "action"
msgid "Save" msgid "Save"
msgstr "Desa" msgstr "Desa"
@ -180,14 +190,14 @@ msgid "Customer"
msgstr "Client" msgstr "Client"
#: web/template/invoices/index.gohtml:70 web/template/quotes/index.gohtml:70 #: 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" msgctxt "title"
msgid "Status" msgid "Status"
msgstr "Estat" msgstr "Estat"
#: web/template/invoices/index.gohtml:71 web/template/quotes/index.gohtml:71 #: 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/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" msgctxt "title"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
@ -326,7 +336,7 @@ msgctxt "input"
msgid "(Max. %s)" msgid "(Max. %s)"
msgstr "(Màx. %s)" msgstr "(Màx. %s)"
#: web/template/form.gohtml:200 #: web/template/form.gohtml:202
msgctxt "action" msgctxt "action"
msgid "Filters" msgid "Filters"
msgstr "Filtra" msgstr "Filtra"
@ -489,15 +499,20 @@ msgstr "Despeses"
#: web/template/app.gohtml:57 #: web/template/app.gohtml:57
msgctxt "nav" msgctxt "nav"
msgid "Payments"
msgstr "Pagaments"
#: web/template/app.gohtml:58
msgctxt "nav"
msgid "Products" msgid "Products"
msgstr "Productes" msgstr "Productes"
#: web/template/app.gohtml:58 #: web/template/app.gohtml:59
msgctxt "nav" msgctxt "nav"
msgid "Contacts" msgid "Contacts"
msgstr "Contactes" msgstr "Contactes"
#: web/template/app.gohtml:66 #: web/template/app.gohtml:67
msgid "<a href=\"https://numerus.cat/\">Numerus</a> Version: %s" msgid "<a href=\"https://numerus.cat/\">Numerus</a> Version: %s"
msgstr "<a href=\"https://numerus.cat/\">Numerus</a> versió: %s" msgstr "<a href=\"https://numerus.cat/\">Numerus</a> versió: %s"
@ -731,6 +746,84 @@ msgctxt "title"
msgid "Edit Product “%s”" msgid "Edit Product “%s”"
msgstr "Edició del producte «%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 #: pkg/ods.go:62 pkg/ods.go:106
msgctxt "title" msgctxt "title"
msgid "VAT number" msgid "VAT number"
@ -762,49 +855,50 @@ msgstr "No podeu deixar la contrasenya en blanc."
msgid "Invalid user or password." msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes." 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 #: pkg/invoices.go:1147 pkg/contacts.go:149 pkg/contacts.go:262
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708 #: 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/payments.go:145 pkg/expenses.go:343 pkg/expenses.go:510
#: pkg/invoices.go:877 pkg/invoices.go:1462 pkg/contacts.go:154 #: pkg/invoices.go:177 pkg/invoices.go:877 pkg/invoices.go:1462
#: pkg/contacts.go:362 #: pkg/contacts.go:154 pkg/contacts.go:362
msgctxt "input" msgctxt "input"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" 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 #: pkg/contacts.go:158
msgctxt "input" msgctxt "input"
msgid "Tags Condition" msgid "Tags Condition"
msgstr "Condició de les etiquetes" 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 #: pkg/contacts.go:162
msgctxt "tag condition" msgctxt "tag condition"
msgid "All" msgid "All"
msgstr "Totes" 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 #: pkg/contacts.go:163
msgid "Invoices must have all the specified labels." msgid "Invoices must have all the specified labels."
msgstr "Les factures han de tenir totes les etiquetes." 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 #: pkg/contacts.go:167
msgctxt "tag condition" msgctxt "tag condition"
msgid "Any" msgid "Any"
msgstr "Qualsevol" 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 #: pkg/contacts.go:168
msgid "Invoices must have at least one of the specified labels." 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." 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" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
@ -1062,7 +1156,7 @@ msgctxt "input"
msgid "Quotation Status" msgid "Quotation Status"
msgstr "Estat del pressupost" 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" msgid "All status"
msgstr "Tots els estats" msgstr "Tots els estats"
@ -1071,12 +1165,12 @@ msgctxt "input"
msgid "Quotation Number" msgid "Quotation Number"
msgstr "Número de pressupost" 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" msgctxt "input"
msgid "From Date" msgid "From Date"
msgstr "A partir de la data" 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" msgctxt "input"
msgid "To Date" msgid "To Date"
msgstr "Fins la data" msgstr "Fins la data"
@ -1097,8 +1191,8 @@ msgstr "pressuposts.zip"
msgid "quotations.ods" msgid "quotations.ods"
msgstr "pressuposts.ods" msgstr "pressuposts.ods"
#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:720 #: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:719
#: pkg/expenses.go:750 pkg/invoices.go:684 pkg/invoices.go:1437 #: pkg/expenses.go:749 pkg/invoices.go:684 pkg/invoices.go:1437
#: pkg/invoices.go:1445 #: pkg/invoices.go:1445
msgid "Invalid action" msgid "Invalid action"
msgstr "Acció invàlida." msgstr "Acció invàlida."
@ -1218,6 +1312,101 @@ msgstr "La confirmació no és igual a la contrasenya."
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid." 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 #: pkg/dashboard.go:138
msgctxt "input" msgctxt "input"
msgid "Period" msgid "Period"
@ -1257,7 +1446,7 @@ msgstr "Any anterior"
msgid "Select a contact." msgid "Select a contact."
msgstr "Escolliu un contacte." msgstr "Escolliu un contacte."
#: pkg/expenses.go:294 pkg/expenses.go:490 #: pkg/expenses.go:294 pkg/expenses.go:489
msgctxt "input" msgctxt "input"
msgid "Contact" msgid "Contact"
msgstr "Contacte" msgstr "Contacte"
@ -1267,22 +1456,12 @@ msgctxt "input"
msgid "Invoice number" msgid "Invoice number"
msgstr "Número de factura" 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 #: pkg/expenses.go:331 pkg/invoices.go:888
msgctxt "input" msgctxt "input"
msgid "File" msgid "File"
msgstr "Fitxer" msgstr "Fitxer"
#: pkg/expenses.go:337 pkg/expenses.go:515 #: pkg/expenses.go:337 pkg/expenses.go:514
msgctxt "input" msgctxt "input"
msgid "Expense Status" msgid "Expense Status"
msgstr "Estat de la despesa" 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." msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida." 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 #: pkg/expenses.go:384
msgid "Selected expense status is not valid." msgid "Selected expense status is not valid."
msgstr "Heu seleccionat un estat de despesa que no és vàlid." msgstr "Heu seleccionat un estat de despesa que no és vàlid."
#: pkg/expenses.go:491 #: pkg/expenses.go:490
msgid "All contacts" msgid "All contacts"
msgstr "Tots els contactes" msgstr "Tots els contactes"
#: pkg/expenses.go:496 pkg/invoices.go:162 #: pkg/expenses.go:495 pkg/invoices.go:162
msgctxt "input" msgctxt "input"
msgid "Invoice Number" msgid "Invoice Number"
msgstr "Número de factura" msgstr "Número de factura"
#: pkg/expenses.go:748 #: pkg/expenses.go:747
msgid "expenses.ods" msgid "expenses.ods"
msgstr "despeses.ods" msgstr "despeses.ods"
@ -1364,11 +1535,6 @@ msgctxt "input"
msgid "Need to input tax details" msgid "Need to input tax details"
msgstr "Necessito poder facturar aquest contacte" msgstr "Necessito poder facturar aquest contacte"
#: pkg/contacts.go:352
msgctxt "input"
msgid "IBAN"
msgstr "IBAN"
#: pkg/contacts.go:357 #: pkg/contacts.go:357
msgctxt "bic" msgctxt "bic"
msgid "BIC" msgid "BIC"
@ -1445,10 +1611,6 @@ msgstr "Fitxer Excel del Holded"
#~ msgid "Product ID can not be empty." #~ msgid "Product ID can not be empty."
#~ msgstr "No podeu deixar lidentificador del producte en blanc." #~ msgstr "No podeu deixar lidentificador del producte en blanc."
#~ msgctxt "input"
#~ msgid "Number"
#~ msgstr "Número"
#~ msgctxt "title" #~ msgctxt "title"
#~ msgid "Label" #~ msgid "Label"
#~ msgstr "Etiqueta" #~ msgstr "Etiqueta"

270
po/es.po
View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\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" "PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -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/expenses/index.gohtml:10 web/template/expenses/edit.gohtml:10
#: web/template/tax-details.gohtml:9 web/template/products/new.gohtml:9 #: 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/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" msgctxt "title"
msgid "Home" msgid "Home"
msgstr "Inicio" msgstr "Inicio"
@ -61,6 +66,7 @@ msgstr "Todos"
#: web/template/invoices/products.gohtml:49 #: web/template/invoices/products.gohtml:49
#: web/template/switch-company.gohtml:22 web/template/quotes/products.gohtml:49 #: web/template/switch-company.gohtml:22 web/template/quotes/products.gohtml:49
#: web/template/products/index.gohtml:45 #: web/template/products/index.gohtml:45
#: web/template/payments/accounts/index.gohtml:25
msgctxt "title" msgctxt "title"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
@ -107,7 +113,7 @@ msgstr "Subtotal"
#: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82 #: 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/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/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" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
@ -115,6 +121,8 @@ msgstr "Total"
#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: 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/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93
#: web/template/expenses/new.gohtml:57 web/template/expenses/edit.gohtml:59 #: 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" msgctxt "action"
msgid "Update" msgid "Update"
msgstr "Actualizar" msgstr "Actualizar"
@ -124,6 +132,8 @@ msgstr "Actualizar"
#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: 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/expenses/new.gohtml:60 web/template/expenses/edit.gohtml:62
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 #: 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" msgctxt "action"
msgid "Save" msgid "Save"
msgstr "Guardad" msgstr "Guardad"
@ -180,14 +190,14 @@ msgid "Customer"
msgstr "Cliente" msgstr "Cliente"
#: web/template/invoices/index.gohtml:70 web/template/quotes/index.gohtml:70 #: 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" msgctxt "title"
msgid "Status" msgid "Status"
msgstr "Estado" msgstr "Estado"
#: web/template/invoices/index.gohtml:71 web/template/quotes/index.gohtml:71 #: 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/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" msgctxt "title"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
@ -326,7 +336,7 @@ msgctxt "input"
msgid "(Max. %s)" msgid "(Max. %s)"
msgstr "(Máx. %s)" msgstr "(Máx. %s)"
#: web/template/form.gohtml:200 #: web/template/form.gohtml:202
msgctxt "action" msgctxt "action"
msgid "Filters" msgid "Filters"
msgstr "Filtrar" msgstr "Filtrar"
@ -489,15 +499,20 @@ msgstr "Gastos"
#: web/template/app.gohtml:57 #: web/template/app.gohtml:57
msgctxt "nav" msgctxt "nav"
msgid "Payments"
msgstr "Pagos"
#: web/template/app.gohtml:58
msgctxt "nav"
msgid "Products" msgid "Products"
msgstr "Productos" msgstr "Productos"
#: web/template/app.gohtml:58 #: web/template/app.gohtml:59
msgctxt "nav" msgctxt "nav"
msgid "Contacts" msgid "Contacts"
msgstr "Contactos" msgstr "Contactos"
#: web/template/app.gohtml:66 #: web/template/app.gohtml:67
msgid "<a href=\"https://numerus.cat/\">Numerus</a> Version: %s" msgid "<a href=\"https://numerus.cat/\">Numerus</a> Version: %s"
msgstr "<a href=\"https://numerus.cat/\">Numerus</a> versión: %s" msgstr "<a href=\"https://numerus.cat/\">Numerus</a> versión: %s"
@ -731,6 +746,84 @@ msgctxt "title"
msgid "Edit Product “%s”" msgid "Edit Product “%s”"
msgstr "Edición del producto «%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 #: pkg/ods.go:62 pkg/ods.go:106
msgctxt "title" msgctxt "title"
msgid "VAT number" msgid "VAT number"
@ -762,49 +855,50 @@ msgstr "No podéis dejar la contraseña en blanco."
msgid "Invalid user or password." msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido." 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 #: pkg/invoices.go:1147 pkg/contacts.go:149 pkg/contacts.go:262
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708 #: 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/payments.go:145 pkg/expenses.go:343 pkg/expenses.go:510
#: pkg/invoices.go:877 pkg/invoices.go:1462 pkg/contacts.go:154 #: pkg/invoices.go:177 pkg/invoices.go:877 pkg/invoices.go:1462
#: pkg/contacts.go:362 #: pkg/contacts.go:154 pkg/contacts.go:362
msgctxt "input" msgctxt "input"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" 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 #: pkg/contacts.go:158
msgctxt "input" msgctxt "input"
msgid "Tags Condition" msgid "Tags Condition"
msgstr "Condición de las etiquetas" 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 #: pkg/contacts.go:162
msgctxt "tag condition" msgctxt "tag condition"
msgid "All" msgid "All"
msgstr "Todas" 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 #: pkg/contacts.go:163
msgid "Invoices must have all the specified labels." msgid "Invoices must have all the specified labels."
msgstr "Las facturas deben tener todas las etiquetas." 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 #: pkg/contacts.go:167
msgctxt "tag condition" msgctxt "tag condition"
msgid "Any" msgid "Any"
msgstr "Cualquiera" 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 #: pkg/contacts.go:168
msgid "Invoices must have at least one of the specified labels." msgid "Invoices must have at least one of the specified labels."
msgstr "Las facturas deben tener como mínimo una de las etiquetas." 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" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripción" msgstr "Descripción"
@ -1062,7 +1156,7 @@ msgctxt "input"
msgid "Quotation Status" msgid "Quotation Status"
msgstr "Estado del presupuesto" 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" msgid "All status"
msgstr "Todos los estados" msgstr "Todos los estados"
@ -1071,12 +1165,12 @@ msgctxt "input"
msgid "Quotation Number" msgid "Quotation Number"
msgstr "Número de presupuesto" 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" msgctxt "input"
msgid "From Date" msgid "From Date"
msgstr "A partir de la fecha" 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" msgctxt "input"
msgid "To Date" msgid "To Date"
msgstr "Hasta la fecha" msgstr "Hasta la fecha"
@ -1097,8 +1191,8 @@ msgstr "presupuestos.zip"
msgid "quotations.ods" msgid "quotations.ods"
msgstr "presupuestos.ods" msgstr "presupuestos.ods"
#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:720 #: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:719
#: pkg/expenses.go:750 pkg/invoices.go:684 pkg/invoices.go:1437 #: pkg/expenses.go:749 pkg/invoices.go:684 pkg/invoices.go:1437
#: pkg/invoices.go:1445 #: pkg/invoices.go:1445
msgid "Invalid action" msgid "Invalid action"
msgstr "Acción inválida." 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." msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido." 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 #: pkg/dashboard.go:138
msgctxt "input" msgctxt "input"
msgid "Period" msgid "Period"
@ -1257,7 +1446,7 @@ msgstr "Año anterior"
msgid "Select a contact." msgid "Select a contact."
msgstr "Escoged un contacto" msgstr "Escoged un contacto"
#: pkg/expenses.go:294 pkg/expenses.go:490 #: pkg/expenses.go:294 pkg/expenses.go:489
msgctxt "input" msgctxt "input"
msgid "Contact" msgid "Contact"
msgstr "Contacto" msgstr "Contacto"
@ -1267,22 +1456,12 @@ msgctxt "input"
msgid "Invoice number" msgid "Invoice number"
msgstr "Número de factura" 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 #: pkg/expenses.go:331 pkg/invoices.go:888
msgctxt "input" msgctxt "input"
msgid "File" msgid "File"
msgstr "Archivo" msgstr "Archivo"
#: pkg/expenses.go:337 pkg/expenses.go:515 #: pkg/expenses.go:337 pkg/expenses.go:514
msgctxt "input" msgctxt "input"
msgid "Expense Status" msgid "Expense Status"
msgstr "Estado del gasto" 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." msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida." 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 #: pkg/expenses.go:384
msgid "Selected expense status is not valid." msgid "Selected expense status is not valid."
msgstr "Habéis escogido un estado de gasto que no es válido." msgstr "Habéis escogido un estado de gasto que no es válido."
#: pkg/expenses.go:491 #: pkg/expenses.go:490
msgid "All contacts" msgid "All contacts"
msgstr "Todos los contactos" msgstr "Todos los contactos"
#: pkg/expenses.go:496 pkg/invoices.go:162 #: pkg/expenses.go:495 pkg/invoices.go:162
msgctxt "input" msgctxt "input"
msgid "Invoice Number" msgid "Invoice Number"
msgstr "Número de factura" msgstr "Número de factura"
#: pkg/expenses.go:748 #: pkg/expenses.go:747
msgid "expenses.ods" msgid "expenses.ods"
msgstr "gastos.ods" msgstr "gastos.ods"
@ -1364,11 +1535,6 @@ msgctxt "input"
msgid "Need to input tax details" msgid "Need to input tax details"
msgstr "Necesito facturar este contacto" msgstr "Necesito facturar este contacto"
#: pkg/contacts.go:352
msgctxt "input"
msgid "IBAN"
msgstr "IBAN"
#: pkg/contacts.go:357 #: pkg/contacts.go:357
msgctxt "bic" msgctxt "bic"
msgid "BIC" msgid "BIC"
@ -1441,10 +1607,6 @@ msgstr "Archivo Excel de Holded"
#~ msgid "Product ID can not be empty." #~ msgid "Product ID can not be empty."
#~ msgstr "No podéis dejar el identificador de producto en blanco." #~ msgstr "No podéis dejar el identificador de producto en blanco."
#~ msgctxt "input"
#~ msgid "Number"
#~ msgstr "Número"
#~ msgctxt "title" #~ msgctxt "title"
#~ msgid "Label" #~ msgid "Label"
#~ msgstr "Etiqueta" #~ 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; begin;
set search_path to numerus; set search_path to numerus;
delete from expense_status_i18n; delete from expense_status_i18n where expense_status = 'partial';
delete from expense_status; delete from expense_status where expense_status = 'partial';
commit; 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 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 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 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; 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 = '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 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 ='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 = '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= 'ca';
select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'es'; 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 "/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 "/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 "/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 "/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> <li><a{{if requestURIHasPrefix (companyURI "/contacts") }} aria-current="page"{{ end }} href="{{ companyURI "/contacts" }}">{{( pgettext "Contacts" "nav" )}}</a></li>
</ul> </ul>

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