Compare commits
2 Commits
d9c93b8797
...
627841d4dd
Author | SHA1 | Date |
---|---|---|
|
627841d4dd | |
|
c037f671f8 |
|
@ -2,9 +2,17 @@ begin;
|
||||||
|
|
||||||
set search_path to auth, numerus, public;
|
set search_path to auth, numerus, public;
|
||||||
|
|
||||||
insert into auth."user" (email, name, password, role)
|
insert into auth."user" (user_id, email, name, password, role)
|
||||||
values ('demo@numerus', 'Demo User', 'demo', 'invoicer')
|
values (1, 'demo@numerus', 'Demo User', 'demo', 'invoicer')
|
||||||
, ('admin@numerus', 'Demo Admin', 'admin', 'admin')
|
, (2, 'admin@numerus', 'Demo Admin', 'admin', 'admin')
|
||||||
|
;
|
||||||
|
|
||||||
|
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country, currency_code)
|
||||||
|
values (1, 'Juli Verd', 'ES40404040D', 'Pesebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de l’Hort', 'Castelló d’Empúries', 'Alt Empordà', '17486', 'Espanya', 'EUR');
|
||||||
|
|
||||||
|
insert into company_user (company_id, user_id)
|
||||||
|
values (1, 1)
|
||||||
|
, (1, 2)
|
||||||
;
|
;
|
||||||
|
|
||||||
commit;
|
commit;
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- Deploy numerus:available_currencies to pg
|
||||||
|
-- requires: schema_numerus
|
||||||
|
-- requires: currency
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
insert into numerus.currency(currency_code, currency_symbol)
|
||||||
|
values ('EUR', '€')
|
||||||
|
, ('USD', '$')
|
||||||
|
;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- Deploy numerus:company to pg
|
||||||
|
-- requires: schema_numerus
|
||||||
|
-- requires: extension_vat
|
||||||
|
-- requires: email
|
||||||
|
-- requires: extension_pg_libphonenumber
|
||||||
|
-- requires: extension_uri
|
||||||
|
-- requires: currency_code
|
||||||
|
-- requires: currency
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
set search_path to numerus,public;
|
||||||
|
|
||||||
|
create table company (
|
||||||
|
company_id serial primary key,
|
||||||
|
slug uuid not null unique default gen_random_uuid(),
|
||||||
|
business_name text not null,
|
||||||
|
vatin vatin not null,
|
||||||
|
trade_name text not null,
|
||||||
|
phone packed_phone_number not null,
|
||||||
|
email email not null,
|
||||||
|
web uri not null,
|
||||||
|
address text not null,
|
||||||
|
city text not null,
|
||||||
|
province text not null,
|
||||||
|
postal_code text not null,
|
||||||
|
country text not null,
|
||||||
|
currency_code currency_code not null references currency,
|
||||||
|
created_at timestamptz not null default current_timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
grant select on table company to invoicer;
|
||||||
|
grant select on table company to admin;
|
||||||
|
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,38 @@
|
||||||
|
-- Deploy numerus:company_user to pg
|
||||||
|
-- requires: schema_numerus
|
||||||
|
-- requires: user
|
||||||
|
-- requires: company
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
set search_path to numerus, auth, public;
|
||||||
|
|
||||||
|
create table company_user (
|
||||||
|
company_id integer not null references company,
|
||||||
|
user_id integer not null references "user",
|
||||||
|
primary key (company_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
grant select on table company_user to invoicer;
|
||||||
|
grant select on table company_user to admin;
|
||||||
|
|
||||||
|
|
||||||
|
alter table company enable row level security;
|
||||||
|
|
||||||
|
create policy company_policy
|
||||||
|
on company
|
||||||
|
using (
|
||||||
|
exists(
|
||||||
|
select 1
|
||||||
|
from company_user
|
||||||
|
join user_profile using (user_id)
|
||||||
|
where company_user.company_id = company.company_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- TODO:
|
||||||
|
-- I think we can not do the same for company_user because it would be
|
||||||
|
-- an infinite loop, but in this case i think it is fine because we can
|
||||||
|
-- only see ids, nothing more.
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- Deploy numerus:currency to pg
|
||||||
|
-- requires: schema_numerus
|
||||||
|
-- requires: currency_code
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
set search_path to numerus, public;
|
||||||
|
|
||||||
|
create table currency (
|
||||||
|
currency_code currency_code not null primary key,
|
||||||
|
currency_symbol text not null,
|
||||||
|
decimal_digits integer not null default 2
|
||||||
|
);
|
||||||
|
|
||||||
|
grant select on table currency to invoicer;
|
||||||
|
grant select on table currency to admin;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- Deploy numerus:currency_code to pg
|
||||||
|
-- requires: schema_numerus
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
set search_path to numerus, public;
|
||||||
|
|
||||||
|
create domain currency_code as text
|
||||||
|
check (value ~ '^[A-Z]{3}$');
|
||||||
|
|
||||||
|
comment on domain currency_code is
|
||||||
|
'A correctly formated, but not necessarily valid, ISO 4217 currency code';
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Deploy numerus:extension_pg_libphonenumber to pg
|
||||||
|
-- requires: schema_public
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
create extension if not exists pg_libphonenumber;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Deploy numerus:extension_uri to pg
|
||||||
|
-- requires: schema_public
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
create extension if not exists uri;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Deploy numerus:extension_vat to pg
|
||||||
|
-- requires: schema_public
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
create extension if not exists vat;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,105 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContextCompanyKey = "numerus-company"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Company struct {
|
||||||
|
Id int
|
||||||
|
Slug string
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompanyHandler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
slug := r.URL.Path
|
||||||
|
if idx := strings.IndexByte(slug, '/'); idx >= 0 {
|
||||||
|
slug = slug[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := getConn(r)
|
||||||
|
company := &Company{
|
||||||
|
Slug: slug,
|
||||||
|
}
|
||||||
|
err := conn.QueryRow(r.Context(), "select company_id from company where slug = $1", slug).Scan(&company.Id)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), ContextCompanyKey, company)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
// Same as StripPrefix
|
||||||
|
p := strings.TrimPrefix(r.URL.Path, slug)
|
||||||
|
rp := strings.TrimPrefix(r.URL.RawPath, slug)
|
||||||
|
if len(p) < len(r.URL.Path) && (r.URL.RawPath == "" || len(rp) < len(r.URL.RawPath)) {
|
||||||
|
r2 := new(http.Request)
|
||||||
|
*r2 = *r
|
||||||
|
r2.URL = new(url.URL)
|
||||||
|
*r2.URL = *r.URL
|
||||||
|
if p == "" {
|
||||||
|
r2.URL.Path = "/"
|
||||||
|
} else {
|
||||||
|
r2.URL.Path = p
|
||||||
|
}
|
||||||
|
r2.URL.RawPath = rp
|
||||||
|
next.ServeHTTP(w, r2)
|
||||||
|
} else {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCompany(r *http.Request) *Company {
|
||||||
|
company := r.Context().Value(ContextCompanyKey)
|
||||||
|
if company == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return company.(*Company)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaxDetailsPage struct {
|
||||||
|
Title string
|
||||||
|
BusinessName string
|
||||||
|
VATIN string
|
||||||
|
TradeName string
|
||||||
|
Phone string
|
||||||
|
Email string
|
||||||
|
Web string
|
||||||
|
Address string
|
||||||
|
City string
|
||||||
|
Province string
|
||||||
|
PostalCode string
|
||||||
|
Country string
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompanyTaxDetailsHandler() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
locale := getLocale(r)
|
||||||
|
page := &TaxDetailsPage{
|
||||||
|
Title: pgettext("title", "Tax Details", locale),
|
||||||
|
}
|
||||||
|
company := mustGetCompany(r)
|
||||||
|
conn := getConn(r)
|
||||||
|
err := conn.QueryRow(r.Context(), "select business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country from company where company_id = $1", company.Id).Scan(&page.BusinessName, &page.VATIN, &page.TradeName, &page.Phone, &page.Email, &page.Web, &page.Address, &page.City, &page.Province, &page.PostalCode, &page.Country);
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
mustRenderAppTemplate(w, r, "tax-details.html", page)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetCompany(r *http.Request) *Company {
|
||||||
|
company := getCompany(r)
|
||||||
|
if company == nil {
|
||||||
|
panic(errors.New("company: required but not found"))
|
||||||
|
}
|
||||||
|
return company;
|
||||||
|
}
|
15
pkg/login.go
15
pkg/login.go
|
@ -61,13 +61,9 @@ func LoginHandler() http.Handler {
|
||||||
|
|
||||||
func LogoutHandler() http.Handler {
|
func LogoutHandler() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUser(r)
|
|
||||||
if user.LoggedIn {
|
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
conn.MustExec(r.Context(), "select logout()")
|
conn.MustExec(r.Context(), "select logout()")
|
||||||
http.SetCookie(w, createSessionCookie("", -24*time.Hour))
|
http.SetCookie(w, createSessionCookie("", -24*time.Hour))
|
||||||
}
|
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,3 +126,14 @@ func getUser(r *http.Request) *AppUser {
|
||||||
func getConn(r *http.Request) *Conn {
|
func getConn(r *http.Request) *Conn {
|
||||||
return r.Context().Value(ContextConnKey).(*Conn)
|
return r.Context().Value(ContextConnKey).(*Conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Authenticated(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := getUser(r)
|
||||||
|
if user.LoggedIn {
|
||||||
|
next.ServeHTTP(w, r);
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -23,10 +23,6 @@ type ProfilePage struct {
|
||||||
func ProfileHandler() http.Handler {
|
func ProfileHandler() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUser(r)
|
user := getUser(r)
|
||||||
if !user.LoggedIn {
|
|
||||||
http.Redirect(w, r, "/login", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
locale := getLocale(r)
|
locale := getLocale(r)
|
||||||
page := ProfilePage{
|
page := ProfilePage{
|
||||||
|
|
|
@ -5,15 +5,28 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRouter(db *Db) http.Handler {
|
func NewRouter(db *Db) http.Handler {
|
||||||
|
companyRouter := http.NewServeMux()
|
||||||
|
companyRouter.Handle("/tax-details", CompanyTaxDetailsHandler())
|
||||||
|
companyRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mustRenderAppTemplate(w, r, "dashboard.html", nil)
|
||||||
|
})
|
||||||
|
|
||||||
router := http.NewServeMux()
|
router := http.NewServeMux()
|
||||||
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
||||||
router.Handle("/login", LoginHandler())
|
router.Handle("/login", LoginHandler())
|
||||||
router.Handle("/logout", LogoutHandler())
|
router.Handle("/logout", Authenticated(LogoutHandler()))
|
||||||
router.Handle("/profile", ProfileHandler())
|
router.Handle("/profile", Authenticated(ProfileHandler()))
|
||||||
|
router.Handle("/company/", Authenticated(http.StripPrefix("/company/", CompanyHandler(companyRouter))))
|
||||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUser(r)
|
user := getUser(r)
|
||||||
if user.LoggedIn {
|
if user.LoggedIn {
|
||||||
mustRenderAppTemplate(w, r, "dashboard.html", nil)
|
conn := getConn(r)
|
||||||
|
var slug string
|
||||||
|
err := conn.QueryRow(r.Context(), "select slug::text from company order by company_id limit 1").Scan(&slug)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "/company/"+slug, http.StatusFound)
|
||||||
} else {
|
} else {
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,17 @@ func templateFile (name string) string {
|
||||||
|
|
||||||
func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename string, data interface{}) {
|
func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename string, data interface{}) {
|
||||||
locale := getLocale(r)
|
locale := getLocale(r)
|
||||||
|
company := getCompany(r)
|
||||||
t := template.New(filename)
|
t := template.New(filename)
|
||||||
t.Funcs(template.FuncMap{
|
t.Funcs(template.FuncMap{
|
||||||
"gettext": locale.Get,
|
"gettext": locale.Get,
|
||||||
"pgettext": locale.GetC,
|
"pgettext": locale.GetC,
|
||||||
|
"companyURI": func(uri string) string {
|
||||||
|
if company == nil {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
return "/company/" + company.Slug + uri;
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if _, err := t.ParseFiles(templateFile(filename), templateFile(layout)); err != nil {
|
if _, err := t.ParseFiles(templateFile(filename), templateFile(layout)); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
86
po/ca.po
86
po/ca.po
|
@ -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: 2023-01-23 18:50+0100\n"
|
"POT-Creation-Date: 2023-01-24 21:37+0100\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"
|
||||||
|
@ -26,12 +26,13 @@ msgstr "Entrada"
|
||||||
msgid "Invalid user or password"
|
msgid "Invalid user or password"
|
||||||
msgstr "Nom d’usuari o contrasenya incorrectes"
|
msgstr "Nom d’usuari o contrasenya incorrectes"
|
||||||
|
|
||||||
#: web/template/login.html:13 web/template/profile.html:14
|
#: web/template/login.html:13 web/template/profile.html:15
|
||||||
|
#: web/template/tax-details.html:23
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "Correu-e"
|
msgstr "Correu-e"
|
||||||
|
|
||||||
#: web/template/login.html:18 web/template/profile.html:22
|
#: web/template/login.html:18 web/template/profile.html:23
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr "Contrasenya"
|
msgstr "Contrasenya"
|
||||||
|
@ -41,51 +42,112 @@ msgctxt "action"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entra"
|
msgstr "Entra"
|
||||||
|
|
||||||
#: web/template/profile.html:2 pkg/profile.go:33
|
#: web/template/profile.html:3 pkg/profile.go:29
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "User Settings"
|
msgid "User Settings"
|
||||||
msgstr "Configuració usuari"
|
msgstr "Configuració usuari"
|
||||||
|
|
||||||
#: web/template/profile.html:5
|
#: web/template/profile.html:6
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "User Access Data"
|
msgid "User Access Data"
|
||||||
msgstr "Dades accés usuari"
|
msgstr "Dades accés usuari"
|
||||||
|
|
||||||
#: web/template/profile.html:9
|
#: web/template/profile.html:10
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "User name"
|
msgid "User name"
|
||||||
msgstr "Nom d’usuari"
|
msgstr "Nom d’usuari"
|
||||||
|
|
||||||
#: web/template/profile.html:18
|
#: web/template/profile.html:19
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Password Change"
|
msgid "Password Change"
|
||||||
msgstr "Canvi contrasenya"
|
msgstr "Canvi contrasenya"
|
||||||
|
|
||||||
#: web/template/profile.html:27
|
#: web/template/profile.html:28
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password Confirmation"
|
msgid "Password Confirmation"
|
||||||
msgstr "Confirmació contrasenya"
|
msgstr "Confirmació contrasenya"
|
||||||
|
|
||||||
#: web/template/profile.html:31
|
#: web/template/profile.html:33
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Idioma"
|
msgstr "Idioma"
|
||||||
|
|
||||||
#: web/template/profile.html:33
|
#: web/template/profile.html:36
|
||||||
msgctxt "language option"
|
msgctxt "language option"
|
||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr "Automàtic"
|
msgstr "Automàtic"
|
||||||
|
|
||||||
#: web/template/profile.html:38
|
#: web/template/profile.html:42
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Save changes"
|
msgid "Save changes"
|
||||||
msgstr "Desa canvis"
|
msgstr "Desa canvis"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:3 pkg/company.go:87
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Tax Details"
|
||||||
|
msgstr "Configuració fiscal"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:7
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Business name"
|
||||||
|
msgstr "Nom i cognom"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:11
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "VAT number"
|
||||||
|
msgstr "DNI / NIF"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:15
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Trade name"
|
||||||
|
msgstr "Nom comercial"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:19
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Phone"
|
||||||
|
msgstr "Telèfon"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:27
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Web"
|
||||||
|
msgstr "Web"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:31
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Address"
|
||||||
|
msgstr "Adreça"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:35
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "City"
|
||||||
|
msgstr "Població"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:39
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Province"
|
||||||
|
msgstr "Província"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:43
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Postal code"
|
||||||
|
msgstr "Codi postal"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:47
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Country"
|
||||||
|
msgstr "País"
|
||||||
|
|
||||||
#: web/template/app.html:20
|
#: web/template/app.html:20
|
||||||
|
msgctxt "menu"
|
||||||
msgid "Account"
|
msgid "Account"
|
||||||
msgstr "Compte"
|
msgstr "Compte"
|
||||||
|
|
||||||
#: web/template/app.html:27
|
#: web/template/app.html:26
|
||||||
|
msgctxt "menu"
|
||||||
|
msgid "Tax Details"
|
||||||
|
msgstr "Configuració fiscal"
|
||||||
|
|
||||||
|
#: web/template/app.html:33
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr "Surt"
|
msgstr "Surt"
|
||||||
|
|
86
po/es.po
86
po/es.po
|
@ -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: 2023-01-23 18:50+0100\n"
|
"POT-Creation-Date: 2023-01-24 21:37+0100\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"
|
||||||
|
@ -26,12 +26,13 @@ msgstr "Entrada"
|
||||||
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"
|
||||||
|
|
||||||
#: web/template/login.html:13 web/template/profile.html:14
|
#: web/template/login.html:13 web/template/profile.html:15
|
||||||
|
#: web/template/tax-details.html:23
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "Correo-e"
|
msgstr "Correo-e"
|
||||||
|
|
||||||
#: web/template/login.html:18 web/template/profile.html:22
|
#: web/template/login.html:18 web/template/profile.html:23
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr "Contraseña"
|
msgstr "Contraseña"
|
||||||
|
@ -41,51 +42,112 @@ msgctxt "action"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entrar"
|
msgstr "Entrar"
|
||||||
|
|
||||||
#: web/template/profile.html:2 pkg/profile.go:33
|
#: web/template/profile.html:3 pkg/profile.go:29
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "User Settings"
|
msgid "User Settings"
|
||||||
msgstr "Configuración usuario"
|
msgstr "Configuración usuario"
|
||||||
|
|
||||||
#: web/template/profile.html:5
|
#: web/template/profile.html:6
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "User Access Data"
|
msgid "User Access Data"
|
||||||
msgstr "Datos acceso usuario"
|
msgstr "Datos acceso usuario"
|
||||||
|
|
||||||
#: web/template/profile.html:9
|
#: web/template/profile.html:10
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "User name"
|
msgid "User name"
|
||||||
msgstr "Nombre de usuario"
|
msgstr "Nombre de usuario"
|
||||||
|
|
||||||
#: web/template/profile.html:18
|
#: web/template/profile.html:19
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Password Change"
|
msgid "Password Change"
|
||||||
msgstr "Cambio de contraseña"
|
msgstr "Cambio de contraseña"
|
||||||
|
|
||||||
#: web/template/profile.html:27
|
#: web/template/profile.html:28
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password Confirmation"
|
msgid "Password Confirmation"
|
||||||
msgstr "Confirmación contrasenya"
|
msgstr "Confirmación contrasenya"
|
||||||
|
|
||||||
#: web/template/profile.html:31
|
#: web/template/profile.html:33
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Idioma"
|
msgstr "Idioma"
|
||||||
|
|
||||||
#: web/template/profile.html:33
|
#: web/template/profile.html:36
|
||||||
msgctxt "language option"
|
msgctxt "language option"
|
||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr "Automático"
|
msgstr "Automático"
|
||||||
|
|
||||||
#: web/template/profile.html:38
|
#: web/template/profile.html:42
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Save changes"
|
msgid "Save changes"
|
||||||
msgstr "Guardar cambios"
|
msgstr "Guardar cambios"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:3 pkg/company.go:87
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Tax Details"
|
||||||
|
msgstr "Configuración fiscal"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:7
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Business name"
|
||||||
|
msgstr "Nombre y apellidos"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:11
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "VAT number"
|
||||||
|
msgstr "DNI / NIF"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:15
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Trade name"
|
||||||
|
msgstr "Nombre comercial"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:19
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Phone"
|
||||||
|
msgstr "Teléfono"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:27
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Web"
|
||||||
|
msgstr "Web"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:31
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Address"
|
||||||
|
msgstr "Dirección"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:35
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "City"
|
||||||
|
msgstr "Población"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:39
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Province"
|
||||||
|
msgstr "Provincia"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:43
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Postal code"
|
||||||
|
msgstr "Código postal"
|
||||||
|
|
||||||
|
#: web/template/tax-details.html:47
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Country"
|
||||||
|
msgstr "País"
|
||||||
|
|
||||||
#: web/template/app.html:20
|
#: web/template/app.html:20
|
||||||
|
msgctxt "menu"
|
||||||
msgid "Account"
|
msgid "Account"
|
||||||
msgstr "Cuenta"
|
msgstr "Cuenta"
|
||||||
|
|
||||||
#: web/template/app.html:27
|
#: web/template/app.html:26
|
||||||
|
msgctxt "menu"
|
||||||
|
msgid "Tax Details"
|
||||||
|
msgstr "Configuración fiscal"
|
||||||
|
|
||||||
|
#: web/template/app.html:33
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr "Salir"
|
msgstr "Salir"
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert numerus:available_currencies from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
delete from numerus.currency;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert numerus:company from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop table numerus.company;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,9 @@
|
||||||
|
-- Revert numerus:company_user from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop policy if exists company_policy on numerus.company;
|
||||||
|
drop policy if exists company_policy on numerus.company_user;
|
||||||
|
drop table if exists numerus.company_user;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert numerus:currency from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop table numerus.currency;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert numerus:currency_code from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop domain if exists numerus.currency_code;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert numerus:extension_pg_libphonenumber from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop extension if exists pg_libphonenumber;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert numerus:extension_uri from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop extension if exists uri;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert numerus:extension_vat from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop extension if exists vat;
|
||||||
|
|
||||||
|
commit;
|
12
sqitch.plan
12
sqitch.plan
|
@ -9,11 +9,11 @@ schema_numerus [roles] 2023-01-12T22:57:22Z jordi fita mas <jordi@tandem.blog> #
|
||||||
extension_citext [schema_public] 2023-01-12T23:03:33Z jordi fita mas <jordi@tandem.blog> # Add citext extension
|
extension_citext [schema_public] 2023-01-12T23:03:33Z jordi fita mas <jordi@tandem.blog> # Add citext extension
|
||||||
email [schema_numerus extension_citext] 2023-01-12T23:09:59Z jordi fita mas <jordi@tandem.blog> # Add email domain
|
email [schema_numerus extension_citext] 2023-01-12T23:09:59Z jordi fita mas <jordi@tandem.blog> # Add email domain
|
||||||
language [schema_numerus] 2023-01-21T20:55:49Z jordi fita mas <jordi@tandem.blog> # Add relation of available languages
|
language [schema_numerus] 2023-01-21T20:55:49Z jordi fita mas <jordi@tandem.blog> # Add relation of available languages
|
||||||
user [roles schema_auth email language] 2023-01-12T23:44:03Z jordi fita mas <jordi@tandem.blog> # Create user table
|
user [roles schema_auth email language] 2023-01-12T23:44:03Z jordi fita mas <jordi@tandem.blog> # Create user relation
|
||||||
ensure_role_exists [schema_auth user] 2023-01-12T23:57:59Z jordi fita mas <jordi@tandem.blog> # Add trigger to ensure the user’s role exists
|
ensure_role_exists [schema_auth user] 2023-01-12T23:57:59Z jordi fita mas <jordi@tandem.blog> # Add trigger to ensure the user’s role exists
|
||||||
extension_pgcrypto [schema_auth] 2023-01-13T00:11:50Z jordi fita mas <jordi@tandem.blog> # Add pgcrypto extension
|
extension_pgcrypto [schema_auth] 2023-01-13T00:11:50Z jordi fita mas <jordi@tandem.blog> # Add pgcrypto extension
|
||||||
encrypt_password [schema_auth user extension_pgcrypto] 2023-01-13T00:14:30Z jordi fita mas <jordi@tandem.blog> # Add trigger to encrypt user’s password
|
encrypt_password [schema_auth user extension_pgcrypto] 2023-01-13T00:14:30Z jordi fita mas <jordi@tandem.blog> # Add trigger to encrypt user’s password
|
||||||
login_attempt [schema_auth] 2023-01-17T14:05:49Z jordi fita mas <jordi@tandem.blog> # Add table to log login attempts
|
login_attempt [schema_auth] 2023-01-17T14:05:49Z jordi fita mas <jordi@tandem.blog> # Add relation to log login attempts
|
||||||
login [roles schema_numerus schema_auth extension_pgcrypto email user login_attempt] 2023-01-13T00:32:32Z jordi fita mas <jordi@tandem.blog> # Add function to login
|
login [roles schema_numerus schema_auth extension_pgcrypto email user login_attempt] 2023-01-13T00:32:32Z jordi fita mas <jordi@tandem.blog> # Add function to login
|
||||||
current_user_cookie [schema_numerus] 2023-01-21T20:16:28Z jordi fita mas <jordi@tandem.blog> # Add function to get the cookie of the current Numerus’ user
|
current_user_cookie [schema_numerus] 2023-01-21T20:16:28Z jordi fita mas <jordi@tandem.blog> # Add function to get the cookie of the current Numerus’ user
|
||||||
current_user_email [schema_numerus] 2023-01-23T19:11:53Z jordi fita mas <jordi@tandem.blog> # Add function to get the email of the current Numerus’ user
|
current_user_email [schema_numerus] 2023-01-23T19:11:53Z jordi fita mas <jordi@tandem.blog> # Add function to get the email of the current Numerus’ user
|
||||||
|
@ -24,3 +24,11 @@ set_cookie [schema_public check_cookie] 2023-01-19T11:00:22Z jordi fita mas <jor
|
||||||
available_languages [schema_numerus language] 2023-01-21T21:11:08Z jordi fita mas <jordi@tandem.blog> # Add the initial available languages
|
available_languages [schema_numerus language] 2023-01-21T21:11:08Z jordi fita mas <jordi@tandem.blog> # Add the initial available languages
|
||||||
user_profile [schema_numerus user current_user_email current_user_cookie] 2023-01-21T23:18:20Z jordi fita mas <jordi@tandem.blog> # Add view for user profile
|
user_profile [schema_numerus user current_user_email current_user_cookie] 2023-01-21T23:18:20Z jordi fita mas <jordi@tandem.blog> # Add view for user profile
|
||||||
change_password [schema_numerus user] 2023-01-23T20:22:45Z jordi fita mas <jordi@tandem.blog> # Add function to change the current user’s password
|
change_password [schema_numerus user] 2023-01-23T20:22:45Z jordi fita mas <jordi@tandem.blog> # Add function to change the current user’s password
|
||||||
|
extension_vat [schema_public] 2023-01-24T10:28:17Z jordi fita mas <jordi@tandem.blog> # Add vat extension
|
||||||
|
extension_pg_libphonenumber [schema_public] 2023-01-24T13:50:14Z jordi fita mas <jordi@tandem.blog> # Add extension for phone numbers
|
||||||
|
extension_uri [schema_public] 2023-01-24T14:29:29Z jordi fita mas <jordi@tandem.blog> # Add extension for URIs
|
||||||
|
currency_code [schema_numerus] 2023-01-24T14:36:04Z jordi fita mas <jordi@tandem.blog> # Add the domain for currency code in ISO 4217
|
||||||
|
currency [schema_numerus currency_code] 2023-01-24T14:45:26Z jordi fita mas <jordi@tandem.blog> # Add the relation for currencies
|
||||||
|
available_currencies [schema_numerus currency] 2023-01-24T14:54:18Z jordi fita mas <jordi@tandem.blog> # Add the initial list of available currencies
|
||||||
|
company [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri currency_code currency] 2023-01-24T15:03:15Z jordi fita mas <jordi@tandem.blog> # Add the relation for companies
|
||||||
|
company_user [schema_numerus user company] 2023-01-24T17:50:06Z jordi fita mas <jordi@tandem.blog> # Add the relation of companies and their users
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
-- Test company
|
||||||
|
set client_min_messages to warning;
|
||||||
|
create extension if not exists pgtap;
|
||||||
|
reset client_min_messages;
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select plan(77);
|
||||||
|
|
||||||
|
set search_path to numerus, auth, public;
|
||||||
|
|
||||||
|
select has_table('company');
|
||||||
|
select has_pk('company');
|
||||||
|
select table_privs_are('company', 'guest', array []::text[]);
|
||||||
|
select table_privs_are('company', 'invoicer', array ['SELECT']);
|
||||||
|
select table_privs_are('company', 'admin', array ['SELECT']);
|
||||||
|
select table_privs_are('company', 'authenticator', array []::text[]);
|
||||||
|
|
||||||
|
select has_column('company', 'company_id');
|
||||||
|
select col_is_pk('company', 'company_id');
|
||||||
|
select col_type_is('company', 'company_id', 'integer');
|
||||||
|
select col_not_null('company', 'company_id');
|
||||||
|
select col_has_default('company', 'company_id');
|
||||||
|
select col_default_is('company', 'company_id', 'nextval(''company_company_id_seq''::regclass)');
|
||||||
|
|
||||||
|
select has_column('company', 'slug');
|
||||||
|
select col_is_unique('company', 'slug');
|
||||||
|
select col_type_is('company', 'slug', 'uuid');
|
||||||
|
select col_not_null('company', 'slug');
|
||||||
|
select col_has_default('company', 'slug');
|
||||||
|
select col_default_is('company', 'slug', 'gen_random_uuid()');
|
||||||
|
|
||||||
|
select has_column('company', 'business_name');
|
||||||
|
select col_type_is('company', 'business_name', 'text');
|
||||||
|
select col_not_null('company', 'business_name');
|
||||||
|
select col_hasnt_default('company', 'business_name');
|
||||||
|
|
||||||
|
select has_column('company', 'vatin');
|
||||||
|
select col_type_is('company', 'vatin', 'vatin');
|
||||||
|
select col_not_null('company', 'vatin');
|
||||||
|
select col_hasnt_default('company', 'vatin');
|
||||||
|
|
||||||
|
select has_column('company', 'trade_name');
|
||||||
|
select col_type_is('company', 'trade_name', 'text');
|
||||||
|
select col_not_null('company', 'trade_name');
|
||||||
|
select col_hasnt_default('company', 'trade_name');
|
||||||
|
|
||||||
|
select has_column('company', 'phone');
|
||||||
|
select col_type_is('company', 'phone', 'packed_phone_number');
|
||||||
|
select col_not_null('company', 'phone');
|
||||||
|
select col_hasnt_default('company', 'phone');
|
||||||
|
|
||||||
|
select has_column('company', 'email');
|
||||||
|
select col_type_is('company', 'email', 'email');
|
||||||
|
select col_not_null('company', 'email');
|
||||||
|
select col_hasnt_default('company', 'email');
|
||||||
|
|
||||||
|
select has_column('company', 'web');
|
||||||
|
select col_type_is('company', 'web', 'uri');
|
||||||
|
select col_not_null('company', 'web');
|
||||||
|
select col_hasnt_default('company', 'web');
|
||||||
|
|
||||||
|
select has_column('company', 'address');
|
||||||
|
select col_type_is('company', 'address', 'text');
|
||||||
|
select col_not_null('company', 'address');
|
||||||
|
select col_hasnt_default('company', 'address');
|
||||||
|
|
||||||
|
select has_column('company', 'city');
|
||||||
|
select col_type_is('company', 'city', 'text');
|
||||||
|
select col_not_null('company', 'city');
|
||||||
|
select col_hasnt_default('company', 'city');
|
||||||
|
|
||||||
|
select has_column('company', 'province');
|
||||||
|
select col_type_is('company', 'province', 'text');
|
||||||
|
select col_not_null('company', 'province');
|
||||||
|
select col_hasnt_default('company', 'province');
|
||||||
|
|
||||||
|
select has_column('company', 'postal_code');
|
||||||
|
select col_type_is('company', 'postal_code', 'text');
|
||||||
|
select col_not_null('company', 'postal_code');
|
||||||
|
select col_hasnt_default('company', 'postal_code');
|
||||||
|
|
||||||
|
select has_column('company', 'country');
|
||||||
|
select col_type_is('company', 'country', 'text');
|
||||||
|
select col_not_null('company', 'country');
|
||||||
|
select col_hasnt_default('company', 'country');
|
||||||
|
|
||||||
|
select has_column('company', 'currency_code');
|
||||||
|
select col_is_fk('company', 'currency_code');
|
||||||
|
select fk_ok('company', 'currency_code', 'currency', 'currency_code');
|
||||||
|
select col_type_is('company', 'currency_code', 'currency_code');
|
||||||
|
select col_not_null('company', 'currency_code');
|
||||||
|
select col_hasnt_default('company', 'currency_code');
|
||||||
|
|
||||||
|
select has_column('company', 'created_at');
|
||||||
|
select col_type_is('company', 'created_at', 'timestamp with time zone');
|
||||||
|
select col_not_null('company', 'created_at');
|
||||||
|
select col_has_default('company', 'created_at');
|
||||||
|
select col_default_is('company', 'created_at', current_timestamp);
|
||||||
|
|
||||||
|
|
||||||
|
set client_min_messages to warning;
|
||||||
|
truncate company_user 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')
|
||||||
|
;
|
||||||
|
|
||||||
|
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country, currency_code)
|
||||||
|
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'EUR')
|
||||||
|
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'USD')
|
||||||
|
, (6, 'Company 6', 'XX345', '', '777-777-777', 'c@c', '', '', '', '', '', '', 'USD')
|
||||||
|
;
|
||||||
|
|
||||||
|
insert into company_user (company_id, user_id)
|
||||||
|
values (2, 1)
|
||||||
|
, (2, 5)
|
||||||
|
, (4, 1)
|
||||||
|
, (6, 5)
|
||||||
|
;
|
||||||
|
|
||||||
|
prepare company_data as
|
||||||
|
select company_id, business_name
|
||||||
|
from company
|
||||||
|
order by company_id;
|
||||||
|
|
||||||
|
set role invoicer;
|
||||||
|
select is_empty('company_data', 'Should show no data when cookie is not set yet');
|
||||||
|
reset role;
|
||||||
|
|
||||||
|
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
|
||||||
|
select results_eq(
|
||||||
|
'company_data',
|
||||||
|
$$ values ( 2, 'Company 2' )
|
||||||
|
, ( 4, 'Company 4' )
|
||||||
|
$$,
|
||||||
|
'Should only list companies where demo@tandem.blog is user of'
|
||||||
|
);
|
||||||
|
reset role;
|
||||||
|
|
||||||
|
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
|
||||||
|
select results_eq(
|
||||||
|
'company_data',
|
||||||
|
$$ values ( 2, 'Company 2' )
|
||||||
|
, ( 6, 'Company 6' )
|
||||||
|
$$,
|
||||||
|
'Should only list companies where admin@tandem.blog is user of'
|
||||||
|
);
|
||||||
|
reset role;
|
||||||
|
|
||||||
|
select set_cookie('not-a-cookie');
|
||||||
|
select throws_ok(
|
||||||
|
'company_data',
|
||||||
|
'42501', 'permission denied for table company',
|
||||||
|
'Should not allow select to guest users'
|
||||||
|
);
|
||||||
|
reset role;
|
||||||
|
|
||||||
|
select finish();
|
||||||
|
rollback;
|
|
@ -0,0 +1,39 @@
|
||||||
|
-- Test company_user
|
||||||
|
set client_min_messages to warning;
|
||||||
|
create extension if not exists pgtap;
|
||||||
|
reset client_min_messages;
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select plan(19);
|
||||||
|
|
||||||
|
set search_path to numerus, auth, public;
|
||||||
|
|
||||||
|
select has_table('company_user');
|
||||||
|
select has_pk('company_user' );
|
||||||
|
select col_is_pk('company_user', array['company_id', 'user_id']);
|
||||||
|
select table_privs_are('company_user', 'guest', array []::text[]);
|
||||||
|
select table_privs_are('company_user', 'invoicer', array ['SELECT']);
|
||||||
|
select table_privs_are('company_user', 'admin', array ['SELECT']);
|
||||||
|
select table_privs_are('company_user', 'authenticator', array []::text[]);
|
||||||
|
|
||||||
|
select has_column('company_user', 'company_id');
|
||||||
|
select col_is_fk('company_user', 'company_id');
|
||||||
|
select fk_ok('company_user', 'company_id', 'company', 'company_id');
|
||||||
|
select col_type_is('company_user', 'company_id', 'integer');
|
||||||
|
select col_not_null('company_user', 'company_id');
|
||||||
|
select col_hasnt_default('company_user', 'company_id');
|
||||||
|
|
||||||
|
select has_column('company_user', 'user_id');
|
||||||
|
select col_is_fk('company_user', 'user_id');
|
||||||
|
select fk_ok('company_user', 'user_id', 'user', 'user_id');
|
||||||
|
select col_type_is('company_user', 'user_id', 'integer');
|
||||||
|
select col_not_null('company_user', 'user_id');
|
||||||
|
select col_hasnt_default('company_user', 'user_id');
|
||||||
|
|
||||||
|
|
||||||
|
select *
|
||||||
|
from finish();
|
||||||
|
|
||||||
|
rollback;
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
-- Test currency
|
||||||
|
set client_min_messages to warning;
|
||||||
|
create extension if not exists pgtap;
|
||||||
|
reset client_min_messages;
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select plan(20);
|
||||||
|
|
||||||
|
set search_path to numerus, public;
|
||||||
|
|
||||||
|
select has_table('currency');
|
||||||
|
select has_pk('currency');
|
||||||
|
select table_privs_are('currency', 'guest', array []::text[]);
|
||||||
|
select table_privs_are('currency', 'invoicer', array ['SELECT']);
|
||||||
|
select table_privs_are('currency', 'admin', array ['SELECT']);
|
||||||
|
select table_privs_are('currency', 'authenticator', array []::text[]);
|
||||||
|
|
||||||
|
select has_column('currency', 'currency_code');
|
||||||
|
select col_is_pk('currency', 'currency_code');
|
||||||
|
select col_type_is('currency', 'currency_code', 'currency_code');
|
||||||
|
select col_not_null('currency', 'currency_code');
|
||||||
|
select col_hasnt_default('currency', 'currency_code');
|
||||||
|
|
||||||
|
select has_column('currency', 'currency_symbol');
|
||||||
|
select col_type_is('currency', 'currency_symbol', 'text');
|
||||||
|
select col_not_null('currency', 'currency_symbol');
|
||||||
|
select col_hasnt_default('currency', 'currency_symbol');
|
||||||
|
|
||||||
|
select has_column('currency', 'decimal_digits');
|
||||||
|
select col_type_is('currency', 'decimal_digits', 'integer');
|
||||||
|
select col_not_null('currency', 'decimal_digits');
|
||||||
|
select col_has_default('currency', 'decimal_digits');
|
||||||
|
select col_default_is('currency', 'decimal_digits', 2);
|
||||||
|
|
||||||
|
set client_min_messages to warning;
|
||||||
|
truncate currency cascade;
|
||||||
|
reset client_min_messages;
|
||||||
|
|
||||||
|
|
||||||
|
select finish();
|
||||||
|
rollback;
|
|
@ -0,0 +1,38 @@
|
||||||
|
-- Test currency_code
|
||||||
|
set client_min_messages to warning;
|
||||||
|
create extension if not exists pgtap;
|
||||||
|
reset client_min_messages;
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select plan(6);
|
||||||
|
|
||||||
|
set search_path to numerus, public;
|
||||||
|
|
||||||
|
select has_domain('currency_code');
|
||||||
|
select domain_type_is('currency_code', 'text');
|
||||||
|
|
||||||
|
select lives_ok($$ select 'EUR'::currency_code $$, 'Should be able to cast valid text to currency code');
|
||||||
|
|
||||||
|
select throws_ok(
|
||||||
|
$$ SELECT '123'::currency_code $$,
|
||||||
|
23514, null,
|
||||||
|
'Should reject numeric text'
|
||||||
|
);
|
||||||
|
|
||||||
|
select throws_ok(
|
||||||
|
$$ SELECT 'eur'::currency_code $$,
|
||||||
|
23514, null,
|
||||||
|
'Should reject lowecase text'
|
||||||
|
);
|
||||||
|
|
||||||
|
select throws_ok(
|
||||||
|
$$ SELECT 'EURO'::currency_code $$,
|
||||||
|
23514, null,
|
||||||
|
'Should reject text longer than three letters'
|
||||||
|
);
|
||||||
|
|
||||||
|
select *
|
||||||
|
from finish();
|
||||||
|
|
||||||
|
rollback;
|
|
@ -9,9 +9,12 @@ select plan(1);
|
||||||
|
|
||||||
select extensions_are(array[
|
select extensions_are(array[
|
||||||
'citext'
|
'citext'
|
||||||
|
, 'pg_libphonenumber'
|
||||||
, 'pgtap'
|
, 'pgtap'
|
||||||
, 'pgcrypto'
|
, 'pgcrypto'
|
||||||
, 'plpgsql'
|
, 'plpgsql'
|
||||||
|
, 'uri'
|
||||||
|
, 'vat'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
select *
|
select *
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- Verify numerus:available_currencies on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
set search_path to numerus;
|
||||||
|
|
||||||
|
select 1 / count(*)
|
||||||
|
from currency
|
||||||
|
where currency_code = 'EUR'
|
||||||
|
and currency_symbol = '€'
|
||||||
|
and decimal_digits = 2
|
||||||
|
;
|
||||||
|
|
||||||
|
select 1 / count(*)
|
||||||
|
from currency
|
||||||
|
where currency_code = 'USD'
|
||||||
|
and currency_symbol = '$'
|
||||||
|
and decimal_digits = 2
|
||||||
|
;
|
||||||
|
|
||||||
|
rollback;
|
|
@ -0,0 +1,23 @@
|
||||||
|
-- Verify numerus:company on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select company_id
|
||||||
|
, slug
|
||||||
|
, business_name
|
||||||
|
, vatin
|
||||||
|
, trade_name
|
||||||
|
, phone
|
||||||
|
, email
|
||||||
|
, web
|
||||||
|
, address
|
||||||
|
, city
|
||||||
|
, province
|
||||||
|
, postal_code
|
||||||
|
, country
|
||||||
|
, currency_code
|
||||||
|
, created_at
|
||||||
|
from numerus.company
|
||||||
|
where false;
|
||||||
|
|
||||||
|
rollback;
|
|
@ -0,0 +1,10 @@
|
||||||
|
-- Verify numerus:company_user on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select company_id
|
||||||
|
, user_id
|
||||||
|
from numerus.company_user
|
||||||
|
where false;
|
||||||
|
|
||||||
|
rollback;
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- Verify numerus:currency on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select currency_code
|
||||||
|
, currency_symbol
|
||||||
|
, decimal_digits
|
||||||
|
from numerus.currency
|
||||||
|
where false;
|
||||||
|
|
||||||
|
rollback;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Verify numerus:currency_code on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select pg_catalog.has_type_privilege('numerus.currency_code', 'usage');
|
||||||
|
|
||||||
|
rollback;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Verify numerus:extension_pg_libphonenumber on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select 1/count(*) from pg_extension where extname = 'pg_libphonenumber';
|
||||||
|
|
||||||
|
rollback;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Verify numerus:extension_uri on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select 1/count(*) from pg_extension where extname = 'uri';
|
||||||
|
|
||||||
|
rollback;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Verify numerus:extension_vat on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select 1/count(*) from pg_extension where extname = 'vat';
|
||||||
|
|
||||||
|
rollback;
|
|
@ -245,9 +245,10 @@ main {
|
||||||
.input {
|
.input {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"], input[type="password"], input[type="email"], select {
|
input[type="text"], input[type="password"], input[type="email"], input[type="tel"], input[type="url"], select {
|
||||||
background-color: var(--numerus--background-color);
|
background-color: var(--numerus--background-color);
|
||||||
border: 1px solid var(--numerus--color--black);
|
border: 1px solid var(--numerus--color--black);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
@ -255,6 +256,10 @@ input[type="text"], input[type="password"], input[type="email"], select {
|
||||||
min-width: 30rem;
|
min-width: 30rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.width-2x {
|
||||||
|
min-width: 60.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
.input input::placeholder {
|
.input input::placeholder {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
@ -309,7 +314,7 @@ fieldset {
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width legend {
|
.full-width legend {
|
||||||
margin-bottom: initial;
|
margin-bottom: -1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,13 @@
|
||||||
<li role="presentation">
|
<li role="presentation">
|
||||||
<a role="menuitem" href="/profile">
|
<a role="menuitem" href="/profile">
|
||||||
<i class="ri-account-circle-line"></i>
|
<i class="ri-account-circle-line"></i>
|
||||||
{{( gettext "Account" )}}
|
{{( pgettext "Account" "menu" )}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation">
|
||||||
|
<a role="menuitem" href="{{ companyURI "/tax-details" }}">
|
||||||
|
<i class="ri-vip-diamond-line"></i>
|
||||||
|
{{( pgettext "Tax Details" "menu" )}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation">
|
<li role="presentation">
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<section class="dialog-content">
|
||||||
|
<h2>{{(pgettext "Tax Details" "title")}}</h2>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="business_name" id="business_name" required="required" value="{{ .BusinessName }}" placeholder="{{( pgettext "Business name" "input" )}}">
|
||||||
|
<label for="business_name">{{( pgettext "Business name" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="vatin" id="vatin" required="required" value="{{ .VATIN }}" placeholder="{{( pgettext "VAT number" "input" )}}">
|
||||||
|
<label for="vatin">{{( pgettext "VAT number" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="trade_name" id="trade_name" value="{{ .TradeName }}" placeholder="{{( pgettext "Trade name" "input" )}}">
|
||||||
|
<label for="trade_name">{{( pgettext "Trade name" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="tel" name="phone" id="phone" required="required" value="{{ .Phone }}" placeholder="{{( pgettext "Phone" "input" )}}">
|
||||||
|
<label for="phone">{{( pgettext "Phone" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="email" name="email" id="email" required="required" value="{{ .Email }}" placeholder="{{( pgettext "Email" "input" )}}">
|
||||||
|
<label for="email">{{( pgettext "Email" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="url" name="web" id="web" value="{{ .Web }}" placeholder="{{( pgettext "Web" "input" )}}">
|
||||||
|
<label for="web">{{( pgettext "Web" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="address" id="address" class="width-2x" required="required" value="{{ .Address }}" placeholder="{{( pgettext "Address" "input" )}}">
|
||||||
|
<label for="address">{{( pgettext "Address" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="city" id="city" required="required" value="{{ .City }}" placeholder="{{( pgettext "City" "input" )}}">
|
||||||
|
<label for="city">{{( pgettext "City" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="province" id="province" required="required" value="{{ .City }}" placeholder="{{( pgettext "Province" "input" )}}">
|
||||||
|
<label for="province">{{( pgettext "Province" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="postal_code" id="postal_code" required="required" value="{{ .PostalCode }}" placeholder="{{( pgettext "Postal code" "input" )}}">
|
||||||
|
<label for="postal_code">{{( pgettext "Postal code" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="text" name="country" id="country" required="required" value="{{ .Country }}" placeholder="{{( pgettext "Country" "input" )}}">
|
||||||
|
<label for="country">{{( pgettext "Country" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{{- end }}
|
Loading…
Reference in New Issue