Compare commits
3 Commits
97ef02b0f9
...
985f843e8e
Author | SHA1 | Date |
---|---|---|
jordi fita mas | 985f843e8e | |
jordi fita mas | 980db529f1 | |
jordi fita mas | 8dbf8ef2d0 |
|
@ -4,10 +4,10 @@
|
||||||
|
|
||||||
begin;
|
begin;
|
||||||
|
|
||||||
insert into public.language (lang_tag, name, endonym, selectable)
|
insert into public.language (lang_tag, name, endonym, selectable, currency_pattern)
|
||||||
values ('und', 'Undefined', 'Undefined', false)
|
values ('und', 'Undefined', 'Undefined', false, '%[3]s%.[1]*[2]f')
|
||||||
, ('ca', 'Catalan', 'català', true)
|
, ('ca', 'Catalan', 'català', true, '%.[1]*[2]f %[3]s')
|
||||||
, ('es', 'Spanish', 'español', true)
|
, ('es', 'Spanish', 'español', true, '%.[1]*[2]f %[3]s')
|
||||||
;
|
;
|
||||||
|
|
||||||
commit;
|
commit;
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
-- Deploy numerus:compute_new_invoice_amount to pg
|
||||||
|
-- requires: schema_numerus
|
||||||
|
-- requires: company
|
||||||
|
-- requires: currency
|
||||||
|
-- requires: tax
|
||||||
|
-- requires: new_invoice_product
|
||||||
|
-- requires: new_invoice_amount
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
set search_path to numerus, public;
|
||||||
|
|
||||||
|
create or replace function compute_new_invoice_amount(company_id integer, products new_invoice_product[]) returns new_invoice_amount as
|
||||||
|
$$
|
||||||
|
declare
|
||||||
|
result new_invoice_amount;
|
||||||
|
begin
|
||||||
|
if array_length(products, 1) is null then
|
||||||
|
select to_price(0, decimal_digits), array[]::text[][], to_price(0, decimal_digits)
|
||||||
|
from company
|
||||||
|
join currency using (currency_code)
|
||||||
|
where company.company_id = compute_new_invoice_amount.company_id
|
||||||
|
into result.subtotal, result.taxes, result.total;
|
||||||
|
else
|
||||||
|
with product as (
|
||||||
|
select round(parse_price(price, currency.decimal_digits) * quantity * (1 - discount_rate))::integer as subtotal
|
||||||
|
, tax
|
||||||
|
, decimal_digits
|
||||||
|
from unnest(products)
|
||||||
|
join company on company.company_id = compute_new_invoice_amount.company_id
|
||||||
|
join currency using (currency_code)
|
||||||
|
)
|
||||||
|
, tax_amount as (
|
||||||
|
select tax_id
|
||||||
|
, sum(round(subtotal * tax.rate)::integer)::integer as amount
|
||||||
|
, decimal_digits
|
||||||
|
from product, unnest(product.tax) as product_tax(tax_id)
|
||||||
|
join tax using (tax_id)
|
||||||
|
group by tax_id, decimal_digits
|
||||||
|
)
|
||||||
|
, tax_total as (
|
||||||
|
select sum(amount)::integer as amount, array_agg(array[name, to_price(amount, decimal_digits)]) as taxes
|
||||||
|
from tax_amount
|
||||||
|
join tax using (tax_id)
|
||||||
|
)
|
||||||
|
select to_price(sum(subtotal)::integer, decimal_digits)
|
||||||
|
, coalesce(taxes, array[]::text[][])
|
||||||
|
, to_price(sum(subtotal)::integer + coalesce(tax_total.amount, 0), decimal_digits) as total
|
||||||
|
from product, tax_total
|
||||||
|
group by tax_total.amount, taxes, decimal_digits
|
||||||
|
into result.subtotal, result.taxes, result.total;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
end
|
||||||
|
$$
|
||||||
|
language plpgsql
|
||||||
|
stable;
|
||||||
|
|
||||||
|
revoke execute on function compute_new_invoice_amount(integer, new_invoice_product[]) from public;
|
||||||
|
grant execute on function compute_new_invoice_amount(integer, new_invoice_product[]) to invoicer;
|
||||||
|
grant execute on function compute_new_invoice_amount(integer, new_invoice_product[]) to admin;
|
||||||
|
|
||||||
|
commit;
|
|
@ -9,7 +9,8 @@ create table language (
|
||||||
lang_tag text primary key check (length(lang_tag) < 36), -- RFC5646 recommends 35 at least
|
lang_tag text primary key check (length(lang_tag) < 36), -- RFC5646 recommends 35 at least
|
||||||
name text not null,
|
name text not null,
|
||||||
endonym text not null,
|
endonym text not null,
|
||||||
selectable boolean not null
|
selectable boolean not null,
|
||||||
|
currency_pattern text not null
|
||||||
);
|
);
|
||||||
|
|
||||||
grant select on table language to guest;
|
grant select on table language to guest;
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- Deploy numerus:new_invoice_amount to pg
|
||||||
|
-- requires: schema_numerus
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
set search_path to numerus, public;
|
||||||
|
|
||||||
|
create type new_invoice_amount as (
|
||||||
|
subtotal text,
|
||||||
|
taxes text[][],
|
||||||
|
total text
|
||||||
|
);
|
||||||
|
|
||||||
|
commit;
|
|
@ -11,7 +11,13 @@ declare
|
||||||
parts text[];
|
parts text[];
|
||||||
result int;
|
result int;
|
||||||
frac_part text;
|
frac_part text;
|
||||||
|
sign int := 1;
|
||||||
begin
|
begin
|
||||||
|
if price like '-%' Then
|
||||||
|
sign := -1;
|
||||||
|
price := substring(price from 2);
|
||||||
|
end if;
|
||||||
|
|
||||||
parts := string_to_array(price, '.');
|
parts := string_to_array(price, '.');
|
||||||
if array_length(parts, 1) > 2 then
|
if array_length(parts, 1) > 2 then
|
||||||
raise invalid_parameter_value using message = price || ' is not a valid price representation.';
|
raise invalid_parameter_value using message = price || ' is not a valid price representation.';
|
||||||
|
@ -31,7 +37,7 @@ begin
|
||||||
result := result + frac_part::integer;
|
result := result + frac_part::integer;
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
return result;
|
return sign * result;
|
||||||
end;
|
end;
|
||||||
$$
|
$$
|
||||||
language plpgsql
|
language plpgsql
|
||||||
|
|
|
@ -10,9 +10,14 @@ $$
|
||||||
declare
|
declare
|
||||||
result text;
|
result text;
|
||||||
scale integer := 10^decimal_digits;
|
scale integer := 10^decimal_digits;
|
||||||
|
sign text := '';
|
||||||
begin
|
begin
|
||||||
|
if cents < 0 then
|
||||||
|
sign := '-';
|
||||||
|
cents = -cents;
|
||||||
|
end if;
|
||||||
result = cents::text;
|
result = cents::text;
|
||||||
return (cents / scale)::text || '.' || to_char(mod(cents, scale), rpad('FM', decimal_digits + 2, '0'));
|
return sign || (cents / scale)::text || '.' || to_char(mod(cents, scale), rpad('FM', decimal_digits + 2, '0'));
|
||||||
end;
|
end;
|
||||||
$$
|
$$
|
||||||
language plpgsql
|
language plpgsql
|
||||||
|
|
|
@ -66,10 +66,31 @@ func GetInvoiceForm(w http.ResponseWriter, r *http.Request, params httprouter.Pa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type newInvoicePage struct {
|
||||||
|
Form *invoiceForm
|
||||||
|
Subtotal string
|
||||||
|
Taxes [][]string
|
||||||
|
Total string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNewInvoicePage(form *invoiceForm, r *http.Request) *newInvoicePage {
|
||||||
|
page := &newInvoicePage{
|
||||||
|
Form: form,
|
||||||
|
}
|
||||||
|
conn := getConn(r)
|
||||||
|
company := mustGetCompany(r)
|
||||||
|
err := conn.QueryRow(r.Context(), "select subtotal, taxes, total from compute_new_invoice_amount($1, $2)", company.Id, NewInvoiceProductArray(form.Products)).Scan(&page.Subtotal, &page.Taxes, &page.Total)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
|
||||||
func mustRenderNewInvoiceForm(w http.ResponseWriter, r *http.Request, form *invoiceForm) {
|
func mustRenderNewInvoiceForm(w http.ResponseWriter, r *http.Request, form *invoiceForm) {
|
||||||
locale := getLocale(r)
|
locale := getLocale(r)
|
||||||
form.Customer.EmptyLabel = gettext("Select a customer to bill.", locale)
|
form.Customer.EmptyLabel = gettext("Select a customer to bill.", locale)
|
||||||
mustRenderAppTemplate(w, r, "invoices/new.gohtml", form)
|
page := newNewInvoicePage(form, r)
|
||||||
|
mustRenderAppTemplate(w, r, "invoices/new.gohtml", page)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustRenderNewInvoiceProductsForm(w http.ResponseWriter, r *http.Request, form *invoiceForm) {
|
func mustRenderNewInvoiceProductsForm(w http.ResponseWriter, r *http.Request, form *invoiceForm) {
|
||||||
|
@ -158,7 +179,7 @@ func HandleAddProductsToInvoice(w http.ResponseWriter, r *http.Request, _ httpro
|
||||||
|
|
||||||
index := len(form.Products)
|
index := len(form.Products)
|
||||||
productsId := r.Form["id"]
|
productsId := r.Form["id"]
|
||||||
rows := conn.MustQuery(r.Context(), "select product_id, name, description, to_price(price, decimal_digits), 1 as quantity, 0 as discount, array_agg(tax_id) from product join company using (company_id) join currency using (currency_code) left join product_tax using (product_id) where product_id = any ($1) group by product_id, name, description, price, decimal_digits", productsId)
|
rows := conn.MustQuery(r.Context(), "select product_id, name, description, to_price(price, decimal_digits), 1 as quantity, 0 as discount, array_remove(array_agg(tax_id), null) from product join company using (company_id) join currency using (currency_code) left join product_tax using (product_id) where product_id = any ($1) group by product_id, name, description, price, decimal_digits", productsId)
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
product := newInvoiceProductForm(index, company, locale, form.Tax.Options)
|
product := newInvoiceProductForm(index, company, locale, form.Tax.Options)
|
||||||
|
|
|
@ -12,27 +12,31 @@ const contextLocaleKey = "numerus-locale"
|
||||||
|
|
||||||
type Locale struct {
|
type Locale struct {
|
||||||
*gotext.Locale
|
*gotext.Locale
|
||||||
Language language.Tag
|
CurrencyPattern string
|
||||||
|
Language language.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLocale(lang language.Tag) *Locale {
|
func NewLocale(lang availableLanguage) *Locale {
|
||||||
return &Locale{
|
return &Locale{
|
||||||
gotext.NewLocale("locales", lang.String()),
|
gotext.NewLocale("locales", lang.tag.String()),
|
||||||
lang,
|
lang.currencyPattern,
|
||||||
|
lang.tag,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LocaleSetter(db *Db, next http.Handler) http.Handler {
|
func LocaleSetter(db *Db, next http.Handler) http.Handler {
|
||||||
availableLanguages := mustGetAvailableLanguages(db)
|
availableLanguages := mustGetAvailableLanguages(db)
|
||||||
var matcher = language.NewMatcher(availableLanguages)
|
|
||||||
|
|
||||||
locales := map[language.Tag]*Locale{}
|
locales := map[language.Tag]*Locale{}
|
||||||
|
var tags []language.Tag
|
||||||
for _, lang := range availableLanguages {
|
for _, lang := range availableLanguages {
|
||||||
locale := NewLocale(lang)
|
locale := NewLocale(lang)
|
||||||
locale.AddDomain("numerus")
|
locale.AddDomain("numerus")
|
||||||
locales[lang] = locale
|
locales[lang.tag] = locale
|
||||||
|
tags = append(tags, lang.tag)
|
||||||
}
|
}
|
||||||
defaultLocale := locales[language.Catalan]
|
defaultLocale := locales[language.Catalan]
|
||||||
|
var matcher = language.NewMatcher(tags)
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var locale *Locale
|
var locale *Locale
|
||||||
|
@ -70,21 +74,27 @@ func gettext(str string, locale *Locale) string {
|
||||||
return locale.Get(str)
|
return locale.Get(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetAvailableLanguages(db *Db) []language.Tag {
|
type availableLanguage struct {
|
||||||
rows, err := db.Query(context.Background(), "select lang_tag from language where selectable")
|
tag language.Tag
|
||||||
|
currencyPattern string
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetAvailableLanguages(db *Db) []availableLanguage {
|
||||||
|
rows, err := db.Query(context.Background(), "select lang_tag, currency_pattern from language where selectable")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var langs []language.Tag
|
var langs []availableLanguage
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var langTag string
|
var langTag string
|
||||||
err = rows.Scan(&langTag)
|
var currencyPattern string
|
||||||
|
err = rows.Scan(&langTag, ¤cyPattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
langs = append(langs, language.MustParse(langTag))
|
langs = append(langs, availableLanguage{language.MustParse(langTag), currencyPattern})
|
||||||
}
|
}
|
||||||
if rows.Err() != nil {
|
if rows.Err() != nil {
|
||||||
panic(rows.Err())
|
panic(rows.Err())
|
||||||
|
|
|
@ -38,7 +38,7 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f = math.NaN()
|
f = math.NaN()
|
||||||
}
|
}
|
||||||
return p.Sprintf("%.*f", company.DecimalDigits, number.Decimal(f))
|
return p.Sprintf(locale.CurrencyPattern, company.DecimalDigits, number.Decimal(f), company.CurrencySymbol)
|
||||||
},
|
},
|
||||||
"formatDate": func(time time.Time) string {
|
"formatDate": func(time time.Time) string {
|
||||||
return time.Format("02/01/2006")
|
return time.Format("02/01/2006")
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert numerus:compute_new_invoice_amount from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop function if exists numerus.compute_new_invoice_amount(integer, numerus.new_invoice_product[]);
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert numerus:new_invoice_amount from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop type if exists numerus.new_invoice_amount;
|
||||||
|
|
||||||
|
commit;
|
|
@ -58,3 +58,5 @@ next_invoice_number [schema_numerus invoice_number_counter] 2023-02-17T13:21:48Z
|
||||||
add_invoice [schema_numerus invoice company currency parse_price new_invoice_product tax invoice_product invoice_product_tax next_invoice_number] 2023-02-16T21:12:46Z jordi fita mas <jordi@tandem.blog> # Add function to create new invoices
|
add_invoice [schema_numerus invoice company currency parse_price new_invoice_product tax invoice_product invoice_product_tax next_invoice_number] 2023-02-16T21:12:46Z jordi fita mas <jordi@tandem.blog> # Add function to create new invoices
|
||||||
invoice_tax_amount [schema_numerus invoice_product invoice_product_tax] 2023-02-22T12:08:35Z jordi fita mas <jordi@tandem.blog> # Add view for invoice tax amount
|
invoice_tax_amount [schema_numerus invoice_product invoice_product_tax] 2023-02-22T12:08:35Z jordi fita mas <jordi@tandem.blog> # Add view for invoice tax amount
|
||||||
invoice_amount [schema_numerus invoice_product invoice_tax_amount] 2023-02-22T12:58:46Z jordi fita mas <jordi@tandem.blog> # Add view to compute subtotal and total for invoices
|
invoice_amount [schema_numerus invoice_product invoice_tax_amount] 2023-02-22T12:58:46Z jordi fita mas <jordi@tandem.blog> # Add view to compute subtotal and total for invoices
|
||||||
|
new_invoice_amount [schema_numerus] 2023-02-23T12:08:25Z jordi fita mas <jordi@tandem.blog> # Add type to return when computing new invoice amounts
|
||||||
|
compute_new_invoice_amount [schema_numerus company currency tax new_invoice_product new_invoice_amount] 2023-02-23T12:20:13Z jordi fita mas <jordi@tandem.blog> # Add function to compute the subtotal, taxes, and total amounts for a new invoice
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
-- Test compute_new_invoice_amount
|
||||||
|
set client_min_messages to warning;
|
||||||
|
create extension if not exists pgtap;
|
||||||
|
reset client_min_messages;
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select plan(14);
|
||||||
|
|
||||||
|
set search_path to numerus, auth, public;
|
||||||
|
|
||||||
|
select has_function('numerus', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]']);
|
||||||
|
select function_lang_is('numerus', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'plpgsql');
|
||||||
|
select function_returns('numerus', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'new_invoice_amount');
|
||||||
|
select isnt_definer('numerus', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]']);
|
||||||
|
select volatility_is('numerus', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'stable');
|
||||||
|
select function_privs_are('numerus', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'guest', array []::text[]);
|
||||||
|
select function_privs_are('numerus', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'invoicer', array ['EXECUTE']);
|
||||||
|
select function_privs_are('numerus', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'admin', array ['EXECUTE']);
|
||||||
|
select function_privs_are('numerus', 'compute_new_invoice_amount', array ['integer', 'new_invoice_product[]'], 'authenticator', array []::text[]);
|
||||||
|
|
||||||
|
set client_min_messages to warning;
|
||||||
|
truncate tax cascade;
|
||||||
|
truncate company cascade;
|
||||||
|
reset client_min_messages;
|
||||||
|
|
||||||
|
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code)
|
||||||
|
values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR')
|
||||||
|
;
|
||||||
|
|
||||||
|
insert into tax (tax_id, company_id, name, rate)
|
||||||
|
values (2, 1, 'IRPF -15 %', -0.15)
|
||||||
|
, (3, 1, 'IVA 4 %', 0.04)
|
||||||
|
, (4, 1, 'IVA 10 %', 0.10)
|
||||||
|
, (5, 1, 'IVA 21 %', 0.21)
|
||||||
|
;
|
||||||
|
|
||||||
|
select is(
|
||||||
|
compute_new_invoice_amount(1, '{}'),
|
||||||
|
'(0.00,"{}",0.00)'::new_invoice_amount
|
||||||
|
);
|
||||||
|
|
||||||
|
select is(
|
||||||
|
compute_new_invoice_amount(1, '{"(6,P,D,1.00,1,0.0,\"{2,5}\")","(6,P,D,2.00,2,0.1,{3})"}'),
|
||||||
|
'(4.60,"{{IRPF -15 %,-0.15},{IVA 4 %,0.14},{IVA 21 %,0.21}}",4.80)'::new_invoice_amount
|
||||||
|
);
|
||||||
|
|
||||||
|
select is(
|
||||||
|
compute_new_invoice_amount(1, '{"(6,P,D,2.22,3,0.0,\"{2,4,5}\")","(6,P,D,3.33,4,0.2,{4})"}'),
|
||||||
|
'(17.32,"{{IRPF -15 %,-1.00},{IVA 10 %,1.74},{IVA 21 %,1.40}}",19.46)'::new_invoice_amount
|
||||||
|
);
|
||||||
|
|
||||||
|
select is(
|
||||||
|
compute_new_invoice_amount(1, '{"(6,P,D,4.44,5,0.0,\"{4,5}\")","(6,P,D,5.55,6,0.1,\"{5,3}\")"}'),
|
||||||
|
'(52.17,"{{IVA 4 %,1.20},{IVA 10 %,2.22},{IVA 21 %,10.95}}",66.54)'::new_invoice_amount
|
||||||
|
);
|
||||||
|
|
||||||
|
select is(
|
||||||
|
compute_new_invoice_amount(1, '{"(6,P,D,7.77,8,0.0,\"{}\")"}'),
|
||||||
|
'(62.16,"{}",62.16)'::new_invoice_amount
|
||||||
|
);
|
||||||
|
|
||||||
|
select *
|
||||||
|
from finish();
|
||||||
|
|
||||||
|
rollback;
|
|
@ -7,7 +7,7 @@ begin;
|
||||||
|
|
||||||
set search_path to public;
|
set search_path to public;
|
||||||
|
|
||||||
select plan(23);
|
select plan(27);
|
||||||
|
|
||||||
select has_table('language');
|
select has_table('language');
|
||||||
select has_pk('language');
|
select has_pk('language');
|
||||||
|
@ -37,6 +37,11 @@ select col_type_is('language', 'selectable', 'boolean');
|
||||||
select col_not_null('language', 'selectable');
|
select col_not_null('language', 'selectable');
|
||||||
select col_hasnt_default('language', 'selectable');
|
select col_hasnt_default('language', 'selectable');
|
||||||
|
|
||||||
|
select has_column('language', 'currency_pattern');
|
||||||
|
select col_type_is('language', 'currency_pattern', 'text');
|
||||||
|
select col_not_null('language', 'currency_pattern');
|
||||||
|
select col_hasnt_default('language', 'currency_pattern');
|
||||||
|
|
||||||
select *
|
select *
|
||||||
from finish();
|
from finish();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
-- Test new_invoice_amount
|
||||||
|
set client_min_messages to warning;
|
||||||
|
create extension if not exists pgtap;
|
||||||
|
reset client_min_messages;
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select plan(5);
|
||||||
|
|
||||||
|
set search_path to numerus, public;
|
||||||
|
|
||||||
|
select has_composite('numerus', 'new_invoice_amount', 'Composite type numerus.new_invoice_amount should exist');
|
||||||
|
select columns_are('numerus', 'new_invoice_amount', array['subtotal', 'taxes', 'total']);
|
||||||
|
select col_type_is('numerus'::name, 'new_invoice_amount'::name, 'subtotal'::name, 'text');
|
||||||
|
select col_type_is('numerus'::name, 'new_invoice_amount'::name, 'taxes'::name, 'text[]');
|
||||||
|
select col_type_is('numerus'::name, 'new_invoice_amount'::name, 'total'::name, 'text');
|
||||||
|
|
||||||
|
|
||||||
|
select *
|
||||||
|
from finish();
|
||||||
|
|
||||||
|
rollback;
|
|
@ -5,7 +5,7 @@ reset client_min_messages;
|
||||||
|
|
||||||
begin;
|
begin;
|
||||||
|
|
||||||
select plan(36);
|
select plan(42);
|
||||||
|
|
||||||
set search_path to auth, numerus, public;
|
set search_path to auth, numerus, public;
|
||||||
|
|
||||||
|
@ -23,8 +23,12 @@ select is( parse_price('1.1', 2), 110 );
|
||||||
select is( parse_price('1.1', 3), 1100 );
|
select is( parse_price('1.1', 3), 1100 );
|
||||||
select is( parse_price('0', 2), 0 );
|
select is( parse_price('0', 2), 0 );
|
||||||
select is( parse_price('0', 3), 0 );
|
select is( parse_price('0', 3), 0 );
|
||||||
|
select is( parse_price('-0', 2), 0 );
|
||||||
|
select is( parse_price('-0', 3), 0 );
|
||||||
select is( parse_price('0.01', 2), 1 );
|
select is( parse_price('0.01', 2), 1 );
|
||||||
select is( parse_price('0.001', 3), 1 );
|
select is( parse_price('0.001', 3), 1 );
|
||||||
|
select is( parse_price('-0.01', 2), -1 );
|
||||||
|
select is( parse_price('-0.001', 3), -1 );
|
||||||
select is( parse_price('0.1', 2), 10 );
|
select is( parse_price('0.1', 2), 10 );
|
||||||
select is( parse_price('0.01', 3), 10 );
|
select is( parse_price('0.01', 3), 10 );
|
||||||
select is( parse_price('1', 2), 100 );
|
select is( parse_price('1', 2), 100 );
|
||||||
|
@ -33,6 +37,8 @@ select is( parse_price('10', 2), 1000 );
|
||||||
select is( parse_price('1', 3), 1000 );
|
select is( parse_price('1', 3), 1000 );
|
||||||
select is( parse_price('23.23', 2), 2323 );
|
select is( parse_price('23.23', 2), 2323 );
|
||||||
select is( parse_price('23.23', 3), 23230 );
|
select is( parse_price('23.23', 3), 23230 );
|
||||||
|
select is( parse_price('-23.23', 2), -2323 );
|
||||||
|
select is( parse_price('-23.23', 3), -23230 );
|
||||||
select throws_ok( $$ select parse_price('234.234', 2) $$ );
|
select throws_ok( $$ select parse_price('234.234', 2) $$ );
|
||||||
select is( parse_price('234.234', 3), 234234 );
|
select is( parse_price('234.234', 3), 234234 );
|
||||||
select throws_ok( $$ select parse_price('2345.2345', 2) $$ );
|
select throws_ok( $$ select parse_price('2345.2345', 2) $$ );
|
||||||
|
|
|
@ -5,7 +5,7 @@ reset client_min_messages;
|
||||||
|
|
||||||
begin;
|
begin;
|
||||||
|
|
||||||
select plan(23);
|
select plan(29);
|
||||||
|
|
||||||
set search_path to numerus, public;
|
set search_path to numerus, public;
|
||||||
|
|
||||||
|
@ -23,6 +23,8 @@ select is( to_price(0, 2), '0.00' );
|
||||||
select is( to_price(0, 3), '0.000' );
|
select is( to_price(0, 3), '0.000' );
|
||||||
select is( to_price(1, 2), '0.01' );
|
select is( to_price(1, 2), '0.01' );
|
||||||
select is( to_price(1, 3), '0.001' );
|
select is( to_price(1, 3), '0.001' );
|
||||||
|
select is( to_price(-1, 2), '-0.01' );
|
||||||
|
select is( to_price(-1, 3), '-0.001' );
|
||||||
select is( to_price(10, 2), '0.10' );
|
select is( to_price(10, 2), '0.10' );
|
||||||
select is( to_price(10, 3), '0.010' );
|
select is( to_price(10, 3), '0.010' );
|
||||||
select is( to_price(100, 2), '1.00' );
|
select is( to_price(100, 2), '1.00' );
|
||||||
|
@ -33,6 +35,10 @@ select is( to_price(12345678, 2), '123456.78' );
|
||||||
select is( to_price(12345678, 3), '12345.678' );
|
select is( to_price(12345678, 3), '12345.678' );
|
||||||
select is( to_price(12345678, 4), '1234.5678' );
|
select is( to_price(12345678, 4), '1234.5678' );
|
||||||
select is( to_price(12345678, 5), '123.45678' );
|
select is( to_price(12345678, 5), '123.45678' );
|
||||||
|
select is( to_price(-12345678, 2), '-123456.78' );
|
||||||
|
select is( to_price(-12345678, 3), '-12345.678' );
|
||||||
|
select is( to_price(-12345678, 4), '-1234.5678' );
|
||||||
|
select is( to_price(-12345678, 5), '-123.45678' );
|
||||||
|
|
||||||
select *
|
select *
|
||||||
from finish();
|
from finish();
|
||||||
|
|
|
@ -9,20 +9,23 @@ from language
|
||||||
where lang_tag = 'und'
|
where lang_tag = 'und'
|
||||||
and name = 'Undefined'
|
and name = 'Undefined'
|
||||||
and endonym = 'Undefined'
|
and endonym = 'Undefined'
|
||||||
and not selectable;
|
and not selectable
|
||||||
|
and currency_pattern = '%[3]s%.[1]*[2]f';
|
||||||
|
|
||||||
select 1 / count(*)
|
select 1 / count(*)
|
||||||
from language
|
from language
|
||||||
where lang_tag = 'ca'
|
where lang_tag = 'ca'
|
||||||
and name = 'Catalan'
|
and name = 'Catalan'
|
||||||
and endonym = 'català'
|
and endonym = 'català'
|
||||||
and selectable;
|
and selectable
|
||||||
|
and currency_pattern = '%.[1]*[2]f %[3]s';
|
||||||
|
|
||||||
select 1 / count(*)
|
select 1 / count(*)
|
||||||
from language
|
from language
|
||||||
where lang_tag = 'es'
|
where lang_tag = 'es'
|
||||||
and name = 'Spanish'
|
and name = 'Spanish'
|
||||||
and endonym = 'español'
|
and endonym = 'español'
|
||||||
and selectable;
|
and selectable
|
||||||
|
and currency_pattern = '%.[1]*[2]f %[3]s';
|
||||||
|
|
||||||
rollback;
|
rollback;
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Verify numerus:compute_new_invoice_amount on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select has_function_privilege('numerus.compute_new_invoice_amount(integer, numerus.new_invoice_product[])', 'execute');
|
||||||
|
|
||||||
|
rollback;
|
|
@ -6,6 +6,7 @@ select lang_tag
|
||||||
, name
|
, name
|
||||||
, endonym
|
, endonym
|
||||||
, selectable
|
, selectable
|
||||||
|
, currency_pattern
|
||||||
from public.language
|
from public.language
|
||||||
where false;
|
where false;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Verify numerus:new_invoice_amount on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select pg_catalog.has_type_privilege('numerus.new_invoice_amount', 'usage');
|
||||||
|
|
||||||
|
rollback;
|
|
@ -16,23 +16,44 @@
|
||||||
<form method="POST" action="{{ companyURI "/invoices" }}">
|
<form method="POST" action="{{ companyURI "/invoices" }}">
|
||||||
{{ csrfToken }}
|
{{ csrfToken }}
|
||||||
|
|
||||||
{{ template "select-field" .Customer }}
|
{{ with .Form -}}
|
||||||
{{ template "input-field" .Number }}
|
{{ template "select-field" .Customer }}
|
||||||
{{ template "input-field" .Date }}
|
{{ template "input-field" .Number }}
|
||||||
{{ template "input-field" .Notes }}
|
{{ template "input-field" .Date }}
|
||||||
|
{{ template "input-field" .Notes }}
|
||||||
|
|
||||||
{{- range $product := .Products }}
|
{{- range $product := .Products }}
|
||||||
<fieldset class="new-invoice-product">
|
<fieldset class="new-invoice-product">
|
||||||
{{ template "hidden-field" .ProductId }}
|
{{ template "hidden-field" .ProductId }}
|
||||||
{{ template "input-field" .Name }}
|
{{ template "input-field" .Name }}
|
||||||
{{ template "input-field" .Price }}
|
{{ template "input-field" .Price }}
|
||||||
{{ template "input-field" .Quantity }}
|
{{ template "input-field" .Quantity }}
|
||||||
{{ template "input-field" .Discount }}
|
{{ template "input-field" .Discount }}
|
||||||
{{ template "input-field" .Description }}
|
{{ template "input-field" .Description }}
|
||||||
{{ template "select-field" .Tax }}
|
{{ template "select-field" .Tax }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{(pgettext "Subtotal" "title")}}</th>
|
||||||
|
<td class="numeric">{{ .Subtotal | formatPrice }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- range $tax := .Taxes }}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ index . 0 }}</th>
|
||||||
|
<td class="numeric">{{ index . 1 | formatPrice }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{(pgettext "Total" "title")}}</th>
|
||||||
|
<td class="numeric">{{ .Total | formatPrice }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<button formnovalidate name="action" value="products"
|
<button formnovalidate name="action" value="products"
|
||||||
type="submit">{{( pgettext "Add products" "action" )}}</button>
|
type="submit">{{( pgettext "Add products" "action" )}}</button>
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td><input type="checkbox" name="id" id="new-product-id-{{$key}}" value="{{.Id}}"></td>
|
<td><input type="checkbox" name="id" id="new-product-id-{{$key}}" value="{{.Id}}"></td>
|
||||||
<td><label for="new-product-id-{{$key}}">{{ .Name }}</label></td>
|
<td><label for="new-product-id-{{$key}}">{{ .Name }}</label></td>
|
||||||
<td>{{ .Price | formatPrice }}</td>
|
<td class="numeric">{{ .Price | formatPrice }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td><a href="{{ companyURI "/products/"}}{{ .Slug }}">{{ .Name }}</a></td>
|
<td><a href="{{ companyURI "/products/"}}{{ .Slug }}">{{ .Name }}</a></td>
|
||||||
<td>{{ .Price | formatPrice }}</td>
|
<td class="numeric">{{ .Price | formatPrice }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
|
Loading…
Reference in New Issue