Compare commits
No commits in common. "666935b54c8f2d735d8fe5591407ca231bba7e4c" and "7513030334e343af63be7a99e86045c6967e9b8d" have entirely different histories.
666935b54c
...
7513030334
|
@ -15,9 +15,4 @@ values (1, 1)
|
||||||
, (1, 2)
|
, (1, 2)
|
||||||
;
|
;
|
||||||
|
|
||||||
insert into tax (company_id, name, rate)
|
|
||||||
values (1, 'Retenció 15 %', -0.15)
|
|
||||||
, (1, 'IVA 21 %', 0.21)
|
|
||||||
;
|
|
||||||
|
|
||||||
commit;
|
commit;
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
-- Deploy numerus:tax to pg
|
|
||||||
-- requires: schema_numerus
|
|
||||||
-- requires: company
|
|
||||||
-- requires: tax_rate
|
|
||||||
|
|
||||||
begin;
|
|
||||||
|
|
||||||
set search_path to numerus, public;
|
|
||||||
|
|
||||||
create table tax (
|
|
||||||
tax_id serial primary key,
|
|
||||||
company_id integer not null references company,
|
|
||||||
name text not null,
|
|
||||||
rate tax_rate not null
|
|
||||||
);
|
|
||||||
|
|
||||||
grant select, insert, update, delete on table tax to invoicer;
|
|
||||||
grant select, insert, update, delete on table tax to admin;
|
|
||||||
|
|
||||||
grant usage on sequence tax_tax_id_seq to invoicer;
|
|
||||||
grant usage on sequence tax_tax_id_seq to admin;
|
|
||||||
|
|
||||||
alter table tax enable row level security;
|
|
||||||
|
|
||||||
create policy company_policy
|
|
||||||
on tax
|
|
||||||
using (
|
|
||||||
exists(
|
|
||||||
select 1
|
|
||||||
from company_user
|
|
||||||
join user_profile using (user_id)
|
|
||||||
where company_user.company_id = tax.company_id
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
commit;
|
|
|
@ -1,14 +0,0 @@
|
||||||
-- Deploy numerus:tax_rate to pg
|
|
||||||
-- requires: schema_numerus
|
|
||||||
|
|
||||||
begin;
|
|
||||||
|
|
||||||
set search_path to numerus, public;
|
|
||||||
|
|
||||||
create domain tax_rate as numeric
|
|
||||||
check (value > -1 and value < 1);
|
|
||||||
|
|
||||||
comment on domain country_code is
|
|
||||||
'A tax rate in the range (-1, 1)';
|
|
||||||
|
|
||||||
commit;
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -66,22 +65,11 @@ func getCompany(r *http.Request) *Company {
|
||||||
return company.(*Company)
|
return company.(*Company)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CurrencyOption struct {
|
|
||||||
Code string
|
|
||||||
Symbol string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CountryOption struct {
|
type CountryOption struct {
|
||||||
Code string
|
Code string
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tax struct {
|
|
||||||
Id int
|
|
||||||
Name string
|
|
||||||
Rate int
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaxDetailsPage struct {
|
type TaxDetailsPage struct {
|
||||||
Title string
|
Title string
|
||||||
BusinessName string
|
BusinessName string
|
||||||
|
@ -96,9 +84,6 @@ type TaxDetailsPage struct {
|
||||||
PostalCode string
|
PostalCode string
|
||||||
CountryCode string
|
CountryCode string
|
||||||
Countries []CountryOption
|
Countries []CountryOption
|
||||||
CurrencyCode string
|
|
||||||
Currencies []CurrencyOption
|
|
||||||
Taxes []Tax
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompanyTaxDetailsHandler() http.Handler {
|
func CompanyTaxDetailsHandler() http.Handler {
|
||||||
|
@ -122,18 +107,15 @@ func CompanyTaxDetailsHandler() http.Handler {
|
||||||
page.City = r.FormValue("city")
|
page.City = r.FormValue("city")
|
||||||
page.Province = r.FormValue("province")
|
page.Province = r.FormValue("province")
|
||||||
page.PostalCode = r.FormValue("postal_code")
|
page.PostalCode = r.FormValue("postal_code")
|
||||||
page.CurrencyCode = r.FormValue("currency")
|
conn.MustExec(r.Context(), "update company set business_name = $1, vatin = $2, trade_name = $3, phone = parse_packed_phone_number($4, $11), email = $5, web = $6, address = $7, city = $8, province = $9, postal_code = $10, country_code = $11 where company_id = $12", page.BusinessName, page.VATIN, page.TradeName, page.Phone, page.Email, page.Web, page.Address, page.City, page.Province, page.PostalCode, page.CountryCode, company.Id)
|
||||||
conn.MustExec(r.Context(), "update company set business_name = $1, vatin = $2, trade_name = $3, phone = parse_packed_phone_number($4, $11), email = $5, web = $6, address = $7, city = $8, province = $9, postal_code = $10, country_code = $11, currency_code = $12 where company_id = $13", page.BusinessName, page.VATIN, page.TradeName, page.Phone, page.Email, page.Web, page.Address, page.City, page.Province, page.PostalCode, page.CountryCode, page.CurrencyCode, company.Id)
|
|
||||||
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
||||||
} else {
|
} else {
|
||||||
err := conn.QueryRow(r.Context(), "select business_name, substr(vatin::text, 3), trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code 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.CountryCode, &page.CurrencyCode)
|
err := conn.QueryRow(r.Context(), "select business_name, substr(vatin::text, 3), trade_name, phone, email, web, address, city, province, postal_code, country_code 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.CountryCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
page.Countries = mustGetCountryOptions(r.Context(), conn, locale)
|
page.Countries = mustGetCountryOptions(r.Context(), conn, locale)
|
||||||
page.Currencies = mustGetCurrencyOptions(r.Context(), conn)
|
|
||||||
page.Taxes = mustGetTaxes(r.Context(), conn, company)
|
|
||||||
mustRenderAppTemplate(w, r, "tax-details.html", page)
|
mustRenderAppTemplate(w, r, "tax-details.html", page)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -147,7 +129,7 @@ func mustGetCompany(r *http.Request) *Company {
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []CountryOption {
|
func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []CountryOption {
|
||||||
rows, err := conn.Query(ctx, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", locale.Language)
|
rows, err := conn.Query(ctx, "select country.country_code, coalesce(i18n.name, country.name) from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1", locale.Language)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -168,69 +150,3 @@ func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []Co
|
||||||
|
|
||||||
return countries
|
return countries
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetCurrencyOptions(ctx context.Context, conn *Conn) []CurrencyOption {
|
|
||||||
rows, err := conn.Query(ctx, "select currency_code, currency_symbol from currency order by currency_code")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var currencies []CurrencyOption
|
|
||||||
for rows.Next() {
|
|
||||||
var currency CurrencyOption
|
|
||||||
err = rows.Scan(¤cy.Code, ¤cy.Symbol)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
currencies = append(currencies, currency)
|
|
||||||
}
|
|
||||||
if rows.Err() != nil {
|
|
||||||
panic(rows.Err())
|
|
||||||
}
|
|
||||||
|
|
||||||
return currencies
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []Tax {
|
|
||||||
rows, err := conn.Query(ctx, "select tax_id, name, (rate * 100)::integer from tax where company_id = $1 order by rate, name", company.Id)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var taxes []Tax
|
|
||||||
for rows.Next() {
|
|
||||||
var tax Tax
|
|
||||||
err = rows.Scan(&tax.Id, &tax.Name, &tax.Rate)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
taxes = append(taxes, tax)
|
|
||||||
}
|
|
||||||
if rows.Err() != nil {
|
|
||||||
panic(rows.Err())
|
|
||||||
}
|
|
||||||
|
|
||||||
return taxes
|
|
||||||
}
|
|
||||||
|
|
||||||
func CompanyTaxHandler() http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
param := r.URL.Path
|
|
||||||
if idx := strings.LastIndexByte(param, '/'); idx >= 0 {
|
|
||||||
param = param[idx+1:]
|
|
||||||
}
|
|
||||||
conn := getConn(r)
|
|
||||||
company := mustGetCompany(r)
|
|
||||||
if taxId, err := strconv.Atoi(param); err == nil {
|
|
||||||
conn.MustExec(r.Context(), "delete from tax where tax_id = $1", taxId)
|
|
||||||
} else {
|
|
||||||
r.ParseForm()
|
|
||||||
name := r.FormValue("name")
|
|
||||||
rate, _ := strconv.Atoi(r.FormValue("rate"))
|
|
||||||
conn.MustExec(r.Context(), "insert into tax (company_id, name, rate) values ($1, $2, $3 / 100::decimal)", company.Id, name, rate)
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,8 +7,6 @@ import (
|
||||||
func NewRouter(db *Db) http.Handler {
|
func NewRouter(db *Db) http.Handler {
|
||||||
companyRouter := http.NewServeMux()
|
companyRouter := http.NewServeMux()
|
||||||
companyRouter.Handle("/tax-details", CompanyTaxDetailsHandler())
|
companyRouter.Handle("/tax-details", CompanyTaxDetailsHandler())
|
||||||
companyRouter.Handle("/tax/", CompanyTaxHandler())
|
|
||||||
companyRouter.Handle("/tax", CompanyTaxHandler())
|
|
||||||
companyRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
companyRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
mustRenderAppTemplate(w, r, "dashboard.html", nil)
|
mustRenderAppTemplate(w, r, "dashboard.html", nil)
|
||||||
})
|
})
|
||||||
|
|
47
po/ca.po
47
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-28 14:14+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"
|
||||||
|
@ -77,12 +77,12 @@ msgctxt "language option"
|
||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr "Automàtic"
|
msgstr "Automàtic"
|
||||||
|
|
||||||
#: web/template/profile.html:42 web/template/tax-details.html:127
|
#: 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:108
|
#: web/template/tax-details.html:3 pkg/company.go:87
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Tax Details"
|
msgid "Tax Details"
|
||||||
msgstr "Configuració fiscal"
|
msgstr "Configuració fiscal"
|
||||||
|
@ -132,50 +132,11 @@ msgctxt "input"
|
||||||
msgid "Postal code"
|
msgid "Postal code"
|
||||||
msgstr "Codi postal"
|
msgstr "Codi postal"
|
||||||
|
|
||||||
#: web/template/tax-details.html:52
|
#: web/template/tax-details.html:47
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Country"
|
msgid "Country"
|
||||||
msgstr "País"
|
msgstr "País"
|
||||||
|
|
||||||
#: web/template/tax-details.html:56
|
|
||||||
msgctxt "input"
|
|
||||||
msgid "Currency"
|
|
||||||
msgstr "Moneda"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:74
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Tax Name"
|
|
||||||
msgstr "Nom import"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:75
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Rate (%)"
|
|
||||||
msgstr "Percentatge"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:96
|
|
||||||
msgid "No taxes added yet."
|
|
||||||
msgstr "No hi ha cap impost."
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:102
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "New Line"
|
|
||||||
msgstr "Nova línia"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:106
|
|
||||||
msgctxt "input"
|
|
||||||
msgid "Tax name"
|
|
||||||
msgstr "Nom impost"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:112
|
|
||||||
msgctxt "input"
|
|
||||||
msgid "Rate (%)"
|
|
||||||
msgstr "Percentatge"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:119
|
|
||||||
msgctxt "action"
|
|
||||||
msgid "Add new tax"
|
|
||||||
msgstr "Afegeix nou impost"
|
|
||||||
|
|
||||||
#: web/template/app.html:20
|
#: web/template/app.html:20
|
||||||
msgctxt "menu"
|
msgctxt "menu"
|
||||||
msgid "Account"
|
msgid "Account"
|
||||||
|
|
47
po/es.po
47
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-28 14:14+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"
|
||||||
|
@ -77,12 +77,12 @@ msgctxt "language option"
|
||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr "Automático"
|
msgstr "Automático"
|
||||||
|
|
||||||
#: web/template/profile.html:42 web/template/tax-details.html:127
|
#: 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:108
|
#: web/template/tax-details.html:3 pkg/company.go:87
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Tax Details"
|
msgid "Tax Details"
|
||||||
msgstr "Configuración fiscal"
|
msgstr "Configuración fiscal"
|
||||||
|
@ -132,50 +132,11 @@ msgctxt "input"
|
||||||
msgid "Postal code"
|
msgid "Postal code"
|
||||||
msgstr "Código postal"
|
msgstr "Código postal"
|
||||||
|
|
||||||
#: web/template/tax-details.html:52
|
#: web/template/tax-details.html:47
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Country"
|
msgid "Country"
|
||||||
msgstr "País"
|
msgstr "País"
|
||||||
|
|
||||||
#: web/template/tax-details.html:56
|
|
||||||
msgctxt "input"
|
|
||||||
msgid "Currency"
|
|
||||||
msgstr "Moneda"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:74
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Tax Name"
|
|
||||||
msgstr "Nombre impuesto"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:75
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Rate (%)"
|
|
||||||
msgstr "Porcentage"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:96
|
|
||||||
msgid "No taxes added yet."
|
|
||||||
msgstr "No hay impuestos."
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:102
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "New Line"
|
|
||||||
msgstr "Nueva línea"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:106
|
|
||||||
msgctxt "input"
|
|
||||||
msgid "Tax name"
|
|
||||||
msgstr "Nombre impuesto"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:112
|
|
||||||
msgctxt "input"
|
|
||||||
msgid "Rate (%)"
|
|
||||||
msgstr "Porcentage"
|
|
||||||
|
|
||||||
#: web/template/tax-details.html:119
|
|
||||||
msgctxt "action"
|
|
||||||
msgid "Add new tax"
|
|
||||||
msgstr "Añadir nuevo impuesto"
|
|
||||||
|
|
||||||
#: web/template/app.html:20
|
#: web/template/app.html:20
|
||||||
msgctxt "menu"
|
msgctxt "menu"
|
||||||
msgid "Account"
|
msgid "Account"
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
-- Revert numerus:tax from pg
|
|
||||||
|
|
||||||
begin;
|
|
||||||
|
|
||||||
drop policy company_policy on numerus.tax;
|
|
||||||
drop table if exists numerus.tax;
|
|
||||||
|
|
||||||
commit;
|
|
|
@ -1,7 +0,0 @@
|
||||||
-- Revert numerus:tax_rate from pg
|
|
||||||
|
|
||||||
begin;
|
|
||||||
|
|
||||||
drop domain if exists numerus.tax_rate;
|
|
||||||
|
|
||||||
commit;
|
|
|
@ -36,5 +36,3 @@ country_i18n [schema_numerus country_code language country] 2023-01-27T19:20:43Z
|
||||||
available_countries [schema_numerus country] 2023-01-27T18:49:28Z jordi fita mas <jordi@tandem.blog> # Add the list of available countries
|
available_countries [schema_numerus country] 2023-01-27T18:49:28Z jordi fita mas <jordi@tandem.blog> # Add the list of available countries
|
||||||
company [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-24T15:03:15Z jordi fita mas <jordi@tandem.blog> # Add the relation for companies
|
company [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 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
|
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
|
||||||
tax_rate [schema_numerus] 2023-01-28T11:33:39Z jordi fita mas <jordi@tandem.blog> # Add domain for tax rates
|
|
||||||
tax [schema_numerus company tax_rate] 2023-01-28T11:45:47Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes
|
|
||||||
|
|
127
test/tax.sql
127
test/tax.sql
|
@ -1,127 +0,0 @@
|
||||||
-- Test tax
|
|
||||||
set client_min_messages to warning;
|
|
||||||
create extension if not exists pgtap;
|
|
||||||
reset client_min_messages;
|
|
||||||
|
|
||||||
begin;
|
|
||||||
|
|
||||||
select plan(35);
|
|
||||||
|
|
||||||
set search_path to numerus, auth, public;
|
|
||||||
|
|
||||||
select has_table('tax');
|
|
||||||
select has_pk('tax' );
|
|
||||||
select table_privs_are('tax', 'guest', array []::text[]);
|
|
||||||
select table_privs_are('tax', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
|
|
||||||
select table_privs_are('tax', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
|
|
||||||
select table_privs_are('tax', 'authenticator', array []::text[]);
|
|
||||||
|
|
||||||
SELECT has_sequence('tax_tax_id_seq');
|
|
||||||
SELECT sequence_privs_are('tax_tax_id_seq', 'guest', array[]::text[]);
|
|
||||||
SELECT sequence_privs_are('tax_tax_id_seq', 'invoicer', array['USAGE']);
|
|
||||||
SELECT sequence_privs_are('tax_tax_id_seq', 'admin', array['USAGE']);
|
|
||||||
SELECT sequence_privs_are('tax_tax_id_seq', 'authenticator', array[]::text[]);
|
|
||||||
|
|
||||||
select has_column('tax', 'tax_id');
|
|
||||||
select col_is_pk('tax', 'tax_id');
|
|
||||||
select col_type_is('tax', 'tax_id', 'integer');
|
|
||||||
select col_not_null('tax', 'tax_id');
|
|
||||||
select col_has_default('tax', 'tax_id');
|
|
||||||
select col_default_is('tax', 'tax_id', 'nextval(''tax_tax_id_seq''::regclass)');
|
|
||||||
|
|
||||||
select has_column('tax', 'company_id');
|
|
||||||
select col_is_fk('tax', 'company_id');
|
|
||||||
select fk_ok('tax', 'company_id', 'company', 'company_id');
|
|
||||||
select col_type_is('tax', 'company_id', 'integer');
|
|
||||||
select col_not_null('tax', 'company_id');
|
|
||||||
select col_hasnt_default('tax', 'company_id');
|
|
||||||
|
|
||||||
select has_column('tax', 'name');
|
|
||||||
select col_type_is('tax', 'name', 'text');
|
|
||||||
select col_not_null('tax', 'name');
|
|
||||||
select col_hasnt_default('tax', 'name');
|
|
||||||
|
|
||||||
select has_column('tax', 'rate');
|
|
||||||
select col_type_is('tax', 'rate', 'tax_rate');
|
|
||||||
select col_not_null('tax', 'rate');
|
|
||||||
select col_hasnt_default('tax', 'rate');
|
|
||||||
|
|
||||||
|
|
||||||
set client_min_messages to warning;
|
|
||||||
truncate tax cascade;
|
|
||||||
truncate company_user cascade;
|
|
||||||
truncate company cascade;
|
|
||||||
truncate auth."user" cascade;
|
|
||||||
reset client_min_messages;
|
|
||||||
|
|
||||||
insert into auth."user" (user_id, email, name, password, 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)
|
|
||||||
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR')
|
|
||||||
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD')
|
|
||||||
;
|
|
||||||
|
|
||||||
insert into company_user (company_id, user_id)
|
|
||||||
values (2, 1)
|
|
||||||
, (4, 5)
|
|
||||||
;
|
|
||||||
|
|
||||||
insert into tax (company_id, name, rate)
|
|
||||||
values (2, 'VAT 21 %', 0.21)
|
|
||||||
, (2, 'IRPF -15 %', -0.15)
|
|
||||||
, (4, 'VAT 21 %', 0.21)
|
|
||||||
, (4, 'VAT 10 %', 0.10)
|
|
||||||
, (4, 'VAT 5 %', 0.05)
|
|
||||||
, (4, 'VAT 4 %', 0.04)
|
|
||||||
, (4, 'VAT 0 %', 0.00)
|
|
||||||
;
|
|
||||||
|
|
||||||
prepare tax_data as
|
|
||||||
select company_id, name, rate
|
|
||||||
from tax
|
|
||||||
order by company_id, rate;
|
|
||||||
|
|
||||||
set role invoicer;
|
|
||||||
select is_empty('tax_data', 'Should show no data when cookie is not set yet');
|
|
||||||
reset role;
|
|
||||||
|
|
||||||
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
|
|
||||||
select bag_eq(
|
|
||||||
'tax_data',
|
|
||||||
$$ values ( 2, 'IRPF -15 %', -0.15::tax_rate )
|
|
||||||
, ( 2, 'VAT 21 %', 0.21::tax_rate )
|
|
||||||
$$,
|
|
||||||
'Should only list taxes of the companies where demo@tandem.blog is user of'
|
|
||||||
);
|
|
||||||
reset role;
|
|
||||||
|
|
||||||
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
|
|
||||||
select bag_eq(
|
|
||||||
'tax_data',
|
|
||||||
$$ values (4, 'VAT 0 %', 0.00::tax_rate)
|
|
||||||
, (4, 'VAT 4 %', 0.04::tax_rate)
|
|
||||||
, (4, 'VAT 5 %', 0.05::tax_rate)
|
|
||||||
, (4, 'VAT 10 %', 0.10::tax_rate)
|
|
||||||
, (4, 'VAT 21 %', 0.21::tax_rate)
|
|
||||||
$$,
|
|
||||||
'Should only list taxes of the companies where admin@tandem.blog is user of'
|
|
||||||
);
|
|
||||||
reset role;
|
|
||||||
|
|
||||||
select set_cookie('not-a-cookie');
|
|
||||||
select throws_ok(
|
|
||||||
'tax_data',
|
|
||||||
'42501', 'permission denied for table tax',
|
|
||||||
'Should not allow select to guest users'
|
|
||||||
);
|
|
||||||
reset role;
|
|
||||||
|
|
||||||
|
|
||||||
select *
|
|
||||||
from finish();
|
|
||||||
|
|
||||||
rollback;
|
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
-- Test tax_rate
|
|
||||||
set client_min_messages to warning;
|
|
||||||
create extension if not exists pgtap;
|
|
||||||
reset client_min_messages;
|
|
||||||
|
|
||||||
begin;
|
|
||||||
|
|
||||||
select plan(7);
|
|
||||||
|
|
||||||
set search_path to numerus, public;
|
|
||||||
|
|
||||||
select has_domain('tax_rate');
|
|
||||||
select domain_type_is('tax_rate', 'numeric');
|
|
||||||
|
|
||||||
select lives_ok($$ select 0.21::tax_rate $$, 'Should be able to cast valid positive decimals to tax rate');
|
|
||||||
select lives_ok($$ select -0.15::tax_rate $$, 'Should be able to cast valid negative decimals to tax rate');
|
|
||||||
select lives_ok($$ select 0::tax_rate $$, 'Should be able to cast valid zero to tax rate');
|
|
||||||
|
|
||||||
select throws_ok(
|
|
||||||
$$ SELECT 1::tax_rate $$,
|
|
||||||
23514, null,
|
|
||||||
'Should reject 100 % tax rate'
|
|
||||||
);
|
|
||||||
|
|
||||||
select throws_ok(
|
|
||||||
$$ SELECT -1::tax_rate $$,
|
|
||||||
23514, null,
|
|
||||||
'Should reject -100 % tax rate'
|
|
||||||
);
|
|
||||||
|
|
||||||
select *
|
|
||||||
from finish();
|
|
||||||
|
|
||||||
rollback;
|
|
|
@ -7,7 +7,4 @@ select company_id
|
||||||
from numerus.company_user
|
from numerus.company_user
|
||||||
where false;
|
where false;
|
||||||
|
|
||||||
select 1 / count(*) from pg_class where oid = 'numerus.company'::regclass and relrowsecurity;
|
|
||||||
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.company'::regclass;
|
|
||||||
|
|
||||||
rollback;
|
rollback;
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
-- Verify numerus:tax on pg
|
|
||||||
|
|
||||||
begin;
|
|
||||||
|
|
||||||
select tax_id
|
|
||||||
, company_id
|
|
||||||
, name
|
|
||||||
, rate
|
|
||||||
from numerus.tax
|
|
||||||
where false;
|
|
||||||
|
|
||||||
select 1 / count(*) from pg_class where oid = 'numerus.tax'::regclass and relrowsecurity;
|
|
||||||
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.tax'::regclass;
|
|
||||||
|
|
||||||
rollback;
|
|
|
@ -1,7 +0,0 @@
|
||||||
-- Verify numerus:tax_rate on pg
|
|
||||||
|
|
||||||
begin;
|
|
||||||
|
|
||||||
select pg_catalog.has_type_privilege('numerus.tax_rate', 'usage');
|
|
||||||
|
|
||||||
rollback;
|
|
|
@ -205,15 +205,6 @@ input[type="submit"]:active, button:active {
|
||||||
text-color: var(--numerus--color--white);
|
text-color: var(--numerus--color--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.icon {
|
|
||||||
min-width: 0;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.web {
|
.web {
|
||||||
margin: 8.5rem 4rem;
|
margin: 8.5rem 4rem;
|
||||||
background-color: var(--numerus--header--background-color);
|
background-color: var(--numerus--header--background-color);
|
||||||
|
@ -257,13 +248,7 @@ main {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"]
|
input[type="text"], input[type="password"], input[type="email"], input[type="tel"], input[type="url"], select {
|
||||||
, input[type="password"]
|
|
||||||
, input[type="email"]
|
|
||||||
, input[type="tel"]
|
|
||||||
, input[type="url"]
|
|
||||||
, input[type="number"]
|
|
||||||
, 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;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<section class="dialog-content">
|
<section class="dialog-content">
|
||||||
<h2>{{(pgettext "Tax Details" "title")}}</h2>
|
<h2>{{(pgettext "Tax Details" "title")}}</h2>
|
||||||
<form id="details" method="POST">
|
<form method="POST">
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<input type="text" name="business_name" id="business_name" required="required" value="{{ .BusinessName }}" placeholder="{{( pgettext "Business name" "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>
|
<label for="business_name">{{( pgettext "Business name" "input" )}}</label>
|
||||||
|
@ -49,82 +49,10 @@
|
||||||
<option value="{{ .Code }}" {{ if eq .Code $.CountryCode }}selected="selected"{{ end }}>{{ .Name }}</option>
|
<option value="{{ .Code }}" {{ if eq .Code $.CountryCode }}selected="selected"{{ end }}>{{ .Name }}</option>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</select>
|
</select>
|
||||||
<label for="country">{{( pgettext "Country" "input" )}}</label>
|
<label for="country">{{( pgettext "Country" "input" )}}<label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset>
|
<button type="submit">{{( pgettext "Save changes" "action" )}}</button>
|
||||||
<legend id="currency-legend">{{( pgettext "Currency" "input" )}}</legend>
|
|
||||||
|
|
||||||
<select id="currency" name="currency" aria-labelledby="currency-legend">
|
|
||||||
{{- range $currency := .Currencies }}
|
|
||||||
<option value="{{ .Code }}" {{ if eq .Code $.CurrencyCode }}selected="selected"{{ end }}>{{ .Symbol }} ({{ .Code }})</option>
|
|
||||||
{{- end }}
|
|
||||||
</select>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form id="newtax" method="POST" action="{{ companyURI "/tax" }}">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th width="50%"></th>
|
|
||||||
<th>{{( pgettext "Tax Name" "title" )}}</th>
|
|
||||||
<th>{{( pgettext "Rate (%)" "title" )}}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{ with .Taxes }}
|
|
||||||
{{- range $tax := . }}
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>{{ .Name }}</td>
|
|
||||||
<td>{{ .Rate }}</td>
|
|
||||||
<td>
|
|
||||||
<form method="POST" action="{{ companyURI "/tax"}}/{{ .Id }}">
|
|
||||||
<input type="hidden" name="_method" name="DELETE"/>
|
|
||||||
<button class="icon" aria-label="{{( gettext "Delete tax" )}}" type="submit"><i class="ri-delete-back-2-line"></i></button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{- end }}
|
|
||||||
{{ else }}
|
|
||||||
<tr>
|
|
||||||
<td colspan="4">{{( gettext "No taxes added yet." )}}</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{( pgettext "New Line" "title")}}</th>
|
|
||||||
<td>
|
|
||||||
<div class="input">
|
|
||||||
<input form="newtax" type="text" name="name" id="tax_name" required="required" placeholder="{{( pgettext "Tax name" "input" )}}">
|
|
||||||
<label for="tax_name">{{( pgettext "Tax name" "input" )}}</label>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td colspan="2">
|
|
||||||
<div class="input">
|
|
||||||
<input form="newtax" type="number" name="rate" id="tax_rate" min="-99" max="99" required="required" placeholder="{{( pgettext "Rate (%)" "input" )}}">
|
|
||||||
<label form="newtax" for="tax_rate">{{( pgettext "Rate (%)" "input" )}}</label>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"></td>
|
|
||||||
<td colspan="2">
|
|
||||||
<button form="newtax" type="submit">{{( pgettext "Add new tax" "action" )}}</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<button form="details" type="submit">{{( pgettext "Save changes" "action" )}}</button>
|
|
||||||
</fieldset>
|
|
||||||
</section>
|
</section>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
Loading…
Reference in New Issue