Compare commits
8 Commits
35b12f7ea4
...
9931796744
Author | SHA1 | Date |
---|---|---|
jordi fita mas | 9931796744 | |
jordi fita mas | efbb4da07f | |
jordi fita mas | f54681de93 | |
jordi fita mas | 86bf8765fc | |
jordi fita mas | a066726c2e | |
jordi fita mas | aeca90256c | |
jordi fita mas | 0e20eab46a | |
jordi fita mas | 775cdef097 |
|
@ -0,0 +1,89 @@
|
|||
-- Deploy numerus:add_quote to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: quote
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: new_quote_product
|
||||
-- requires: tax
|
||||
-- requires: quote_product
|
||||
-- requires: quote_payment_method
|
||||
-- requires: quote_contact
|
||||
-- requires: quote_product_product
|
||||
-- requires: quote_product_tax
|
||||
-- requires: next_quote_number
|
||||
-- requires: tag_name
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_quote(company integer, quote_date date, contact_id integer, terms_and_conditions text, notes text, payment_method_id integer, tags tag_name[], products new_quote_product[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
qid integer;
|
||||
qslug uuid;
|
||||
product new_quote_product;
|
||||
ccode text;
|
||||
qpid integer;
|
||||
begin
|
||||
insert into quote (company_id, quote_number, quote_date, terms_and_conditions, notes, tags, currency_code)
|
||||
select company_id
|
||||
, next_quote_number(add_quote.company, quote_date)
|
||||
, quote_date
|
||||
, terms_and_conditions
|
||||
, notes
|
||||
, tags
|
||||
, currency_code
|
||||
from company
|
||||
where company.company_id = add_quote.company
|
||||
returning quote_id, slug, currency_code
|
||||
into qid, qslug, ccode;
|
||||
|
||||
if contact_id is not null then
|
||||
insert into quote_contact (quote_id, contact_id)
|
||||
values (qid, contact_id);
|
||||
end if;
|
||||
|
||||
if payment_method_id is not null then
|
||||
insert into quote_payment_method (quote_id, payment_method_id)
|
||||
values (qid, payment_method_id);
|
||||
end if;
|
||||
|
||||
foreach product in array products
|
||||
loop
|
||||
insert into quote_product (quote_id, name, description, price, quantity, discount_rate)
|
||||
select qid
|
||||
, product.name
|
||||
, coalesce(product.description, '')
|
||||
, parse_price(product.price, currency.decimal_digits)
|
||||
, product.quantity
|
||||
, product.discount_rate
|
||||
from currency
|
||||
where currency_code = ccode
|
||||
returning quote_product_id
|
||||
into qpid;
|
||||
|
||||
if product.product_id is not null then
|
||||
insert into quote_product_product (quote_product_id, product_id)
|
||||
values (qpid, product.product_id);
|
||||
end if;
|
||||
|
||||
insert into quote_product_tax (quote_product_id, tax_id, tax_rate)
|
||||
select qpid, tax_id, tax.rate
|
||||
from tax
|
||||
join unnest(product.tax) as ptax(tax_id) using (tax_id);
|
||||
end loop;
|
||||
|
||||
return qslug;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function add_quote(integer, date, integer, text, text, integer, tag_name[], new_quote_product[]) from public;
|
||||
grant execute on function add_quote(integer, date, integer, text, text, integer, tag_name[], new_quote_product[]) to invoicer;
|
||||
grant execute on function add_quote(integer, date, integer, text, text, integer, tag_name[], new_quote_product[]) to admin;
|
||||
|
||||
commit;
|
|
@ -27,6 +27,7 @@ create table company (
|
|||
country_code country_code not null references country,
|
||||
currency_code currency_code not null references currency,
|
||||
invoice_number_format text not null default '"FRA"YYYY0000',
|
||||
quote_number_format text not null default '"PRE"YYYY0000',
|
||||
legal_disclaimer text not null default '',
|
||||
created_at timestamptz not null default current_timestamp
|
||||
);
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
-- Deploy numerus:compute_new_quote_amount to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: company
|
||||
-- requires: tax
|
||||
-- requires: new_quote_product
|
||||
-- requires: new_quote_amount
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function compute_new_quote_amount(company_id integer, products new_quote_product[]) returns new_quote_amount as
|
||||
$$
|
||||
declare
|
||||
result new_quote_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_quote_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_quote_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_quote_amount(integer, new_quote_product[]) from public;
|
||||
grant execute on function compute_new_quote_amount(integer, new_quote_product[]) to invoicer;
|
||||
grant execute on function compute_new_quote_amount(integer, new_quote_product[]) to admin;
|
||||
|
||||
commit;
|
|
@ -6,6 +6,7 @@
|
|||
-- requires: edited_invoice_product
|
||||
-- requires: tax
|
||||
-- requires: invoice_product
|
||||
-- requires: invoice_product_product
|
||||
-- requires: invoice_product_tax
|
||||
-- requires: tag_name
|
||||
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
-- Deploy numerus:edit_quote to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: quote
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: edited_quote_product
|
||||
-- requires: tax
|
||||
-- requires: quote_contact
|
||||
-- requires: quote_payment_method
|
||||
-- requires: quote_product
|
||||
-- requires: quote_product_tax
|
||||
-- requires: quote_product_product
|
||||
-- requires: tag_name
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_quote(quote_slug uuid, quote_status text, contact_id integer, terms_and_conditions text, notes text, payment_method_id integer, tags tag_name[], products edited_quote_product[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
qid integer;
|
||||
products_to_keep integer[];
|
||||
products_to_delete integer[];
|
||||
company integer;
|
||||
ccode text;
|
||||
product edited_quote_product;
|
||||
qpid integer;
|
||||
begin
|
||||
update quote
|
||||
set quote_status = edit_quote.quote_status
|
||||
, terms_and_conditions = edit_quote.terms_and_conditions
|
||||
, notes = edit_quote.notes
|
||||
-- contact_id = edit_quote.contact_id
|
||||
--, payment_method_id = edit_quote.payment_method_id
|
||||
, tags = edit_quote.tags
|
||||
where slug = quote_slug
|
||||
returning quote_id, company_id, currency_code
|
||||
into qid, company, ccode
|
||||
;
|
||||
|
||||
if qid is null then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
if payment_method_id is null then
|
||||
delete from quote_payment_method where quote_id = qid;
|
||||
else
|
||||
insert into quote_payment_method (quote_id, payment_method_id)
|
||||
values (qid, payment_method_id)
|
||||
on conflict (quote_id) do update
|
||||
set payment_method_id = edit_quote.payment_method_id;
|
||||
end if;
|
||||
|
||||
if contact_id is null then
|
||||
delete from quote_contact where quote_id = qid;
|
||||
else
|
||||
insert into quote_contact (quote_id, contact_id)
|
||||
values (qid, contact_id)
|
||||
on conflict (quote_id) do update
|
||||
set contact_id = edit_quote.contact_id;
|
||||
end if;
|
||||
|
||||
foreach product in array products
|
||||
loop
|
||||
if product.quote_product_id is null then
|
||||
insert into quote_product (quote_id, name, description, price, quantity, discount_rate)
|
||||
select qid
|
||||
, product.name
|
||||
, coalesce(product.description, '')
|
||||
, parse_price(product.price, currency.decimal_digits)
|
||||
, product.quantity
|
||||
, product.discount_rate
|
||||
from currency
|
||||
where currency_code = ccode
|
||||
returning quote_product_id
|
||||
into qpid;
|
||||
else
|
||||
qpid := product.quote_product_id;
|
||||
|
||||
update quote_product
|
||||
set name = product.name
|
||||
, description = coalesce(product.description, '')
|
||||
, price = parse_price(product.price, currency.decimal_digits)
|
||||
, quantity = product.quantity
|
||||
, discount_rate = product.discount_rate
|
||||
from currency
|
||||
where quote_product_id = qpid
|
||||
and currency_code = ccode;
|
||||
end if;
|
||||
products_to_keep := array_append(products_to_keep, qpid);
|
||||
|
||||
if product.product_id is null then
|
||||
delete from quote_product_product where quote_product_id = qpid;
|
||||
else
|
||||
insert into quote_product_product (quote_product_id, product_id)
|
||||
values (qpid, product.product_id)
|
||||
on conflict (quote_product_id) do update
|
||||
set product_id = product.product_id;
|
||||
end if;
|
||||
|
||||
delete from quote_product_tax where quote_product_id = qpid;
|
||||
|
||||
insert into quote_product_tax (quote_product_id, tax_id, tax_rate)
|
||||
select qpid, tax_id, tax.rate
|
||||
from tax
|
||||
join unnest(product.tax) as ptax(tax_id) using (tax_id);
|
||||
end loop;
|
||||
|
||||
select array_agg(quote_product_id)
|
||||
into products_to_delete
|
||||
from quote_product
|
||||
where quote_id = qid
|
||||
and not (quote_product_id = any(products_to_keep));
|
||||
|
||||
if array_length(products_to_delete, 1) > 0 then
|
||||
delete from quote_product_tax where quote_product_id = any(products_to_delete);
|
||||
delete from quote_product_product where quote_product_id = any(products_to_delete);
|
||||
delete from quote_product where quote_product_id = any(products_to_delete);
|
||||
end if;
|
||||
|
||||
return quote_slug;
|
||||
end;
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function edit_quote(uuid, text, integer, text, text, integer, tag_name[], edited_quote_product[]) from public;
|
||||
grant execute on function edit_quote(uuid, text, integer, text, text, integer, tag_name[], edited_quote_product[]) to invoicer;
|
||||
grant execute on function edit_quote(uuid, text, integer, text, text, integer, tag_name[], edited_quote_product[]) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,20 @@
|
|||
-- Deploy numerus:edited_quote_product to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: discount_rate
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create type edited_quote_product as
|
||||
( quote_product_id integer
|
||||
, product_id integer
|
||||
, name text
|
||||
, description text
|
||||
, price text
|
||||
, quantity integer
|
||||
, discount_rate discount_rate
|
||||
, tax integer[]
|
||||
);
|
||||
|
||||
commit;
|
|
@ -7,7 +7,7 @@ begin;
|
|||
set search_path to numerus, public;
|
||||
|
||||
create table invoice_number_counter (
|
||||
company_id integer not null,
|
||||
company_id integer not null references company,
|
||||
year integer not null constraint year_always_positive check(year > 0),
|
||||
currval integer not null constraint counter_zero_or_positive check(currval >= 0),
|
||||
primary key (company_id, year)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
-- Deploy numerus:new_quote_amount to pg
|
||||
-- requires: schema_numerus
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create type new_quote_amount as (
|
||||
subtotal text,
|
||||
taxes text[][],
|
||||
total text
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,19 @@
|
|||
-- Deploy numerus:new_quote_product to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: discount_rate
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create type new_quote_product as (
|
||||
product_id integer,
|
||||
name text,
|
||||
description text,
|
||||
price text,
|
||||
quantity integer,
|
||||
discount_rate discount_rate,
|
||||
tax integer[]
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,40 @@
|
|||
-- Deploy numerus:next_quote_number to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: quote_number_counter
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function next_quote_number(company integer, quote_date date) returns text
|
||||
as
|
||||
$$
|
||||
declare
|
||||
num integer;
|
||||
quote_number text;
|
||||
begin
|
||||
insert into quote_number_counter (company_id, year, currval)
|
||||
values (next_quote_number.company, date_part('year', quote_date), 1)
|
||||
on conflict (company_id, year) do
|
||||
update
|
||||
set currval = quote_number_counter.currval + 1
|
||||
returning currval
|
||||
into num;
|
||||
|
||||
select to_char(quote_date, to_char(num, 'FM' || replace(quote_number_format, '"', '\""')))
|
||||
into quote_number
|
||||
from company
|
||||
where company_id = next_quote_number.company;
|
||||
|
||||
return quote_number;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function next_quote_number(integer, date) from public;
|
||||
grant execute on function next_quote_number(integer, date) to invoicer;
|
||||
grant execute on function next_quote_number(integer, date) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,23 @@
|
|||
-- Deploy numerus:quote_amount to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: quote_product
|
||||
-- requires: quote_product_amount
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace view quote_amount as
|
||||
select quote_id
|
||||
, sum(subtotal)::integer as subtotal
|
||||
, sum(total)::integer as total
|
||||
from quote_product
|
||||
join quote_product_amount using (quote_product_id)
|
||||
group by quote_id
|
||||
;
|
||||
|
||||
grant select on table quote_amount to invoicer;
|
||||
grant select on table quote_amount to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,33 @@
|
|||
-- Deploy numerus:quote_number_counter to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: company
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table quote_number_counter (
|
||||
company_id integer not null references company,
|
||||
year integer not null constraint year_always_positive check(year > 0),
|
||||
currval integer not null constraint counter_zero_or_positive check(currval >= 0),
|
||||
primary key (company_id, year)
|
||||
);
|
||||
|
||||
grant select, insert, update on table quote_number_counter to invoicer;
|
||||
grant select, insert, update on table quote_number_counter to admin;
|
||||
|
||||
alter table quote_number_counter enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on quote_number_counter
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = quote_number_counter.company_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,23 @@
|
|||
-- Deploy numerus:quote_product_amount to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: quote_product
|
||||
-- requires: quote_product_tax
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace view quote_product_amount as
|
||||
select quote_product_id
|
||||
, round(price * quantity * (1 - discount_rate))::integer as subtotal
|
||||
, max(round(price * quantity * (1 - discount_rate))::integer) + coalesce(sum(round(round(price * quantity * (1 - discount_rate))::integer * tax_rate)::integer)::integer, 0) as total
|
||||
from quote_product
|
||||
left join quote_product_tax using (quote_product_id)
|
||||
group by quote_product_id, price, quantity, discount_rate
|
||||
;
|
||||
|
||||
grant select on table quote_product_amount to invoicer;
|
||||
grant select on table quote_product_amount to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,24 @@
|
|||
-- Deploy numerus:quote_tax_amount to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: quote_product
|
||||
-- requires: quote_product_tax
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace view quote_tax_amount as
|
||||
select quote_id
|
||||
, tax_id
|
||||
, sum(round(round(price * quantity * (1 - discount_rate))::integer * tax_rate)::integer)::integer as amount
|
||||
from quote_product
|
||||
join quote_product_tax using (quote_product_id)
|
||||
group by quote_id
|
||||
, tax_id
|
||||
;
|
||||
|
||||
grant select on table quote_tax_amount to invoicer;
|
||||
grant select on table quote_tax_amount to admin;
|
||||
|
||||
commit;
|
|
@ -604,8 +604,8 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
|||
Name: "payment_method",
|
||||
Required: true,
|
||||
Label: pgettext("input", "Payment Method", locale),
|
||||
Selected: []string{conn.MustGetText(ctx, "", "select default_payment_method_id::text from company where company_id = $1", company.Id)},
|
||||
Options: MustGetOptions(ctx, conn, "select payment_method_id::text, name from payment_method where company_id = $1", company.Id),
|
||||
Selected: []string{mustGetDefaultPaymentMethod(ctx, conn, company)},
|
||||
Options: mustGetPaymentMethodOptions(ctx, conn, company),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -782,6 +782,14 @@ func mustGetContactOptions(ctx context.Context, conn *Conn, company *Company) []
|
|||
return MustGetOptions(ctx, conn, "select contact_id::text, business_name from contact where company_id = $1 order by business_name", company.Id)
|
||||
}
|
||||
|
||||
func mustGetDefaultPaymentMethod(ctx context.Context, conn *Conn, company *Company) string {
|
||||
return conn.MustGetText(ctx, "", "select default_payment_method_id::text from company where company_id = $1", company.Id)
|
||||
}
|
||||
|
||||
func mustGetPaymentMethodOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption {
|
||||
return MustGetOptions(ctx, conn, "select payment_method_id::text, name from payment_method where company_id = $1", company.Id)
|
||||
}
|
||||
|
||||
type invoiceProductForm struct {
|
||||
locale *Locale
|
||||
company *Company
|
||||
|
@ -1044,9 +1052,9 @@ func HandleEditInvoiceAction(w http.ResponseWriter, r *http.Request, params http
|
|||
})
|
||||
}
|
||||
|
||||
type renderFormFunc func(w http.ResponseWriter, r *http.Request, form *invoiceForm)
|
||||
type renderInvoiceFormFunc func(w http.ResponseWriter, r *http.Request, form *invoiceForm)
|
||||
|
||||
func handleInvoiceAction(w http.ResponseWriter, r *http.Request, action string, renderForm renderFormFunc) {
|
||||
func handleInvoiceAction(w http.ResponseWriter, r *http.Request, action string, renderForm renderInvoiceFormFunc) {
|
||||
locale := getLocale(r)
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
|
|
119
pkg/pgtypes.go
119
pkg/pgtypes.go
|
@ -66,6 +66,65 @@ func (src EditedInvoiceProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byt
|
|||
return array.EncodeBinary(ci, buf)
|
||||
}
|
||||
|
||||
type NewQuoteProductArray []*quoteProductForm
|
||||
|
||||
func (src NewQuoteProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
|
||||
typeName := "new_quote_product[]"
|
||||
dt, ok := ci.DataTypeForName(typeName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
||||
}
|
||||
var values [][]interface{}
|
||||
for _, form := range src {
|
||||
var productId interface{} = form.ProductId.Val
|
||||
if form.ProductId.Val == "" {
|
||||
productId = nil
|
||||
}
|
||||
values = append(values, []interface{}{
|
||||
productId,
|
||||
form.Name.Val,
|
||||
form.Description.Val,
|
||||
form.Price.Val,
|
||||
form.Quantity.Val,
|
||||
form.Discount.Float64() / 100.0,
|
||||
form.Tax.Selected,
|
||||
})
|
||||
}
|
||||
array := pgtype.NewValue(dt.Value).(pgtype.ValueTranscoder)
|
||||
if err := array.Set(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return array.EncodeBinary(ci, buf)
|
||||
}
|
||||
|
||||
type EditedQuoteProductArray []*quoteProductForm
|
||||
|
||||
func (src EditedQuoteProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
|
||||
typeName := "edited_quote_product[]"
|
||||
dt, ok := ci.DataTypeForName(typeName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
||||
}
|
||||
var values [][]interface{}
|
||||
for _, form := range src {
|
||||
values = append(values, []interface{}{
|
||||
form.QuoteProductId.IntegerOrNil(),
|
||||
form.ProductId.IntegerOrNil(),
|
||||
form.Name.Val,
|
||||
form.Description.Val,
|
||||
form.Price.Val,
|
||||
form.Quantity.Val,
|
||||
form.Discount.Float64() / 100.0,
|
||||
form.Tax.Selected,
|
||||
})
|
||||
}
|
||||
array := pgtype.NewValue(dt.Value).(pgtype.ValueTranscoder)
|
||||
if err := array.Set(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return array.EncodeBinary(ci, buf)
|
||||
}
|
||||
|
||||
func registerPgTypes(ctx context.Context, conn *pgx.Conn) error {
|
||||
if _, err := conn.Exec(ctx, "set role to admin"); err != nil {
|
||||
return err
|
||||
|
@ -84,6 +143,7 @@ func registerPgTypes(ctx context.Context, conn *pgx.Conn) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newInvoiceProduct, err := pgtype.NewCompositeType(
|
||||
"new_invoice_product",
|
||||
[]pgtype.CompositeTypeField{
|
||||
|
@ -143,6 +203,65 @@ func registerPgTypes(ctx context.Context, conn *pgx.Conn) error {
|
|||
return err
|
||||
}
|
||||
|
||||
newQuoteProduct, err := pgtype.NewCompositeType(
|
||||
"new_quote_product",
|
||||
[]pgtype.CompositeTypeField{
|
||||
{"product_id", pgtype.Int4OID},
|
||||
{"name", pgtype.TextOID},
|
||||
{"description", pgtype.TextOID},
|
||||
{"price", pgtype.TextOID},
|
||||
{"quantity", pgtype.Int4OID},
|
||||
{"discount_rate", discountRateOID},
|
||||
{"tax", pgtype.Int4ArrayOID},
|
||||
},
|
||||
conn.ConnInfo(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newQuoteProductOID, err := registerPgType(ctx, conn, newQuoteProduct, newQuoteProduct.TypeName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newQuoteProductArray := pgtype.NewArrayType("new_quote_product[]", newQuoteProductOID, func() pgtype.ValueTranscoder {
|
||||
value := newQuoteProduct.NewTypeValue()
|
||||
return value.(pgtype.ValueTranscoder)
|
||||
})
|
||||
_, err = registerPgType(ctx, conn, newQuoteProductArray, newQuoteProductArray.TypeName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
editedQuoteProduct, err := pgtype.NewCompositeType(
|
||||
"edited_quote_product",
|
||||
[]pgtype.CompositeTypeField{
|
||||
{"quote_product_id", pgtype.Int4OID},
|
||||
{"product_id", pgtype.Int4OID},
|
||||
{"name", pgtype.TextOID},
|
||||
{"description", pgtype.TextOID},
|
||||
{"price", pgtype.TextOID},
|
||||
{"quantity", pgtype.Int4OID},
|
||||
{"discount_rate", discountRateOID},
|
||||
{"tax", pgtype.Int4ArrayOID},
|
||||
},
|
||||
conn.ConnInfo(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
editedQuoteProductOID, err := registerPgType(ctx, conn, editedQuoteProduct, editedQuoteProduct.TypeName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
editedQuoteProductArray := pgtype.NewArrayType("edited_quote_product[]", editedQuoteProductOID, func() pgtype.ValueTranscoder {
|
||||
value := editedQuoteProduct.NewTypeValue()
|
||||
return value.(pgtype.ValueTranscoder)
|
||||
})
|
||||
_, err = registerPgType(ctx, conn, editedQuoteProductArray, editedQuoteProductArray.TypeName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = conn.Exec(ctx, "reset role")
|
||||
return err
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -37,6 +37,15 @@ func NewRouter(db *Db) http.Handler {
|
|||
companyRouter.POST("/invoices/:slug/edit", HandleEditInvoiceAction)
|
||||
companyRouter.PUT("/invoices/:slug/tags", HandleUpdateInvoiceTags)
|
||||
companyRouter.GET("/invoices/:slug/tags/edit", ServeEditInvoiceTags)
|
||||
companyRouter.GET("/quotes", IndexQuotes)
|
||||
companyRouter.POST("/quotes", HandleAddQuote)
|
||||
companyRouter.GET("/quotes/:slug", ServeQuote)
|
||||
companyRouter.PUT("/quotes/:slug", HandleUpdateQuote)
|
||||
companyRouter.POST("/quotes/:slug", HandleNewQuoteAction)
|
||||
companyRouter.GET("/quotes/:slug/edit", ServeEditQuote)
|
||||
companyRouter.POST("/quotes/:slug/edit", HandleEditQuoteAction)
|
||||
companyRouter.PUT("/quotes/:slug/tags", HandleUpdateQuoteTags)
|
||||
companyRouter.GET("/quotes/:slug/tags/edit", ServeEditQuoteTags)
|
||||
companyRouter.GET("/search/products", HandleProductSearch)
|
||||
companyRouter.GET("/expenses", IndexExpenses)
|
||||
companyRouter.POST("/expenses", HandleAddExpense)
|
||||
|
|
466
po/ca.po
466
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-05-29 00:02+0200\n"
|
||||
"POT-Creation-Date: 2023-06-07 16:05+0200\n"
|
||||
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||
|
@ -25,12 +25,15 @@ msgstr "Afegeix productes a la factura"
|
|||
|
||||
#: web/template/invoices/products.gohtml:9 web/template/invoices/new.gohtml:9
|
||||
#: web/template/invoices/index.gohtml:9 web/template/invoices/view.gohtml:9
|
||||
#: web/template/invoices/edit.gohtml:9 web/template/contacts/new.gohtml:9
|
||||
#: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10
|
||||
#: web/template/profile.gohtml:9 web/template/expenses/new.gohtml:10
|
||||
#: web/template/expenses/index.gohtml:10 web/template/expenses/edit.gohtml:10
|
||||
#: web/template/tax-details.gohtml:9 web/template/products/new.gohtml:9
|
||||
#: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10
|
||||
#: web/template/invoices/edit.gohtml:9 web/template/quotes/products.gohtml:9
|
||||
#: web/template/quotes/new.gohtml:9 web/template/quotes/index.gohtml:9
|
||||
#: web/template/quotes/view.gohtml:9 web/template/quotes/edit.gohtml:9
|
||||
#: web/template/contacts/new.gohtml:9 web/template/contacts/index.gohtml:9
|
||||
#: web/template/contacts/edit.gohtml:10 web/template/profile.gohtml:9
|
||||
#: web/template/expenses/new.gohtml:10 web/template/expenses/index.gohtml:10
|
||||
#: web/template/expenses/edit.gohtml:10 web/template/tax-details.gohtml:9
|
||||
#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:9
|
||||
#: web/template/products/edit.gohtml:10
|
||||
msgctxt "title"
|
||||
msgid "Home"
|
||||
msgstr "Inici"
|
||||
|
@ -49,60 +52,70 @@ msgid "New Invoice"
|
|||
msgstr "Nova factura"
|
||||
|
||||
#: web/template/invoices/products.gohtml:48
|
||||
#: web/template/quotes/products.gohtml:48
|
||||
msgctxt "product"
|
||||
msgid "All"
|
||||
msgstr "Tots"
|
||||
|
||||
#: web/template/invoices/products.gohtml:49
|
||||
#: web/template/products/index.gohtml:40
|
||||
#: web/template/quotes/products.gohtml:49 web/template/products/index.gohtml:40
|
||||
msgctxt "title"
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: web/template/invoices/products.gohtml:50
|
||||
#: web/template/invoices/view.gohtml:62 web/template/products/index.gohtml:42
|
||||
#: web/template/invoices/view.gohtml:62 web/template/quotes/products.gohtml:50
|
||||
#: web/template/quotes/view.gohtml:62 web/template/products/index.gohtml:42
|
||||
msgctxt "title"
|
||||
msgid "Price"
|
||||
msgstr "Preu"
|
||||
|
||||
#: web/template/invoices/products.gohtml:64
|
||||
#: web/template/products/index.gohtml:82
|
||||
#: web/template/quotes/products.gohtml:64 web/template/products/index.gohtml:82
|
||||
msgid "No products added yet."
|
||||
msgstr "No hi ha cap producte."
|
||||
|
||||
#: web/template/invoices/products.gohtml:72 web/template/invoices/new.gohtml:83
|
||||
#: web/template/invoices/edit.gohtml:84
|
||||
#: web/template/invoices/edit.gohtml:84 web/template/quotes/products.gohtml:72
|
||||
#: web/template/quotes/new.gohtml:84 web/template/quotes/edit.gohtml:85
|
||||
msgctxt "action"
|
||||
msgid "Add products"
|
||||
msgstr "Afegeix productes"
|
||||
|
||||
#: web/template/invoices/new.gohtml:27 web/template/invoices/edit.gohtml:27
|
||||
#: web/template/quotes/new.gohtml:27 web/template/quotes/edit.gohtml:27
|
||||
msgid "Product “%s” removed"
|
||||
msgstr "S’ha esborrat el producte «%s»"
|
||||
|
||||
#: web/template/invoices/new.gohtml:31 web/template/invoices/edit.gohtml:31
|
||||
#: web/template/quotes/new.gohtml:31 web/template/quotes/edit.gohtml:31
|
||||
msgctxt "action"
|
||||
msgid "Undo"
|
||||
msgstr "Desfes"
|
||||
|
||||
#: web/template/invoices/new.gohtml:60 web/template/invoices/view.gohtml:67
|
||||
#: web/template/invoices/edit.gohtml:61
|
||||
#: web/template/invoices/edit.gohtml:61 web/template/quotes/new.gohtml:61
|
||||
#: web/template/quotes/view.gohtml:67 web/template/quotes/edit.gohtml:62
|
||||
msgctxt "title"
|
||||
msgid "Subtotal"
|
||||
msgstr "Subtotal"
|
||||
|
||||
#: web/template/invoices/new.gohtml:70 web/template/invoices/view.gohtml:71
|
||||
#: web/template/invoices/view.gohtml:111 web/template/invoices/edit.gohtml:71
|
||||
#: web/template/quotes/new.gohtml:71 web/template/quotes/view.gohtml:71
|
||||
#: web/template/quotes/view.gohtml:111 web/template/quotes/edit.gohtml:72
|
||||
msgctxt "title"
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
||||
#: web/template/invoices/new.gohtml:87 web/template/invoices/edit.gohtml:88
|
||||
#: web/template/quotes/new.gohtml:88 web/template/quotes/edit.gohtml:89
|
||||
msgctxt "action"
|
||||
msgid "Update"
|
||||
msgstr "Actualitza"
|
||||
|
||||
#: web/template/invoices/new.gohtml:90 web/template/invoices/edit.gohtml:91
|
||||
#: web/template/quotes/new.gohtml:91 web/template/quotes/edit.gohtml:92
|
||||
#: web/template/contacts/new.gohtml:39 web/template/contacts/edit.gohtml:43
|
||||
#: web/template/expenses/new.gohtml:33 web/template/expenses/edit.gohtml:38
|
||||
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36
|
||||
|
@ -121,8 +134,8 @@ msgid "New invoice"
|
|||
msgstr "Nova factura"
|
||||
|
||||
#: web/template/invoices/index.gohtml:43 web/template/dashboard.gohtml:23
|
||||
#: web/template/contacts/index.gohtml:34 web/template/expenses/index.gohtml:36
|
||||
#: web/template/products/index.gohtml:34
|
||||
#: web/template/quotes/index.gohtml:43 web/template/contacts/index.gohtml:34
|
||||
#: web/template/expenses/index.gohtml:36 web/template/products/index.gohtml:34
|
||||
msgctxt "action"
|
||||
msgid "Filter"
|
||||
msgstr "Filtra"
|
||||
|
@ -133,6 +146,7 @@ msgid "All"
|
|||
msgstr "Totes"
|
||||
|
||||
#: web/template/invoices/index.gohtml:50 web/template/invoices/view.gohtml:34
|
||||
#: web/template/quotes/index.gohtml:50 web/template/quotes/view.gohtml:34
|
||||
msgctxt "title"
|
||||
msgid "Date"
|
||||
msgstr "Data"
|
||||
|
@ -142,34 +156,39 @@ msgctxt "title"
|
|||
msgid "Invoice Num."
|
||||
msgstr "Núm. factura"
|
||||
|
||||
#: web/template/invoices/index.gohtml:52 web/template/contacts/index.gohtml:40
|
||||
#: web/template/invoices/index.gohtml:52 web/template/quotes/index.gohtml:52
|
||||
#: web/template/contacts/index.gohtml:40
|
||||
msgctxt "title"
|
||||
msgid "Customer"
|
||||
msgstr "Client"
|
||||
|
||||
#: web/template/invoices/index.gohtml:53
|
||||
#: web/template/invoices/index.gohtml:53 web/template/quotes/index.gohtml:53
|
||||
msgctxt "title"
|
||||
msgid "Status"
|
||||
msgstr "Estat"
|
||||
|
||||
#: web/template/invoices/index.gohtml:54 web/template/contacts/index.gohtml:43
|
||||
#: web/template/expenses/index.gohtml:46 web/template/products/index.gohtml:41
|
||||
#: web/template/invoices/index.gohtml:54 web/template/quotes/index.gohtml:54
|
||||
#: web/template/contacts/index.gohtml:43 web/template/expenses/index.gohtml:46
|
||||
#: web/template/products/index.gohtml:41
|
||||
msgctxt "title"
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetes"
|
||||
|
||||
#: web/template/invoices/index.gohtml:55 web/template/expenses/index.gohtml:47
|
||||
#: web/template/invoices/index.gohtml:55 web/template/quotes/index.gohtml:55
|
||||
#: web/template/expenses/index.gohtml:47
|
||||
msgctxt "title"
|
||||
msgid "Amount"
|
||||
msgstr "Import"
|
||||
|
||||
#: web/template/invoices/index.gohtml:56 web/template/expenses/index.gohtml:48
|
||||
#: web/template/invoices/index.gohtml:56 web/template/quotes/index.gohtml:56
|
||||
#: web/template/expenses/index.gohtml:48
|
||||
msgctxt "title"
|
||||
msgid "Download"
|
||||
msgstr "Descàrrega"
|
||||
|
||||
#: web/template/invoices/index.gohtml:57 web/template/contacts/index.gohtml:44
|
||||
#: web/template/expenses/index.gohtml:49 web/template/products/index.gohtml:43
|
||||
#: web/template/invoices/index.gohtml:57 web/template/quotes/index.gohtml:57
|
||||
#: web/template/contacts/index.gohtml:44 web/template/expenses/index.gohtml:49
|
||||
#: web/template/products/index.gohtml:43
|
||||
msgctxt "title"
|
||||
msgid "Actions"
|
||||
msgstr "Accions"
|
||||
|
@ -180,6 +199,7 @@ msgid "Select invoice %v"
|
|||
msgstr "Selecciona factura %v"
|
||||
|
||||
#: web/template/invoices/index.gohtml:119 web/template/invoices/view.gohtml:19
|
||||
#: web/template/quotes/index.gohtml:119 web/template/quotes/view.gohtml:19
|
||||
#: web/template/contacts/index.gohtml:74 web/template/expenses/index.gohtml:88
|
||||
#: web/template/products/index.gohtml:72
|
||||
msgctxt "action"
|
||||
|
@ -187,6 +207,7 @@ msgid "Edit"
|
|||
msgstr "Edita"
|
||||
|
||||
#: web/template/invoices/index.gohtml:127 web/template/invoices/view.gohtml:16
|
||||
#: web/template/quotes/index.gohtml:127 web/template/quotes/view.gohtml:16
|
||||
msgctxt "action"
|
||||
msgid "Duplicate"
|
||||
msgstr "Duplica"
|
||||
|
@ -205,22 +226,22 @@ msgctxt "action"
|
|||
msgid "Download invoice"
|
||||
msgstr "Descarrega factura"
|
||||
|
||||
#: web/template/invoices/view.gohtml:61
|
||||
#: web/template/invoices/view.gohtml:61 web/template/quotes/view.gohtml:61
|
||||
msgctxt "title"
|
||||
msgid "Concept"
|
||||
msgstr "Concepte"
|
||||
|
||||
#: web/template/invoices/view.gohtml:64
|
||||
#: web/template/invoices/view.gohtml:64 web/template/quotes/view.gohtml:64
|
||||
msgctxt "title"
|
||||
msgid "Discount"
|
||||
msgstr "Descompte"
|
||||
|
||||
#: web/template/invoices/view.gohtml:66
|
||||
#: web/template/invoices/view.gohtml:66 web/template/quotes/view.gohtml:66
|
||||
msgctxt "title"
|
||||
msgid "Units"
|
||||
msgstr "Unitats"
|
||||
|
||||
#: web/template/invoices/view.gohtml:101
|
||||
#: web/template/invoices/view.gohtml:101 web/template/quotes/view.gohtml:101
|
||||
msgctxt "title"
|
||||
msgid "Tax Base"
|
||||
msgstr "Base imposable"
|
||||
|
@ -235,7 +256,7 @@ msgctxt "input"
|
|||
msgid "(Max. %s)"
|
||||
msgstr "(Màx. %s)"
|
||||
|
||||
#: web/template/form.gohtml:171
|
||||
#: web/template/form.gohtml:194
|
||||
msgctxt "action"
|
||||
msgid "Filters"
|
||||
msgstr "Filtra"
|
||||
|
@ -275,6 +296,68 @@ msgctxt "term"
|
|||
msgid "Net Income"
|
||||
msgstr "Ingressos nets"
|
||||
|
||||
#: web/template/quotes/products.gohtml:2 web/template/quotes/products.gohtml:23
|
||||
msgctxt "title"
|
||||
msgid "Add Products to Quotation"
|
||||
msgstr "Afegeix productes al pressupost"
|
||||
|
||||
#: web/template/quotes/products.gohtml:10 web/template/quotes/new.gohtml:10
|
||||
#: web/template/quotes/index.gohtml:2 web/template/quotes/index.gohtml:10
|
||||
#: web/template/quotes/view.gohtml:10 web/template/quotes/edit.gohtml:10
|
||||
msgctxt "title"
|
||||
msgid "Quotations"
|
||||
msgstr "Pressuposts"
|
||||
|
||||
#: web/template/quotes/products.gohtml:12 web/template/quotes/new.gohtml:2
|
||||
#: web/template/quotes/new.gohtml:11 web/template/quotes/new.gohtml:19
|
||||
msgctxt "title"
|
||||
msgid "New Quotation"
|
||||
msgstr "Nou pressupost"
|
||||
|
||||
#: web/template/quotes/index.gohtml:19
|
||||
msgctxt "action"
|
||||
msgid "Download quotations"
|
||||
msgstr "Descarrega pressuposts"
|
||||
|
||||
#: web/template/quotes/index.gohtml:21
|
||||
msgctxt "action"
|
||||
msgid "New quotation"
|
||||
msgstr "Nou pressupost"
|
||||
|
||||
#: web/template/quotes/index.gohtml:49
|
||||
msgctxt "quote"
|
||||
msgid "All"
|
||||
msgstr "Tots"
|
||||
|
||||
#: web/template/quotes/index.gohtml:51
|
||||
msgctxt "title"
|
||||
msgid "Quotation Num."
|
||||
msgstr "Núm. pressupost"
|
||||
|
||||
#: web/template/quotes/index.gohtml:64
|
||||
msgctxt "action"
|
||||
msgid "Select quotation %v"
|
||||
msgstr "Selecciona pressupost %v"
|
||||
|
||||
#: web/template/quotes/index.gohtml:137
|
||||
msgid "No quotations added yet."
|
||||
msgstr "No hi ha cap pressupost."
|
||||
|
||||
#: web/template/quotes/view.gohtml:2 web/template/quotes/view.gohtml:33
|
||||
msgctxt "title"
|
||||
msgid "Quotation %s"
|
||||
msgstr "Pressupost %s"
|
||||
|
||||
#: web/template/quotes/view.gohtml:22
|
||||
msgctxt "action"
|
||||
msgid "Download quotation"
|
||||
msgstr "Descarrega pressupost"
|
||||
|
||||
#: web/template/quotes/edit.gohtml:2 web/template/quotes/edit.gohtml:19
|
||||
msgctxt "title"
|
||||
msgid "Edit Quotation “%s”"
|
||||
msgstr "Edició del pressupost «%s»"
|
||||
|
||||
#: web/template/app.gohtml:23
|
||||
msgctxt "menu"
|
||||
msgid "Account"
|
||||
|
@ -297,20 +380,25 @@ msgstr "Tauler"
|
|||
|
||||
#: web/template/app.gohtml:47
|
||||
msgctxt "nav"
|
||||
msgid "Quotations"
|
||||
msgstr "Pressuposts"
|
||||
|
||||
#: web/template/app.gohtml:48
|
||||
msgctxt "nav"
|
||||
msgid "Invoices"
|
||||
msgstr "Factures"
|
||||
|
||||
#: web/template/app.gohtml:48
|
||||
#: web/template/app.gohtml:49
|
||||
msgctxt "nav"
|
||||
msgid "Expenses"
|
||||
msgstr "Despeses"
|
||||
|
||||
#: web/template/app.gohtml:49
|
||||
#: web/template/app.gohtml:50
|
||||
msgctxt "nav"
|
||||
msgid "Products"
|
||||
msgstr "Productes"
|
||||
|
||||
#: web/template/app.gohtml:50
|
||||
#: web/template/app.gohtml:51
|
||||
msgctxt "nav"
|
||||
msgid "Contacts"
|
||||
msgstr "Contactes"
|
||||
|
@ -382,7 +470,7 @@ msgctxt "title"
|
|||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: web/template/profile.gohtml:39 web/template/tax-details.gohtml:172
|
||||
#: web/template/profile.gohtml:39 web/template/tax-details.gohtml:173
|
||||
msgctxt "action"
|
||||
msgid "Save changes"
|
||||
msgstr "Desa canvis"
|
||||
|
@ -449,54 +537,54 @@ msgctxt "title"
|
|||
msgid "Invoicing"
|
||||
msgstr "Facturació"
|
||||
|
||||
#: web/template/tax-details.gohtml:53
|
||||
#: web/template/tax-details.gohtml:54
|
||||
msgid "Are you sure?"
|
||||
msgstr "N’esteu segur?"
|
||||
|
||||
#: web/template/tax-details.gohtml:59
|
||||
#: web/template/tax-details.gohtml:60
|
||||
msgctxt "title"
|
||||
msgid "Tax Name"
|
||||
msgstr "Nom impost"
|
||||
|
||||
#: web/template/tax-details.gohtml:60
|
||||
#: web/template/tax-details.gohtml:61
|
||||
msgctxt "title"
|
||||
msgid "Rate (%)"
|
||||
msgstr "Percentatge"
|
||||
|
||||
#: web/template/tax-details.gohtml:61
|
||||
#: web/template/tax-details.gohtml:62
|
||||
msgctxt "title"
|
||||
msgid "Class"
|
||||
msgstr "Classe"
|
||||
|
||||
#: web/template/tax-details.gohtml:85
|
||||
#: web/template/tax-details.gohtml:86
|
||||
msgid "No taxes added yet."
|
||||
msgstr "No hi ha cap impost."
|
||||
|
||||
#: web/template/tax-details.gohtml:91 web/template/tax-details.gohtml:152
|
||||
#: web/template/tax-details.gohtml:92 web/template/tax-details.gohtml:153
|
||||
msgctxt "title"
|
||||
msgid "New Line"
|
||||
msgstr "Nova línia"
|
||||
|
||||
#: web/template/tax-details.gohtml:105
|
||||
#: web/template/tax-details.gohtml:106
|
||||
msgctxt "action"
|
||||
msgid "Add new tax"
|
||||
msgstr "Afegeix nou impost"
|
||||
|
||||
#: web/template/tax-details.gohtml:121
|
||||
#: web/template/tax-details.gohtml:122
|
||||
msgctxt "title"
|
||||
msgid "Payment Method"
|
||||
msgstr "Mètode de pagament"
|
||||
|
||||
#: web/template/tax-details.gohtml:122
|
||||
#: web/template/tax-details.gohtml:123
|
||||
msgctxt "title"
|
||||
msgid "Instructions"
|
||||
msgstr "Instruccions"
|
||||
|
||||
#: web/template/tax-details.gohtml:146
|
||||
#: web/template/tax-details.gohtml:147
|
||||
msgid "No payment methods added yet."
|
||||
msgstr "No hi ha cap mètode de pagament."
|
||||
|
||||
#: web/template/tax-details.gohtml:164
|
||||
#: web/template/tax-details.gohtml:165
|
||||
msgctxt "action"
|
||||
msgid "Add new payment method"
|
||||
msgstr "Afegeix nou mètode de pagament"
|
||||
|
@ -553,26 +641,27 @@ msgstr "No podeu deixar la contrasenya en blanc."
|
|||
msgid "Invalid user or password."
|
||||
msgstr "Nom d’usuari o contrasenya incorrectes."
|
||||
|
||||
#: pkg/products.go:164 pkg/products.go:263 pkg/invoices.go:816
|
||||
#: pkg/products.go:164 pkg/products.go:263 pkg/quote.go:793 pkg/invoices.go:824
|
||||
#: pkg/contacts.go:135
|
||||
msgctxt "input"
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: pkg/products.go:169 pkg/products.go:290 pkg/expenses.go:202
|
||||
#: pkg/expenses.go:361 pkg/invoices.go:189 pkg/invoices.go:601
|
||||
#: pkg/invoices.go:1115 pkg/contacts.go:140 pkg/contacts.go:325
|
||||
#: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:188 pkg/quote.go:606
|
||||
#: pkg/expenses.go:202 pkg/expenses.go:361 pkg/invoices.go:189
|
||||
#: pkg/invoices.go:601 pkg/invoices.go:1123 pkg/contacts.go:140
|
||||
#: pkg/contacts.go:325
|
||||
msgctxt "input"
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetes"
|
||||
|
||||
#: pkg/products.go:173 pkg/expenses.go:365 pkg/invoices.go:193
|
||||
#: pkg/products.go:173 pkg/quote.go:192 pkg/expenses.go:365 pkg/invoices.go:193
|
||||
#: pkg/contacts.go:144
|
||||
msgctxt "input"
|
||||
msgid "Tags Condition"
|
||||
msgstr "Condició de les etiquetes"
|
||||
|
||||
#: pkg/products.go:177 pkg/expenses.go:369 pkg/invoices.go:197
|
||||
#: pkg/products.go:177 pkg/quote.go:196 pkg/expenses.go:369 pkg/invoices.go:197
|
||||
#: pkg/contacts.go:148
|
||||
msgctxt "tag condition"
|
||||
msgid "All"
|
||||
|
@ -583,7 +672,7 @@ msgstr "Totes"
|
|||
msgid "Invoices must have all the specified labels."
|
||||
msgstr "Les factures han de tenir totes les etiquetes."
|
||||
|
||||
#: pkg/products.go:182 pkg/expenses.go:374 pkg/invoices.go:202
|
||||
#: pkg/products.go:182 pkg/quote.go:201 pkg/expenses.go:374 pkg/invoices.go:202
|
||||
#: pkg/contacts.go:153
|
||||
msgctxt "tag condition"
|
||||
msgid "Any"
|
||||
|
@ -594,119 +683,262 @@ msgstr "Qualsevol"
|
|||
msgid "Invoices must have at least one of the specified labels."
|
||||
msgstr "Les factures han de tenir com a mínim una de les etiquetes."
|
||||
|
||||
#: pkg/products.go:269 pkg/invoices.go:830
|
||||
#: pkg/products.go:269 pkg/quote.go:807 pkg/invoices.go:838
|
||||
msgctxt "input"
|
||||
msgid "Description"
|
||||
msgstr "Descripció"
|
||||
|
||||
#: pkg/products.go:274 pkg/invoices.go:834
|
||||
#: pkg/products.go:274 pkg/quote.go:811 pkg/invoices.go:842
|
||||
msgctxt "input"
|
||||
msgid "Price"
|
||||
msgstr "Preu"
|
||||
|
||||
#: pkg/products.go:284 pkg/expenses.go:181 pkg/invoices.go:863
|
||||
#: pkg/products.go:284 pkg/quote.go:840 pkg/expenses.go:181 pkg/invoices.go:871
|
||||
msgctxt "input"
|
||||
msgid "Taxes"
|
||||
msgstr "Imposts"
|
||||
|
||||
#: pkg/products.go:309 pkg/profile.go:92 pkg/invoices.go:912
|
||||
#: pkg/products.go:309 pkg/quote.go:889 pkg/profile.go:92 pkg/invoices.go:920
|
||||
msgid "Name can not be empty."
|
||||
msgstr "No podeu deixar el nom en blanc."
|
||||
|
||||
#: pkg/products.go:310 pkg/invoices.go:913
|
||||
#: pkg/products.go:310 pkg/quote.go:890 pkg/invoices.go:921
|
||||
msgid "Price can not be empty."
|
||||
msgstr "No podeu deixar el preu en blanc."
|
||||
|
||||
#: pkg/products.go:311 pkg/invoices.go:914
|
||||
#: pkg/products.go:311 pkg/quote.go:891 pkg/invoices.go:922
|
||||
msgid "Price must be a number greater than zero."
|
||||
msgstr "El preu ha de ser un número major a zero."
|
||||
|
||||
#: pkg/products.go:313 pkg/expenses.go:227 pkg/expenses.go:232
|
||||
#: pkg/invoices.go:922
|
||||
#: pkg/products.go:313 pkg/quote.go:899 pkg/expenses.go:227 pkg/expenses.go:232
|
||||
#: pkg/invoices.go:930
|
||||
msgid "Selected tax is not valid."
|
||||
msgstr "Heu seleccionat un impost que no és vàlid."
|
||||
|
||||
#: pkg/products.go:314 pkg/expenses.go:228 pkg/expenses.go:233
|
||||
#: pkg/invoices.go:923
|
||||
#: pkg/products.go:314 pkg/quote.go:900 pkg/expenses.go:228 pkg/expenses.go:233
|
||||
#: pkg/invoices.go:931
|
||||
msgid "You can only select a tax of each class."
|
||||
msgstr "Només podeu seleccionar un impost de cada classe."
|
||||
|
||||
#: pkg/company.go:98
|
||||
#: pkg/company.go:100
|
||||
msgctxt "input"
|
||||
msgid "Currency"
|
||||
msgstr "Moneda"
|
||||
|
||||
#: pkg/company.go:105
|
||||
#: pkg/company.go:107
|
||||
msgctxt "input"
|
||||
msgid "Invoice number format"
|
||||
msgstr "Format del número de factura"
|
||||
|
||||
#: pkg/company.go:111
|
||||
#: pkg/company.go:113
|
||||
msgctxt "input"
|
||||
msgid "Next invoice number"
|
||||
msgstr "Següent número de factura"
|
||||
|
||||
#: pkg/company.go:122
|
||||
msgctxt "input"
|
||||
msgid "Legal disclaimer"
|
||||
msgstr "Nota legal"
|
||||
|
||||
#: pkg/company.go:129
|
||||
#: pkg/company.go:141
|
||||
msgid "Selected currency is not valid."
|
||||
msgstr "Heu seleccionat una moneda que no és vàlida."
|
||||
|
||||
#: pkg/company.go:130
|
||||
#: pkg/company.go:142
|
||||
msgid "Invoice number format can not be empty."
|
||||
msgstr "No podeu deixar el format del número de factura en blanc."
|
||||
|
||||
#: pkg/company.go:297
|
||||
#: pkg/company.go:143
|
||||
msgid "Next invoice number must be a number greater than zero."
|
||||
msgstr "El següent número de factura ha de ser un número major a zero."
|
||||
|
||||
#: pkg/company.go:350
|
||||
msgctxt "input"
|
||||
msgid "Tax name"
|
||||
msgstr "Nom impost"
|
||||
|
||||
#: pkg/company.go:303
|
||||
#: pkg/company.go:356
|
||||
msgctxt "input"
|
||||
msgid "Tax Class"
|
||||
msgstr "Classe d’impost"
|
||||
|
||||
#: pkg/company.go:306
|
||||
#: pkg/company.go:359
|
||||
msgid "Select a tax class"
|
||||
msgstr "Escolliu una classe d’impost"
|
||||
|
||||
#: pkg/company.go:310
|
||||
#: pkg/company.go:363
|
||||
msgctxt "input"
|
||||
msgid "Rate (%)"
|
||||
msgstr "Percentatge"
|
||||
|
||||
#: pkg/company.go:333
|
||||
#: pkg/company.go:386
|
||||
msgid "Tax name can not be empty."
|
||||
msgstr "No podeu deixar el nom de l’impost en blanc."
|
||||
|
||||
#: pkg/company.go:334
|
||||
#: pkg/company.go:387
|
||||
msgid "Selected tax class is not valid."
|
||||
msgstr "Heu seleccionat una classe d’impost que no és vàlida."
|
||||
|
||||
#: pkg/company.go:335
|
||||
#: pkg/company.go:388
|
||||
msgid "Tax rate can not be empty."
|
||||
msgstr "No podeu deixar percentatge en blanc."
|
||||
|
||||
#: pkg/company.go:336
|
||||
#: pkg/company.go:389
|
||||
msgid "Tax rate must be an integer between -99 and 99."
|
||||
msgstr "El percentatge ha de ser entre -99 i 99."
|
||||
|
||||
#: pkg/company.go:399
|
||||
#: pkg/company.go:452
|
||||
msgctxt "input"
|
||||
msgid "Payment method name"
|
||||
msgstr "Nom del mètode de pagament"
|
||||
|
||||
#: pkg/company.go:405
|
||||
#: pkg/company.go:458
|
||||
msgctxt "input"
|
||||
msgid "Instructions"
|
||||
msgstr "Instruccions"
|
||||
|
||||
#: pkg/company.go:423
|
||||
#: pkg/company.go:476
|
||||
msgid "Payment method name can not be empty."
|
||||
msgstr "No podeu deixar el nom del mètode de pagament en blanc."
|
||||
|
||||
#: pkg/company.go:424
|
||||
#: pkg/company.go:477
|
||||
msgid "Payment instructions can not be empty."
|
||||
msgstr "No podeu deixar les instruccions de pagament en blanc."
|
||||
|
||||
#: pkg/quote.go:161 pkg/quote.go:585 pkg/expenses.go:340 pkg/invoices.go:162
|
||||
#: pkg/invoices.go:584
|
||||
msgctxt "input"
|
||||
msgid "Customer"
|
||||
msgstr "Client"
|
||||
|
||||
#: pkg/quote.go:162 pkg/expenses.go:341 pkg/invoices.go:163
|
||||
msgid "All customers"
|
||||
msgstr "Tots els clients"
|
||||
|
||||
#: pkg/quote.go:167 pkg/quote.go:579
|
||||
msgctxt "input"
|
||||
msgid "Quotation Status"
|
||||
msgstr "Estat del pressupost"
|
||||
|
||||
#: pkg/quote.go:168 pkg/invoices.go:169
|
||||
msgid "All status"
|
||||
msgstr "Tots els estats"
|
||||
|
||||
#: pkg/quote.go:173
|
||||
msgctxt "input"
|
||||
msgid "Quotation Number"
|
||||
msgstr "Número de pressupost"
|
||||
|
||||
#: pkg/quote.go:178 pkg/expenses.go:351 pkg/invoices.go:179
|
||||
msgctxt "input"
|
||||
msgid "From Date"
|
||||
msgstr "A partir de la data"
|
||||
|
||||
#: pkg/quote.go:183 pkg/expenses.go:356 pkg/invoices.go:184
|
||||
msgctxt "input"
|
||||
msgid "To Date"
|
||||
msgstr "Fins la data"
|
||||
|
||||
#: pkg/quote.go:197
|
||||
msgid "Quotations must have all the specified labels."
|
||||
msgstr "Els pressuposts han de tenir totes les etiquetes."
|
||||
|
||||
#: pkg/quote.go:202
|
||||
msgid "Quotations must have at least one of the specified labels."
|
||||
msgstr "Els pressuposts han de tenir com a mínim una de les etiquetes."
|
||||
|
||||
#: pkg/quote.go:451
|
||||
msgid "Select a customer to quote."
|
||||
msgstr "Escolliu un client a pressupostar."
|
||||
|
||||
#: pkg/quote.go:527
|
||||
msgid "quotations.zip"
|
||||
msgstr "pressuposts.zip"
|
||||
|
||||
#: pkg/quote.go:533 pkg/quote.go:1055 pkg/quote.go:1063 pkg/invoices.go:533
|
||||
#: pkg/invoices.go:1098 pkg/invoices.go:1106
|
||||
msgid "Invalid action"
|
||||
msgstr "Acció invàlida."
|
||||
|
||||
#: pkg/quote.go:590
|
||||
msgctxt "input"
|
||||
msgid "Quotation Date"
|
||||
msgstr "Data del pressupost"
|
||||
|
||||
#: pkg/quote.go:596
|
||||
msgctxt "input"
|
||||
msgid "Terms and conditions"
|
||||
msgstr "Condicions d’acceptació"
|
||||
|
||||
#: pkg/quote.go:601 pkg/invoices.go:596
|
||||
msgctxt "input"
|
||||
msgid "Notes"
|
||||
msgstr "Notes"
|
||||
|
||||
#: pkg/quote.go:610 pkg/invoices.go:606
|
||||
msgctxt "input"
|
||||
msgid "Payment Method"
|
||||
msgstr "Mètode de pagament"
|
||||
|
||||
#: pkg/quote.go:646
|
||||
msgid "Selected quotation status is not valid."
|
||||
msgstr "Heu seleccionat un estat de pressupost que no és vàlid."
|
||||
|
||||
#: pkg/quote.go:647 pkg/invoices.go:643
|
||||
msgid "Selected customer is not valid."
|
||||
msgstr "Heu seleccionat un client que no és vàlid."
|
||||
|
||||
#: pkg/quote.go:648
|
||||
msgid "Quotation date can not be empty."
|
||||
msgstr "No podeu deixar la data del pressupost en blanc."
|
||||
|
||||
#: pkg/quote.go:649
|
||||
msgid "Quotation date must be a valid date."
|
||||
msgstr "La data del pressupost ha de ser vàlida."
|
||||
|
||||
#: pkg/quote.go:651 pkg/invoices.go:647
|
||||
msgid "Selected payment method is not valid."
|
||||
msgstr "Heu seleccionat un mètode de pagament que no és vàlid."
|
||||
|
||||
#: pkg/quote.go:783 pkg/quote.go:788 pkg/invoices.go:814 pkg/invoices.go:819
|
||||
msgctxt "input"
|
||||
msgid "Id"
|
||||
msgstr "Identificador"
|
||||
|
||||
#: pkg/quote.go:821 pkg/invoices.go:852
|
||||
msgctxt "input"
|
||||
msgid "Quantity"
|
||||
msgstr "Quantitat"
|
||||
|
||||
#: pkg/quote.go:830 pkg/invoices.go:861
|
||||
msgctxt "input"
|
||||
msgid "Discount (%)"
|
||||
msgstr "Descompte (%)"
|
||||
|
||||
#: pkg/quote.go:884
|
||||
msgid "Quotation product ID must be a number greater than zero."
|
||||
msgstr "L’ID del producte de pressupost ha de ser un número major a zero."
|
||||
|
||||
#: pkg/quote.go:887 pkg/invoices.go:918
|
||||
msgid "Product ID must be a positive number or zero."
|
||||
msgstr "L’ID del producte ha de ser un número positiu o zero."
|
||||
|
||||
#: pkg/quote.go:893 pkg/invoices.go:924
|
||||
msgid "Quantity can not be empty."
|
||||
msgstr "No podeu deixar la quantitat en blanc."
|
||||
|
||||
#: pkg/quote.go:894 pkg/invoices.go:925
|
||||
msgid "Quantity must be a number greater than zero."
|
||||
msgstr "La quantitat ha de ser un número major a zero."
|
||||
|
||||
#: pkg/quote.go:896 pkg/invoices.go:927
|
||||
msgid "Discount can not be empty."
|
||||
msgstr "No podeu deixar el descompte en blanc."
|
||||
|
||||
#: pkg/quote.go:897 pkg/invoices.go:928
|
||||
msgid "Discount must be a percentage between 0 and 100."
|
||||
msgstr "El descompte ha de ser un percentatge entre 0 i 100."
|
||||
|
||||
#: pkg/profile.go:25
|
||||
msgctxt "language option"
|
||||
msgid "Automatic"
|
||||
|
@ -815,39 +1047,16 @@ msgstr "No podeu deixar l’import en blanc."
|
|||
msgid "Amount must be a number greater than zero."
|
||||
msgstr "L’import ha de ser un número major a zero."
|
||||
|
||||
#: pkg/expenses.go:340 pkg/invoices.go:162 pkg/invoices.go:584
|
||||
msgctxt "input"
|
||||
msgid "Customer"
|
||||
msgstr "Client"
|
||||
|
||||
#: pkg/expenses.go:341 pkg/invoices.go:163
|
||||
msgid "All customers"
|
||||
msgstr "Tots els clients"
|
||||
|
||||
#: pkg/expenses.go:346 pkg/invoices.go:174
|
||||
msgctxt "input"
|
||||
msgid "Invoice Number"
|
||||
msgstr "Número de factura"
|
||||
|
||||
#: pkg/expenses.go:351 pkg/invoices.go:179
|
||||
msgctxt "input"
|
||||
msgid "From Date"
|
||||
msgstr "A partir de la data"
|
||||
|
||||
#: pkg/expenses.go:356 pkg/invoices.go:184
|
||||
msgctxt "input"
|
||||
msgid "To Date"
|
||||
msgstr "Fins la data"
|
||||
|
||||
#: pkg/invoices.go:168 pkg/invoices.go:578
|
||||
msgctxt "input"
|
||||
msgid "Invoice Status"
|
||||
msgstr "Estat de la factura"
|
||||
|
||||
#: pkg/invoices.go:169
|
||||
msgid "All status"
|
||||
msgstr "Tots els estats"
|
||||
|
||||
#: pkg/invoices.go:426
|
||||
msgid "Select a customer to bill."
|
||||
msgstr "Escolliu un client a facturar."
|
||||
|
@ -856,75 +1065,18 @@ msgstr "Escolliu un client a facturar."
|
|||
msgid "invoices.zip"
|
||||
msgstr "factures.zip"
|
||||
|
||||
#: pkg/invoices.go:533 pkg/invoices.go:1090 pkg/invoices.go:1098
|
||||
msgid "Invalid action"
|
||||
msgstr "Acció invàlida."
|
||||
|
||||
#: pkg/invoices.go:596
|
||||
msgctxt "input"
|
||||
msgid "Notes"
|
||||
msgstr "Notes"
|
||||
|
||||
#: pkg/invoices.go:606
|
||||
msgctxt "input"
|
||||
msgid "Payment Method"
|
||||
msgstr "Mètode de pagament"
|
||||
|
||||
#: pkg/invoices.go:642
|
||||
msgid "Selected invoice status is not valid."
|
||||
msgstr "Heu seleccionat un estat de factura que no és vàlid."
|
||||
|
||||
#: pkg/invoices.go:643
|
||||
msgid "Selected customer is not valid."
|
||||
msgstr "Heu seleccionat un client que no és vàlid."
|
||||
|
||||
#: pkg/invoices.go:644
|
||||
msgid "Invoice date can not be empty."
|
||||
msgstr "No podeu deixar la data de la factura en blanc."
|
||||
|
||||
#: pkg/invoices.go:647
|
||||
msgid "Selected payment method is not valid."
|
||||
msgstr "Heu seleccionat un mètode de pagament que no és vàlid."
|
||||
|
||||
#: pkg/invoices.go:806 pkg/invoices.go:811
|
||||
msgctxt "input"
|
||||
msgid "Id"
|
||||
msgstr "Identificador"
|
||||
|
||||
#: pkg/invoices.go:844
|
||||
msgctxt "input"
|
||||
msgid "Quantity"
|
||||
msgstr "Quantitat"
|
||||
|
||||
#: pkg/invoices.go:853
|
||||
msgctxt "input"
|
||||
msgid "Discount (%)"
|
||||
msgstr "Descompte (%)"
|
||||
|
||||
#: pkg/invoices.go:907
|
||||
#: pkg/invoices.go:915
|
||||
msgid "Invoice product ID must be a number greater than zero."
|
||||
msgstr "L’ID del producte de factura ha de ser un número major a zero."
|
||||
|
||||
#: pkg/invoices.go:910
|
||||
msgid "Product ID must be a positive number or zero."
|
||||
msgstr "L’ID del producte ha de ser un número positiu o zero."
|
||||
|
||||
#: pkg/invoices.go:916
|
||||
msgid "Quantity can not be empty."
|
||||
msgstr "No podeu deixar la quantitat en blanc."
|
||||
|
||||
#: pkg/invoices.go:917
|
||||
msgid "Quantity must be a number greater than zero."
|
||||
msgstr "La quantitat ha de ser un número major a zero."
|
||||
|
||||
#: pkg/invoices.go:919
|
||||
msgid "Discount can not be empty."
|
||||
msgstr "No podeu deixar el descompte en blanc."
|
||||
|
||||
#: pkg/invoices.go:920
|
||||
msgid "Discount must be a percentage between 0 and 100."
|
||||
msgstr "El descompte ha de ser un percentatge entre 0 i 100."
|
||||
|
||||
#: pkg/contacts.go:238
|
||||
msgctxt "input"
|
||||
msgid "Business name"
|
||||
|
|
470
po/es.po
470
po/es.po
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-05-29 00:02+0200\n"
|
||||
"POT-Creation-Date: 2023-06-07 16:05+0200\n"
|
||||
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||
|
@ -25,12 +25,15 @@ msgstr "Añadir productos a la factura"
|
|||
|
||||
#: web/template/invoices/products.gohtml:9 web/template/invoices/new.gohtml:9
|
||||
#: web/template/invoices/index.gohtml:9 web/template/invoices/view.gohtml:9
|
||||
#: web/template/invoices/edit.gohtml:9 web/template/contacts/new.gohtml:9
|
||||
#: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10
|
||||
#: web/template/profile.gohtml:9 web/template/expenses/new.gohtml:10
|
||||
#: web/template/expenses/index.gohtml:10 web/template/expenses/edit.gohtml:10
|
||||
#: web/template/tax-details.gohtml:9 web/template/products/new.gohtml:9
|
||||
#: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10
|
||||
#: web/template/invoices/edit.gohtml:9 web/template/quotes/products.gohtml:9
|
||||
#: web/template/quotes/new.gohtml:9 web/template/quotes/index.gohtml:9
|
||||
#: web/template/quotes/view.gohtml:9 web/template/quotes/edit.gohtml:9
|
||||
#: web/template/contacts/new.gohtml:9 web/template/contacts/index.gohtml:9
|
||||
#: web/template/contacts/edit.gohtml:10 web/template/profile.gohtml:9
|
||||
#: web/template/expenses/new.gohtml:10 web/template/expenses/index.gohtml:10
|
||||
#: web/template/expenses/edit.gohtml:10 web/template/tax-details.gohtml:9
|
||||
#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:9
|
||||
#: web/template/products/edit.gohtml:10
|
||||
msgctxt "title"
|
||||
msgid "Home"
|
||||
msgstr "Inicio"
|
||||
|
@ -49,60 +52,70 @@ msgid "New Invoice"
|
|||
msgstr "Nueva factura"
|
||||
|
||||
#: web/template/invoices/products.gohtml:48
|
||||
#: web/template/quotes/products.gohtml:48
|
||||
msgctxt "product"
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
#: web/template/invoices/products.gohtml:49
|
||||
#: web/template/products/index.gohtml:40
|
||||
#: web/template/quotes/products.gohtml:49 web/template/products/index.gohtml:40
|
||||
msgctxt "title"
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: web/template/invoices/products.gohtml:50
|
||||
#: web/template/invoices/view.gohtml:62 web/template/products/index.gohtml:42
|
||||
#: web/template/invoices/view.gohtml:62 web/template/quotes/products.gohtml:50
|
||||
#: web/template/quotes/view.gohtml:62 web/template/products/index.gohtml:42
|
||||
msgctxt "title"
|
||||
msgid "Price"
|
||||
msgstr "Precio"
|
||||
|
||||
#: web/template/invoices/products.gohtml:64
|
||||
#: web/template/products/index.gohtml:82
|
||||
#: web/template/quotes/products.gohtml:64 web/template/products/index.gohtml:82
|
||||
msgid "No products added yet."
|
||||
msgstr "No hay productos."
|
||||
|
||||
#: web/template/invoices/products.gohtml:72 web/template/invoices/new.gohtml:83
|
||||
#: web/template/invoices/edit.gohtml:84
|
||||
#: web/template/invoices/edit.gohtml:84 web/template/quotes/products.gohtml:72
|
||||
#: web/template/quotes/new.gohtml:84 web/template/quotes/edit.gohtml:85
|
||||
msgctxt "action"
|
||||
msgid "Add products"
|
||||
msgstr "Añadir productos"
|
||||
|
||||
#: web/template/invoices/new.gohtml:27 web/template/invoices/edit.gohtml:27
|
||||
#: web/template/quotes/new.gohtml:27 web/template/quotes/edit.gohtml:27
|
||||
msgid "Product “%s” removed"
|
||||
msgstr "Se ha borrado el producto «%s»"
|
||||
|
||||
#: web/template/invoices/new.gohtml:31 web/template/invoices/edit.gohtml:31
|
||||
#: web/template/quotes/new.gohtml:31 web/template/quotes/edit.gohtml:31
|
||||
msgctxt "action"
|
||||
msgid "Undo"
|
||||
msgstr "Deshacer"
|
||||
|
||||
#: web/template/invoices/new.gohtml:60 web/template/invoices/view.gohtml:67
|
||||
#: web/template/invoices/edit.gohtml:61
|
||||
#: web/template/invoices/edit.gohtml:61 web/template/quotes/new.gohtml:61
|
||||
#: web/template/quotes/view.gohtml:67 web/template/quotes/edit.gohtml:62
|
||||
msgctxt "title"
|
||||
msgid "Subtotal"
|
||||
msgstr "Subtotal"
|
||||
|
||||
#: web/template/invoices/new.gohtml:70 web/template/invoices/view.gohtml:71
|
||||
#: web/template/invoices/view.gohtml:111 web/template/invoices/edit.gohtml:71
|
||||
#: web/template/quotes/new.gohtml:71 web/template/quotes/view.gohtml:71
|
||||
#: web/template/quotes/view.gohtml:111 web/template/quotes/edit.gohtml:72
|
||||
msgctxt "title"
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
||||
#: web/template/invoices/new.gohtml:87 web/template/invoices/edit.gohtml:88
|
||||
#: web/template/quotes/new.gohtml:88 web/template/quotes/edit.gohtml:89
|
||||
msgctxt "action"
|
||||
msgid "Update"
|
||||
msgstr "Actualizar"
|
||||
|
||||
#: web/template/invoices/new.gohtml:90 web/template/invoices/edit.gohtml:91
|
||||
#: web/template/quotes/new.gohtml:91 web/template/quotes/edit.gohtml:92
|
||||
#: web/template/contacts/new.gohtml:39 web/template/contacts/edit.gohtml:43
|
||||
#: web/template/expenses/new.gohtml:33 web/template/expenses/edit.gohtml:38
|
||||
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36
|
||||
|
@ -121,8 +134,8 @@ msgid "New invoice"
|
|||
msgstr "Nueva factura"
|
||||
|
||||
#: web/template/invoices/index.gohtml:43 web/template/dashboard.gohtml:23
|
||||
#: web/template/contacts/index.gohtml:34 web/template/expenses/index.gohtml:36
|
||||
#: web/template/products/index.gohtml:34
|
||||
#: web/template/quotes/index.gohtml:43 web/template/contacts/index.gohtml:34
|
||||
#: web/template/expenses/index.gohtml:36 web/template/products/index.gohtml:34
|
||||
msgctxt "action"
|
||||
msgid "Filter"
|
||||
msgstr "Filtrar"
|
||||
|
@ -133,6 +146,7 @@ msgid "All"
|
|||
msgstr "Todas"
|
||||
|
||||
#: web/template/invoices/index.gohtml:50 web/template/invoices/view.gohtml:34
|
||||
#: web/template/quotes/index.gohtml:50 web/template/quotes/view.gohtml:34
|
||||
msgctxt "title"
|
||||
msgid "Date"
|
||||
msgstr "Fecha"
|
||||
|
@ -140,36 +154,41 @@ msgstr "Fecha"
|
|||
#: web/template/invoices/index.gohtml:51
|
||||
msgctxt "title"
|
||||
msgid "Invoice Num."
|
||||
msgstr "Nº factura"
|
||||
msgstr "N.º factura"
|
||||
|
||||
#: web/template/invoices/index.gohtml:52 web/template/contacts/index.gohtml:40
|
||||
#: web/template/invoices/index.gohtml:52 web/template/quotes/index.gohtml:52
|
||||
#: web/template/contacts/index.gohtml:40
|
||||
msgctxt "title"
|
||||
msgid "Customer"
|
||||
msgstr "Cliente"
|
||||
|
||||
#: web/template/invoices/index.gohtml:53
|
||||
#: web/template/invoices/index.gohtml:53 web/template/quotes/index.gohtml:53
|
||||
msgctxt "title"
|
||||
msgid "Status"
|
||||
msgstr "Estado"
|
||||
|
||||
#: web/template/invoices/index.gohtml:54 web/template/contacts/index.gohtml:43
|
||||
#: web/template/expenses/index.gohtml:46 web/template/products/index.gohtml:41
|
||||
#: web/template/invoices/index.gohtml:54 web/template/quotes/index.gohtml:54
|
||||
#: web/template/contacts/index.gohtml:43 web/template/expenses/index.gohtml:46
|
||||
#: web/template/products/index.gohtml:41
|
||||
msgctxt "title"
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetes"
|
||||
|
||||
#: web/template/invoices/index.gohtml:55 web/template/expenses/index.gohtml:47
|
||||
#: web/template/invoices/index.gohtml:55 web/template/quotes/index.gohtml:55
|
||||
#: web/template/expenses/index.gohtml:47
|
||||
msgctxt "title"
|
||||
msgid "Amount"
|
||||
msgstr "Importe"
|
||||
|
||||
#: web/template/invoices/index.gohtml:56 web/template/expenses/index.gohtml:48
|
||||
#: web/template/invoices/index.gohtml:56 web/template/quotes/index.gohtml:56
|
||||
#: web/template/expenses/index.gohtml:48
|
||||
msgctxt "title"
|
||||
msgid "Download"
|
||||
msgstr "Descargar"
|
||||
|
||||
#: web/template/invoices/index.gohtml:57 web/template/contacts/index.gohtml:44
|
||||
#: web/template/expenses/index.gohtml:49 web/template/products/index.gohtml:43
|
||||
#: web/template/invoices/index.gohtml:57 web/template/quotes/index.gohtml:57
|
||||
#: web/template/contacts/index.gohtml:44 web/template/expenses/index.gohtml:49
|
||||
#: web/template/products/index.gohtml:43
|
||||
msgctxt "title"
|
||||
msgid "Actions"
|
||||
msgstr "Acciones"
|
||||
|
@ -180,6 +199,7 @@ msgid "Select invoice %v"
|
|||
msgstr "Seleccionar factura %v"
|
||||
|
||||
#: web/template/invoices/index.gohtml:119 web/template/invoices/view.gohtml:19
|
||||
#: web/template/quotes/index.gohtml:119 web/template/quotes/view.gohtml:19
|
||||
#: web/template/contacts/index.gohtml:74 web/template/expenses/index.gohtml:88
|
||||
#: web/template/products/index.gohtml:72
|
||||
msgctxt "action"
|
||||
|
@ -187,6 +207,7 @@ msgid "Edit"
|
|||
msgstr "Editar"
|
||||
|
||||
#: web/template/invoices/index.gohtml:127 web/template/invoices/view.gohtml:16
|
||||
#: web/template/quotes/index.gohtml:127 web/template/quotes/view.gohtml:16
|
||||
msgctxt "action"
|
||||
msgid "Duplicate"
|
||||
msgstr "Duplicar"
|
||||
|
@ -205,22 +226,22 @@ msgctxt "action"
|
|||
msgid "Download invoice"
|
||||
msgstr "Descargar factura"
|
||||
|
||||
#: web/template/invoices/view.gohtml:61
|
||||
#: web/template/invoices/view.gohtml:61 web/template/quotes/view.gohtml:61
|
||||
msgctxt "title"
|
||||
msgid "Concept"
|
||||
msgstr "Concepto"
|
||||
|
||||
#: web/template/invoices/view.gohtml:64
|
||||
#: web/template/invoices/view.gohtml:64 web/template/quotes/view.gohtml:64
|
||||
msgctxt "title"
|
||||
msgid "Discount"
|
||||
msgstr "Descuento"
|
||||
|
||||
#: web/template/invoices/view.gohtml:66
|
||||
#: web/template/invoices/view.gohtml:66 web/template/quotes/view.gohtml:66
|
||||
msgctxt "title"
|
||||
msgid "Units"
|
||||
msgstr "Unidades"
|
||||
|
||||
#: web/template/invoices/view.gohtml:101
|
||||
#: web/template/invoices/view.gohtml:101 web/template/quotes/view.gohtml:101
|
||||
msgctxt "title"
|
||||
msgid "Tax Base"
|
||||
msgstr "Base imponible"
|
||||
|
@ -235,7 +256,7 @@ msgctxt "input"
|
|||
msgid "(Max. %s)"
|
||||
msgstr "(Máx. %s)"
|
||||
|
||||
#: web/template/form.gohtml:171
|
||||
#: web/template/form.gohtml:194
|
||||
msgctxt "action"
|
||||
msgid "Filters"
|
||||
msgstr "Filtrar"
|
||||
|
@ -275,6 +296,68 @@ msgctxt "term"
|
|||
msgid "Net Income"
|
||||
msgstr "Ingresos netos"
|
||||
|
||||
#: web/template/quotes/products.gohtml:2 web/template/quotes/products.gohtml:23
|
||||
msgctxt "title"
|
||||
msgid "Add Products to Quotation"
|
||||
msgstr "Añadir productos al presupuesto"
|
||||
|
||||
#: web/template/quotes/products.gohtml:10 web/template/quotes/new.gohtml:10
|
||||
#: web/template/quotes/index.gohtml:2 web/template/quotes/index.gohtml:10
|
||||
#: web/template/quotes/view.gohtml:10 web/template/quotes/edit.gohtml:10
|
||||
msgctxt "title"
|
||||
msgid "Quotations"
|
||||
msgstr "Presupuestos"
|
||||
|
||||
#: web/template/quotes/products.gohtml:12 web/template/quotes/new.gohtml:2
|
||||
#: web/template/quotes/new.gohtml:11 web/template/quotes/new.gohtml:19
|
||||
msgctxt "title"
|
||||
msgid "New Quotation"
|
||||
msgstr "Nuevo presupuesto"
|
||||
|
||||
#: web/template/quotes/index.gohtml:19
|
||||
msgctxt "action"
|
||||
msgid "Download quotations"
|
||||
msgstr "Descargar presupuestos"
|
||||
|
||||
#: web/template/quotes/index.gohtml:21
|
||||
msgctxt "action"
|
||||
msgid "New quotation"
|
||||
msgstr "Nuevo presupuesto"
|
||||
|
||||
#: web/template/quotes/index.gohtml:49
|
||||
msgctxt "quote"
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
#: web/template/quotes/index.gohtml:51
|
||||
msgctxt "title"
|
||||
msgid "Quotation Num."
|
||||
msgstr "N.º de presupuesto"
|
||||
|
||||
#: web/template/quotes/index.gohtml:64
|
||||
msgctxt "action"
|
||||
msgid "Select quotation %v"
|
||||
msgstr "Seleccionar presupuesto %v"
|
||||
|
||||
#: web/template/quotes/index.gohtml:137
|
||||
msgid "No quotations added yet."
|
||||
msgstr "No hay presupuestos."
|
||||
|
||||
#: web/template/quotes/view.gohtml:2 web/template/quotes/view.gohtml:33
|
||||
msgctxt "title"
|
||||
msgid "Quotation %s"
|
||||
msgstr "Estado del presupuesto"
|
||||
|
||||
#: web/template/quotes/view.gohtml:22
|
||||
msgctxt "action"
|
||||
msgid "Download quotation"
|
||||
msgstr "Descargar presupuesto"
|
||||
|
||||
#: web/template/quotes/edit.gohtml:2 web/template/quotes/edit.gohtml:19
|
||||
msgctxt "title"
|
||||
msgid "Edit Quotation “%s”"
|
||||
msgstr "Edición del presupuesto «%s»"
|
||||
|
||||
#: web/template/app.gohtml:23
|
||||
msgctxt "menu"
|
||||
msgid "Account"
|
||||
|
@ -297,20 +380,25 @@ msgstr "Panel"
|
|||
|
||||
#: web/template/app.gohtml:47
|
||||
msgctxt "nav"
|
||||
msgid "Quotations"
|
||||
msgstr "Presupuestos"
|
||||
|
||||
#: web/template/app.gohtml:48
|
||||
msgctxt "nav"
|
||||
msgid "Invoices"
|
||||
msgstr "Facturas"
|
||||
|
||||
#: web/template/app.gohtml:48
|
||||
#: web/template/app.gohtml:49
|
||||
msgctxt "nav"
|
||||
msgid "Expenses"
|
||||
msgstr "Gastos"
|
||||
|
||||
#: web/template/app.gohtml:49
|
||||
#: web/template/app.gohtml:50
|
||||
msgctxt "nav"
|
||||
msgid "Products"
|
||||
msgstr "Productos"
|
||||
|
||||
#: web/template/app.gohtml:50
|
||||
#: web/template/app.gohtml:51
|
||||
msgctxt "nav"
|
||||
msgid "Contacts"
|
||||
msgstr "Contactos"
|
||||
|
@ -382,7 +470,7 @@ msgctxt "title"
|
|||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: web/template/profile.gohtml:39 web/template/tax-details.gohtml:172
|
||||
#: web/template/profile.gohtml:39 web/template/tax-details.gohtml:173
|
||||
msgctxt "action"
|
||||
msgid "Save changes"
|
||||
msgstr "Guardar cambios"
|
||||
|
@ -449,54 +537,54 @@ msgctxt "title"
|
|||
msgid "Invoicing"
|
||||
msgstr "Facturación"
|
||||
|
||||
#: web/template/tax-details.gohtml:53
|
||||
#: web/template/tax-details.gohtml:54
|
||||
msgid "Are you sure?"
|
||||
msgstr "¿Estáis seguro?"
|
||||
|
||||
#: web/template/tax-details.gohtml:59
|
||||
#: web/template/tax-details.gohtml:60
|
||||
msgctxt "title"
|
||||
msgid "Tax Name"
|
||||
msgstr "Nombre impuesto"
|
||||
|
||||
#: web/template/tax-details.gohtml:60
|
||||
#: web/template/tax-details.gohtml:61
|
||||
msgctxt "title"
|
||||
msgid "Rate (%)"
|
||||
msgstr "Porcentaje"
|
||||
|
||||
#: web/template/tax-details.gohtml:61
|
||||
#: web/template/tax-details.gohtml:62
|
||||
msgctxt "title"
|
||||
msgid "Class"
|
||||
msgstr "Clase"
|
||||
|
||||
#: web/template/tax-details.gohtml:85
|
||||
#: web/template/tax-details.gohtml:86
|
||||
msgid "No taxes added yet."
|
||||
msgstr "No hay impuestos."
|
||||
|
||||
#: web/template/tax-details.gohtml:91 web/template/tax-details.gohtml:152
|
||||
#: web/template/tax-details.gohtml:92 web/template/tax-details.gohtml:153
|
||||
msgctxt "title"
|
||||
msgid "New Line"
|
||||
msgstr "Nueva línea"
|
||||
|
||||
#: web/template/tax-details.gohtml:105
|
||||
#: web/template/tax-details.gohtml:106
|
||||
msgctxt "action"
|
||||
msgid "Add new tax"
|
||||
msgstr "Añadir nuevo impuesto"
|
||||
|
||||
#: web/template/tax-details.gohtml:121
|
||||
#: web/template/tax-details.gohtml:122
|
||||
msgctxt "title"
|
||||
msgid "Payment Method"
|
||||
msgstr "Método de pago"
|
||||
|
||||
#: web/template/tax-details.gohtml:122
|
||||
#: web/template/tax-details.gohtml:123
|
||||
msgctxt "title"
|
||||
msgid "Instructions"
|
||||
msgstr "Instrucciones"
|
||||
|
||||
#: web/template/tax-details.gohtml:146
|
||||
#: web/template/tax-details.gohtml:147
|
||||
msgid "No payment methods added yet."
|
||||
msgstr "No hay métodos de pago."
|
||||
|
||||
#: web/template/tax-details.gohtml:164
|
||||
#: web/template/tax-details.gohtml:165
|
||||
msgctxt "action"
|
||||
msgid "Add new payment method"
|
||||
msgstr "Añadir nuevo método de pago"
|
||||
|
@ -553,26 +641,27 @@ msgstr "No podéis dejar la contraseña en blanco."
|
|||
msgid "Invalid user or password."
|
||||
msgstr "Nombre de usuario o contraseña inválido."
|
||||
|
||||
#: pkg/products.go:164 pkg/products.go:263 pkg/invoices.go:816
|
||||
#: pkg/products.go:164 pkg/products.go:263 pkg/quote.go:793 pkg/invoices.go:824
|
||||
#: pkg/contacts.go:135
|
||||
msgctxt "input"
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: pkg/products.go:169 pkg/products.go:290 pkg/expenses.go:202
|
||||
#: pkg/expenses.go:361 pkg/invoices.go:189 pkg/invoices.go:601
|
||||
#: pkg/invoices.go:1115 pkg/contacts.go:140 pkg/contacts.go:325
|
||||
#: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:188 pkg/quote.go:606
|
||||
#: pkg/expenses.go:202 pkg/expenses.go:361 pkg/invoices.go:189
|
||||
#: pkg/invoices.go:601 pkg/invoices.go:1123 pkg/contacts.go:140
|
||||
#: pkg/contacts.go:325
|
||||
msgctxt "input"
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetes"
|
||||
|
||||
#: pkg/products.go:173 pkg/expenses.go:365 pkg/invoices.go:193
|
||||
#: pkg/products.go:173 pkg/quote.go:192 pkg/expenses.go:365 pkg/invoices.go:193
|
||||
#: pkg/contacts.go:144
|
||||
msgctxt "input"
|
||||
msgid "Tags Condition"
|
||||
msgstr "Condición de las etiquetas"
|
||||
|
||||
#: pkg/products.go:177 pkg/expenses.go:369 pkg/invoices.go:197
|
||||
#: pkg/products.go:177 pkg/quote.go:196 pkg/expenses.go:369 pkg/invoices.go:197
|
||||
#: pkg/contacts.go:148
|
||||
msgctxt "tag condition"
|
||||
msgid "All"
|
||||
|
@ -583,7 +672,7 @@ msgstr "Todas"
|
|||
msgid "Invoices must have all the specified labels."
|
||||
msgstr "Las facturas deben tener todas las etiquetas."
|
||||
|
||||
#: pkg/products.go:182 pkg/expenses.go:374 pkg/invoices.go:202
|
||||
#: pkg/products.go:182 pkg/quote.go:201 pkg/expenses.go:374 pkg/invoices.go:202
|
||||
#: pkg/contacts.go:153
|
||||
msgctxt "tag condition"
|
||||
msgid "Any"
|
||||
|
@ -592,121 +681,264 @@ msgstr "Cualquiera"
|
|||
#: pkg/products.go:183 pkg/expenses.go:375 pkg/invoices.go:203
|
||||
#: pkg/contacts.go:154
|
||||
msgid "Invoices must have at least one of the specified labels."
|
||||
msgstr "Las facturas debent tener como mínimo una de las etiquetas."
|
||||
msgstr "Las facturas deben tener como mínimo una de las etiquetas."
|
||||
|
||||
#: pkg/products.go:269 pkg/invoices.go:830
|
||||
#: pkg/products.go:269 pkg/quote.go:807 pkg/invoices.go:838
|
||||
msgctxt "input"
|
||||
msgid "Description"
|
||||
msgstr "Descripción"
|
||||
|
||||
#: pkg/products.go:274 pkg/invoices.go:834
|
||||
#: pkg/products.go:274 pkg/quote.go:811 pkg/invoices.go:842
|
||||
msgctxt "input"
|
||||
msgid "Price"
|
||||
msgstr "Precio"
|
||||
|
||||
#: pkg/products.go:284 pkg/expenses.go:181 pkg/invoices.go:863
|
||||
#: pkg/products.go:284 pkg/quote.go:840 pkg/expenses.go:181 pkg/invoices.go:871
|
||||
msgctxt "input"
|
||||
msgid "Taxes"
|
||||
msgstr "Impuestos"
|
||||
|
||||
#: pkg/products.go:309 pkg/profile.go:92 pkg/invoices.go:912
|
||||
#: pkg/products.go:309 pkg/quote.go:889 pkg/profile.go:92 pkg/invoices.go:920
|
||||
msgid "Name can not be empty."
|
||||
msgstr "No podéis dejar el nombre en blanco."
|
||||
|
||||
#: pkg/products.go:310 pkg/invoices.go:913
|
||||
#: pkg/products.go:310 pkg/quote.go:890 pkg/invoices.go:921
|
||||
msgid "Price can not be empty."
|
||||
msgstr "No podéis dejar el precio en blanco."
|
||||
|
||||
#: pkg/products.go:311 pkg/invoices.go:914
|
||||
#: pkg/products.go:311 pkg/quote.go:891 pkg/invoices.go:922
|
||||
msgid "Price must be a number greater than zero."
|
||||
msgstr "El precio tiene que ser un número mayor a cero."
|
||||
|
||||
#: pkg/products.go:313 pkg/expenses.go:227 pkg/expenses.go:232
|
||||
#: pkg/invoices.go:922
|
||||
#: pkg/products.go:313 pkg/quote.go:899 pkg/expenses.go:227 pkg/expenses.go:232
|
||||
#: pkg/invoices.go:930
|
||||
msgid "Selected tax is not valid."
|
||||
msgstr "Habéis escogido un impuesto que no es válido."
|
||||
|
||||
#: pkg/products.go:314 pkg/expenses.go:228 pkg/expenses.go:233
|
||||
#: pkg/invoices.go:923
|
||||
#: pkg/products.go:314 pkg/quote.go:900 pkg/expenses.go:228 pkg/expenses.go:233
|
||||
#: pkg/invoices.go:931
|
||||
msgid "You can only select a tax of each class."
|
||||
msgstr "Solo podéis escoger un impuesto de cada clase."
|
||||
|
||||
#: pkg/company.go:98
|
||||
#: pkg/company.go:100
|
||||
msgctxt "input"
|
||||
msgid "Currency"
|
||||
msgstr "Moneda"
|
||||
|
||||
#: pkg/company.go:105
|
||||
#: pkg/company.go:107
|
||||
msgctxt "input"
|
||||
msgid "Invoice number format"
|
||||
msgstr "Formato del número de factura"
|
||||
|
||||
#: pkg/company.go:111
|
||||
#: pkg/company.go:113
|
||||
msgctxt "input"
|
||||
msgid "Next invoice number"
|
||||
msgstr "Número de presupuesto"
|
||||
|
||||
#: pkg/company.go:122
|
||||
msgctxt "input"
|
||||
msgid "Legal disclaimer"
|
||||
msgstr "Nota legal"
|
||||
|
||||
#: pkg/company.go:129
|
||||
#: pkg/company.go:141
|
||||
msgid "Selected currency is not valid."
|
||||
msgstr "Habéis escogido una moneda que no es válida."
|
||||
|
||||
#: pkg/company.go:130
|
||||
#: pkg/company.go:142
|
||||
msgid "Invoice number format can not be empty."
|
||||
msgstr "No podéis dejar el formato del número de factura en blanco."
|
||||
|
||||
#: pkg/company.go:297
|
||||
#: pkg/company.go:143
|
||||
msgid "Next invoice number must be a number greater than zero."
|
||||
msgstr "El siguiente número de factura tiene que ser un número mayor a cero."
|
||||
|
||||
#: pkg/company.go:350
|
||||
msgctxt "input"
|
||||
msgid "Tax name"
|
||||
msgstr "Nombre impuesto"
|
||||
|
||||
#: pkg/company.go:303
|
||||
#: pkg/company.go:356
|
||||
msgctxt "input"
|
||||
msgid "Tax Class"
|
||||
msgstr "Clase de impuesto"
|
||||
|
||||
#: pkg/company.go:306
|
||||
#: pkg/company.go:359
|
||||
msgid "Select a tax class"
|
||||
msgstr "Escoged una clase de impuesto"
|
||||
|
||||
#: pkg/company.go:310
|
||||
#: pkg/company.go:363
|
||||
msgctxt "input"
|
||||
msgid "Rate (%)"
|
||||
msgstr "Porcentaje"
|
||||
|
||||
#: pkg/company.go:333
|
||||
#: pkg/company.go:386
|
||||
msgid "Tax name can not be empty."
|
||||
msgstr "No podéis dejar el nombre del impuesto en blanco."
|
||||
|
||||
#: pkg/company.go:334
|
||||
#: pkg/company.go:387
|
||||
msgid "Selected tax class is not valid."
|
||||
msgstr "Habéis escogido una clase impuesto que no es válida."
|
||||
|
||||
#: pkg/company.go:335
|
||||
#: pkg/company.go:388
|
||||
msgid "Tax rate can not be empty."
|
||||
msgstr "No podéis dejar el porcentaje en blanco."
|
||||
|
||||
#: pkg/company.go:336
|
||||
#: pkg/company.go:389
|
||||
msgid "Tax rate must be an integer between -99 and 99."
|
||||
msgstr "El porcentaje tiene que estar entre -99 y 99."
|
||||
|
||||
#: pkg/company.go:399
|
||||
#: pkg/company.go:452
|
||||
msgctxt "input"
|
||||
msgid "Payment method name"
|
||||
msgstr "Nombre del método de pago"
|
||||
|
||||
#: pkg/company.go:405
|
||||
#: pkg/company.go:458
|
||||
msgctxt "input"
|
||||
msgid "Instructions"
|
||||
msgstr "Instrucciones"
|
||||
|
||||
#: pkg/company.go:423
|
||||
#: pkg/company.go:476
|
||||
msgid "Payment method name can not be empty."
|
||||
msgstr "No podéis dejar el nombre del método de pago en blanco."
|
||||
|
||||
#: pkg/company.go:424
|
||||
#: pkg/company.go:477
|
||||
msgid "Payment instructions can not be empty."
|
||||
msgstr "No podéis dejar las instrucciones de pago en blanco."
|
||||
|
||||
#: pkg/quote.go:161 pkg/quote.go:585 pkg/expenses.go:340 pkg/invoices.go:162
|
||||
#: pkg/invoices.go:584
|
||||
msgctxt "input"
|
||||
msgid "Customer"
|
||||
msgstr "Cliente"
|
||||
|
||||
#: pkg/quote.go:162 pkg/expenses.go:341 pkg/invoices.go:163
|
||||
msgid "All customers"
|
||||
msgstr "Todos los clientes"
|
||||
|
||||
#: pkg/quote.go:167 pkg/quote.go:579
|
||||
msgctxt "input"
|
||||
msgid "Quotation Status"
|
||||
msgstr "Estado del presupuesto"
|
||||
|
||||
#: pkg/quote.go:168 pkg/invoices.go:169
|
||||
msgid "All status"
|
||||
msgstr "Todos los estados"
|
||||
|
||||
#: pkg/quote.go:173
|
||||
msgctxt "input"
|
||||
msgid "Quotation Number"
|
||||
msgstr "Número de presupuesto"
|
||||
|
||||
#: pkg/quote.go:178 pkg/expenses.go:351 pkg/invoices.go:179
|
||||
msgctxt "input"
|
||||
msgid "From Date"
|
||||
msgstr "A partir de la fecha"
|
||||
|
||||
#: pkg/quote.go:183 pkg/expenses.go:356 pkg/invoices.go:184
|
||||
msgctxt "input"
|
||||
msgid "To Date"
|
||||
msgstr "Hasta la fecha"
|
||||
|
||||
#: pkg/quote.go:197
|
||||
msgid "Quotations must have all the specified labels."
|
||||
msgstr "Los presupuestos deben tener todas las etiquetas."
|
||||
|
||||
#: pkg/quote.go:202
|
||||
msgid "Quotations must have at least one of the specified labels."
|
||||
msgstr "Los presupuestos deben tener como mínimo una de las etiquetas."
|
||||
|
||||
#: pkg/quote.go:451
|
||||
msgid "Select a customer to quote."
|
||||
msgstr "Escoged un cliente a presupuestar."
|
||||
|
||||
#: pkg/quote.go:527
|
||||
msgid "quotations.zip"
|
||||
msgstr "presupuestos.zip"
|
||||
|
||||
#: pkg/quote.go:533 pkg/quote.go:1055 pkg/quote.go:1063 pkg/invoices.go:533
|
||||
#: pkg/invoices.go:1098 pkg/invoices.go:1106
|
||||
msgid "Invalid action"
|
||||
msgstr "Acción inválida."
|
||||
|
||||
#: pkg/quote.go:590
|
||||
msgctxt "input"
|
||||
msgid "Quotation Date"
|
||||
msgstr "Fecha del presupuesto"
|
||||
|
||||
#: pkg/quote.go:596
|
||||
msgctxt "input"
|
||||
msgid "Terms and conditions"
|
||||
msgstr "Condiciones de aceptación"
|
||||
|
||||
#: pkg/quote.go:601 pkg/invoices.go:596
|
||||
msgctxt "input"
|
||||
msgid "Notes"
|
||||
msgstr "Notas"
|
||||
|
||||
#: pkg/quote.go:610 pkg/invoices.go:606
|
||||
msgctxt "input"
|
||||
msgid "Payment Method"
|
||||
msgstr "Método de pago"
|
||||
|
||||
#: pkg/quote.go:646
|
||||
msgid "Selected quotation status is not valid."
|
||||
msgstr "Habéis escogido un estado de presupuesto que no es válido."
|
||||
|
||||
#: pkg/quote.go:647 pkg/invoices.go:643
|
||||
msgid "Selected customer is not valid."
|
||||
msgstr "Habéis escogido un cliente que no es válido."
|
||||
|
||||
#: pkg/quote.go:648
|
||||
msgid "Quotation date can not be empty."
|
||||
msgstr "No podéis dejar la fecha del presupuesto en blanco."
|
||||
|
||||
#: pkg/quote.go:649
|
||||
msgid "Quotation date must be a valid date."
|
||||
msgstr "La fecha de presupuesto debe ser válida."
|
||||
|
||||
#: pkg/quote.go:651 pkg/invoices.go:647
|
||||
msgid "Selected payment method is not valid."
|
||||
msgstr "Habéis escogido un método de pago que no es válido."
|
||||
|
||||
#: pkg/quote.go:783 pkg/quote.go:788 pkg/invoices.go:814 pkg/invoices.go:819
|
||||
msgctxt "input"
|
||||
msgid "Id"
|
||||
msgstr "Identificador"
|
||||
|
||||
#: pkg/quote.go:821 pkg/invoices.go:852
|
||||
msgctxt "input"
|
||||
msgid "Quantity"
|
||||
msgstr "Cantidad"
|
||||
|
||||
#: pkg/quote.go:830 pkg/invoices.go:861
|
||||
msgctxt "input"
|
||||
msgid "Discount (%)"
|
||||
msgstr "Descuento (%)"
|
||||
|
||||
#: pkg/quote.go:884
|
||||
msgid "Quotation product ID must be a number greater than zero."
|
||||
msgstr "El ID de producto de presupuesto tiene que ser un número mayor a cero."
|
||||
|
||||
#: pkg/quote.go:887 pkg/invoices.go:918
|
||||
msgid "Product ID must be a positive number or zero."
|
||||
msgstr "El ID de producto tiene que ser un número positivo o cero."
|
||||
|
||||
#: pkg/quote.go:893 pkg/invoices.go:924
|
||||
msgid "Quantity can not be empty."
|
||||
msgstr "No podéis dejar la cantidad en blanco."
|
||||
|
||||
#: pkg/quote.go:894 pkg/invoices.go:925
|
||||
msgid "Quantity must be a number greater than zero."
|
||||
msgstr "La cantidad tiene que ser un número mayor a cero."
|
||||
|
||||
#: pkg/quote.go:896 pkg/invoices.go:927
|
||||
msgid "Discount can not be empty."
|
||||
msgstr "No podéis dejar el descuento en blanco."
|
||||
|
||||
#: pkg/quote.go:897 pkg/invoices.go:928
|
||||
msgid "Discount must be a percentage between 0 and 100."
|
||||
msgstr "El descuento tiene que ser un porcentaje entre 0 y 100."
|
||||
|
||||
#: pkg/profile.go:25
|
||||
msgctxt "language option"
|
||||
msgid "Automatic"
|
||||
|
@ -815,39 +1047,16 @@ msgstr "No podéis dejar el importe en blanco."
|
|||
msgid "Amount must be a number greater than zero."
|
||||
msgstr "El importe tiene que ser un número mayor a cero."
|
||||
|
||||
#: pkg/expenses.go:340 pkg/invoices.go:162 pkg/invoices.go:584
|
||||
msgctxt "input"
|
||||
msgid "Customer"
|
||||
msgstr "Cliente"
|
||||
|
||||
#: pkg/expenses.go:341 pkg/invoices.go:163
|
||||
msgid "All customers"
|
||||
msgstr "Todos los clientes"
|
||||
|
||||
#: pkg/expenses.go:346 pkg/invoices.go:174
|
||||
msgctxt "input"
|
||||
msgid "Invoice Number"
|
||||
msgstr "Número de factura"
|
||||
|
||||
#: pkg/expenses.go:351 pkg/invoices.go:179
|
||||
msgctxt "input"
|
||||
msgid "From Date"
|
||||
msgstr "A partir de la fecha"
|
||||
|
||||
#: pkg/expenses.go:356 pkg/invoices.go:184
|
||||
msgctxt "input"
|
||||
msgid "To Date"
|
||||
msgstr "Hasta la fecha"
|
||||
|
||||
#: pkg/invoices.go:168 pkg/invoices.go:578
|
||||
msgctxt "input"
|
||||
msgid "Invoice Status"
|
||||
msgstr "Estado de la factura"
|
||||
|
||||
#: pkg/invoices.go:169
|
||||
msgid "All status"
|
||||
msgstr "Todos los estados"
|
||||
|
||||
#: pkg/invoices.go:426
|
||||
msgid "Select a customer to bill."
|
||||
msgstr "Escoged un cliente a facturar."
|
||||
|
@ -856,75 +1065,18 @@ msgstr "Escoged un cliente a facturar."
|
|||
msgid "invoices.zip"
|
||||
msgstr "facturas.zip"
|
||||
|
||||
#: pkg/invoices.go:533 pkg/invoices.go:1090 pkg/invoices.go:1098
|
||||
msgid "Invalid action"
|
||||
msgstr "Acción inválida."
|
||||
|
||||
#: pkg/invoices.go:596
|
||||
msgctxt "input"
|
||||
msgid "Notes"
|
||||
msgstr "Notas"
|
||||
|
||||
#: pkg/invoices.go:606
|
||||
msgctxt "input"
|
||||
msgid "Payment Method"
|
||||
msgstr "Método de pago"
|
||||
|
||||
#: pkg/invoices.go:642
|
||||
msgid "Selected invoice status is not valid."
|
||||
msgstr "Habéis escogido un estado de factura que no es válido."
|
||||
|
||||
#: pkg/invoices.go:643
|
||||
msgid "Selected customer is not valid."
|
||||
msgstr "Habéis escogido un cliente que no es válido."
|
||||
|
||||
#: pkg/invoices.go:644
|
||||
msgid "Invoice date can not be empty."
|
||||
msgstr "No podéis dejar la fecha de la factura en blanco."
|
||||
|
||||
#: pkg/invoices.go:647
|
||||
msgid "Selected payment method is not valid."
|
||||
msgstr "Habéis escogido un método de pago que no es válido."
|
||||
|
||||
#: pkg/invoices.go:806 pkg/invoices.go:811
|
||||
msgctxt "input"
|
||||
msgid "Id"
|
||||
msgstr "Identificador"
|
||||
|
||||
#: pkg/invoices.go:844
|
||||
msgctxt "input"
|
||||
msgid "Quantity"
|
||||
msgstr "Cantidad"
|
||||
|
||||
#: pkg/invoices.go:853
|
||||
msgctxt "input"
|
||||
msgid "Discount (%)"
|
||||
msgstr "Descuento (%)"
|
||||
|
||||
#: pkg/invoices.go:907
|
||||
#: pkg/invoices.go:915
|
||||
msgid "Invoice product ID must be a number greater than zero."
|
||||
msgstr "El ID de producto de factura tiene que ser un número mayor a cero."
|
||||
|
||||
#: pkg/invoices.go:910
|
||||
msgid "Product ID must be a positive number or zero."
|
||||
msgstr "El ID de producto tiene que ser un número positivo o cero."
|
||||
|
||||
#: pkg/invoices.go:916
|
||||
msgid "Quantity can not be empty."
|
||||
msgstr "No podéis dejar la cantidad en blanco."
|
||||
|
||||
#: pkg/invoices.go:917
|
||||
msgid "Quantity must be a number greater than zero."
|
||||
msgstr "La cantidad tiene que ser un número mayor a cero."
|
||||
|
||||
#: pkg/invoices.go:919
|
||||
msgid "Discount can not be empty."
|
||||
msgstr "No podéis dejar el descuento en blanco."
|
||||
|
||||
#: pkg/invoices.go:920
|
||||
msgid "Discount must be a percentage between 0 and 100."
|
||||
msgstr "El descuento tiene que ser un porcentaje entre 0 y 100."
|
||||
|
||||
#: pkg/contacts.go:238
|
||||
msgctxt "input"
|
||||
msgid "Business name"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:add_quote from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.add_quote(integer, date, integer, text, text, integer, numerus.tag_name[], numerus.new_quote_product[]);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:compute_new_quote_amount from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.compute_new_quote_amount(integer, numerus.new_quote_product[]);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:edit_quote from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.edit_quote(uuid, text, integer, text, text, integer, numerus.tag_name[], numerus.edited_quote_product[]);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:edited_quote_product from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop type if exists numerus.edited_quote_product;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:new_quote_amount from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop type if exists numerus.new_quote_amount;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:new_quote_product from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop type if exists numerus.new_quote_product;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:next_quote_number from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.next_quote_number(integer, date);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:quote_amount from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop view if exists numerus.quote_amount;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:quote_number_counter from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists numerus.quote_number_counter;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:quote_product_amount from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop view if exists numerus.quote_product_amount;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:quote_tax_amount from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop view if exists numerus.quote_tax_amount;
|
||||
|
||||
commit;
|
13
sqitch.plan
13
sqitch.plan
|
@ -67,7 +67,7 @@ invoice_amount [schema_numerus invoice_product invoice_product_amount] 2023-02-2
|
|||
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
|
||||
edited_invoice_product [schema_numerus discount_rate] 2023-03-11T19:22:24Z jordi fita mas <jordi@tandem.blog> # Add typo for passing products to edited invoices
|
||||
edit_invoice [schema_numerus invoice currency parse_price edited_invoice_product tax invoice_product invoice_product_tax tag_name] 2023-03-11T18:30:50Z jordi fita mas <jordi@tandem.blog> # Add function to edit invoices
|
||||
edit_invoice [schema_numerus invoice currency parse_price edited_invoice_product tax invoice_product invoice_product_product invoice_product_tax tag_name] 2023-03-11T18:30:50Z jordi fita mas <jordi@tandem.blog> # Add function to edit invoices
|
||||
add_contact [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri country_code tag_name contact] 2023-03-25T22:32:37Z jordi fita mas <jordi@tandem.blog> # Add function to create new contacts
|
||||
edit_contact [schema_numerus email extension_uri country_code tag_name contact extension_vat extension_pg_libphonenumber] 2023-03-25T23:20:27Z jordi fita mas <jordi@tandem.blog> # Add function to edit contacts
|
||||
expense [schema_numerus contact company currency_code currency tag_name] 2023-04-30T13:46:36Z jordi fita mas <jordi@tandem.blog> # Add the expense relation
|
||||
|
@ -86,3 +86,14 @@ quote_product [roles schema_numerus quote discount_rate] 2023-06-06T18:25:05Z jo
|
|||
quote_product_product [roles schema_numerus quote_product product] 2023-06-06T18:38:26Z jordi fita mas <jordi@tandem.blog> # Add relation of quote products and registered products
|
||||
quote_product_tax [roles schema_numerus quote_product tax tax_rate] 2023-06-06T18:46:33Z jordi fita mas <jordi@tandem.blog> # Add relation of quotation product tax
|
||||
quote_payment_method [roles schema_numerus quote payment_method] 2023-06-06T18:59:12Z jordi fita mas <jordi@tandem.blog> # Add relation for the payment method of quotes
|
||||
quote_number_counter [roles schema_numerus company] 2023-06-07T11:05:51Z jordi fita mas <jordi@tandem.blog> # Add relatin to keep a counter of quote numbers
|
||||
next_quote_number [roles schema_numerus quote_number_counter] 2023-06-07T11:20:54Z jordi fita mas <jordi@tandem.blog> # Add function to retrieve the next quote number
|
||||
new_quote_product [schema_numerus discount_rate] 2023-06-07T11:36:37Z jordi fita mas <jordi@tandem.blog> # Add type for passing products to new quotes
|
||||
add_quote [roles schema_numerus quote company currency parse_price new_quote_product tax quote_product quote_payment_method quote_contact quote_product_product quote_product_tax next_quote_number tag_name] 2023-06-07T11:39:45Z jordi fita mas <jordi@tandem.blog> # Add function to create new quotes
|
||||
quote_tax_amount [roles schema_numerus quote_product quote_product_tax] 2023-06-07T12:45:17Z jordi fita mas <jordi@tandem.blog> # Add add view for quote tax amount
|
||||
quote_product_amount [roles schema_numerus quote_product quote_product_tax] 2023-06-07T12:48:58Z jordi fita mas <jordi@tandem.blog> # Add view for quote product subtotal and total
|
||||
quote_amount [roles schema_numerus quote_product quote_product_amount] 2023-06-07T12:52:51Z jordi fita mas <jordi@tandem.blog> # Add view to compute subtotal and total for quotes
|
||||
new_quote_amount [schema_numerus] 2023-06-07T12:57:45Z jordi fita mas <jordi@tandem.blog> # Add type to return when computing new quote amounts
|
||||
compute_new_quote_amount [roles schema_numerus company tax new_quote_product new_quote_amount] 2023-06-07T13:00:07Z jordi fita mas <jordi@tandem.blog> # Add function to compute the subtotal, taxes, and total amounts for a new quotation
|
||||
edited_quote_product [schema_numerus discount_rate] 2023-06-07T13:03:23Z jordi fita mas <jordi@tandem.blog> # Add type for passing products to edit quotations
|
||||
edit_quote [roles schema_numerus quote currency parse_price edited_quote_product tax quote_contact quote_payment_method quote_product quote_product_tax quote_product_product tag_name] 2023-06-07T13:08:10Z jordi fita mas <jordi@tandem.blog> # Add function to edit quotations
|
||||
|
|
|
@ -31,9 +31,9 @@ reset client_min_messages;
|
|||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, invoice_number_format, default_payment_method_id)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', '"F"YYYY0000', 111)
|
||||
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', '"INV"000-YY', 222)
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
-- Test add_quote
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(18);
|
||||
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
select has_function('numerus', 'add_quote', array ['integer', 'date', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'new_quote_product[]']);
|
||||
select function_lang_is('numerus', 'add_quote', array ['integer', 'date', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'new_quote_product[]'], 'plpgsql');
|
||||
select function_returns('numerus', 'add_quote', array ['integer', 'date', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'new_quote_product[]'], 'uuid');
|
||||
select isnt_definer('numerus', 'add_quote', array ['integer', 'date', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'new_quote_product[]']);
|
||||
select volatility_is('numerus', 'add_quote', array ['integer', 'date', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'new_quote_product[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'add_quote', array ['integer', 'date', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'new_quote_product[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'add_quote', array ['integer', 'date', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'new_quote_product[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'add_quote', array ['integer', 'date', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'new_quote_product[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'add_quote', array ['integer', 'date', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'new_quote_product[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate quote_number_counter cascade;
|
||||
truncate quote_product_tax cascade;
|
||||
truncate quote_product cascade;
|
||||
truncate quote_contact cascade;
|
||||
truncate quote_payment_method cascade;
|
||||
truncate quote cascade;
|
||||
truncate contact cascade;
|
||||
truncate product cascade;
|
||||
truncate tax cascade;
|
||||
truncate tax_class cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, quote_number_format, default_payment_method_id)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', '"Q"YYYY0000', 111)
|
||||
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', '"QUO"000-YY', 222)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
, (222, 2, 'cash', 'cash')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into quote_number_counter (company_id, year, currval)
|
||||
values (1, 2023, '5')
|
||||
, (2, 2023, '55')
|
||||
;
|
||||
|
||||
insert into tax_class (tax_class_id, company_id, name)
|
||||
values (11, 1, 'tax')
|
||||
, (22, 2, 'tax')
|
||||
;
|
||||
|
||||
insert into tax (tax_id, company_id, tax_class_id, name, rate)
|
||||
values (3, 1, 11, 'IRPF -15 %', -0.15)
|
||||
, (4, 1, 11, 'IVA 21 %', 0.21)
|
||||
, (5, 2, 22, 'IRPF -7 %', -0.07)
|
||||
, (6, 2, 22, 'IVA 10 %', 0.10)
|
||||
;
|
||||
|
||||
insert into product (product_id, company_id, name, price)
|
||||
values ( 7, 1, 'Product 2.1', 1212)
|
||||
, ( 8, 1, 'Product 2.2', 2424)
|
||||
, ( 9, 2, 'Product 4.1', 4848)
|
||||
, (10, 2, 'Product 4.2', 9696)
|
||||
, (11, 2, 'Product 4.3', 1010)
|
||||
;
|
||||
|
||||
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||
values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
|
||||
, (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
|
||||
, (14, 2, 'Contact 4.1', 'XX777', '', '999-999-999', 'e@e', '', '', '', '', '', 'ES')
|
||||
, (15, 2, 'Contact 4.2', 'XX888', '', '000-000-000', 'f@f', '', '', '', '', '', 'ES')
|
||||
;
|
||||
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_quote(1, '2023-02-15', 12, 'No need for advance payment', 'Notes 1', null, '{tag1,tag2}','{"(7,Product 1,Description 1,12.24,2,0.0,{4})"}') $$,
|
||||
'Should be able to insert an quote for the first company with a product'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_quote(1, '2023-02-16', null, 'Pay 10% in advance', 'Notes 2', 111, '{}', '{"(7,Product 1 bis,Description 1 bis,33.33,1,0.50,\"{4,3}\")","(8,Product 2,Description 2,24.00,3,0.75,{})"}') $$,
|
||||
'Should be able to insert a second quote for the first company with two product'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_quote(2, '2023-02-14', 15, 'Pay half in advance', 'Notes 3', 222, '{tag3}','{"(11,Product 4.3,,11.11,1,0.0,{6})","(,Product 4.4,Description 4.4,22.22,3,0.05,{})"}') $$,
|
||||
'Should be able to insert an quote for the second company with a product'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select company_id, quote_number, quote_date, quote_status, terms_and_conditions, notes, currency_code, tags, created_at from quote $$,
|
||||
$$ values (1, 'Q20230006', '2023-02-15'::date, 'created', 'No need for advance payment', 'Notes 1', 'EUR', '{tag1,tag2}'::tag_name[], current_timestamp)
|
||||
, (1, 'Q20230007', '2023-02-16'::date, 'created', 'Pay 10% in advance', 'Notes 2', 'EUR', '{}'::tag_name[], current_timestamp)
|
||||
, (2, 'QUO056-23', '2023-02-14'::date, 'created', 'Pay half in advance', 'Notes 3', 'USD', '{tag3}'::tag_name[], current_timestamp)
|
||||
$$,
|
||||
'Should have created all quotes'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_number, payment_method_id from quote_payment_method join quote using (quote_id) $$,
|
||||
$$ values ('Q20230007', 111)
|
||||
, ('QUO056-23', 222)
|
||||
$$,
|
||||
'Should have created all payment methods'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_number, contact_id from quote_contact join quote using (quote_id) $$,
|
||||
$$ values ('Q20230006', 12)
|
||||
, ('QUO056-23', 15)
|
||||
$$,
|
||||
'Should have created all contacts'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_number, name, description, price, quantity, discount_rate from quote_product join quote using (quote_id) $$,
|
||||
$$ values ('Q20230006', 'Product 1', 'Description 1', 1224, 2, 0.00)
|
||||
, ('Q20230007', 'Product 1 bis', 'Description 1 bis', 3333, 1, 0.50)
|
||||
, ('Q20230007', 'Product 2', 'Description 2', 2400, 3, 0.75)
|
||||
, ('QUO056-23', 'Product 4.3', '', 1111, 1, 0.0)
|
||||
, ('QUO056-23', 'Product 4.4', 'Description 4.4', 2222, 3, 0.05)
|
||||
$$,
|
||||
'Should have created all quote products'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_number, product_id, name from quote_product left join quote_product_product using (quote_product_id) join quote using (quote_id) $$,
|
||||
$$ values ('Q20230006', 7, 'Product 1')
|
||||
, ('Q20230007', 7, 'Product 1 bis')
|
||||
, ('Q20230007', 8, 'Product 2')
|
||||
, ('QUO056-23', 11, 'Product 4.3')
|
||||
, ('QUO056-23', NULL, 'Product 4.4')
|
||||
$$,
|
||||
'Should have linked all quote products'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_number, name, tax_id, tax_rate from quote_product_tax join quote_product using (quote_product_id) join quote using (quote_id) $$,
|
||||
$$ values ('Q20230006', 'Product 1', 4, 0.21)
|
||||
, ('Q20230007', 'Product 1 bis', 4, 0.21)
|
||||
, ('Q20230007', 'Product 1 bis', 3, -0.15)
|
||||
, ('QUO056-23', 'Product 4.3', 6, 0.10)
|
||||
$$,
|
||||
'Should have created all quote product taxes'
|
||||
);
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -5,7 +5,7 @@ reset client_min_messages;
|
|||
|
||||
begin;
|
||||
|
||||
select plan(96);
|
||||
select plan(101);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
|
@ -100,6 +100,12 @@ select col_not_null('company', 'invoice_number_format');
|
|||
select col_has_default('company', 'invoice_number_format');
|
||||
select col_default_is('company', 'invoice_number_format', '"FRA"YYYY0000');
|
||||
|
||||
select has_column('company', 'quote_number_format');
|
||||
select col_type_is('company', 'quote_number_format', 'text');
|
||||
select col_not_null('company', 'quote_number_format');
|
||||
select col_has_default('company', 'quote_number_format');
|
||||
select col_default_is('company', 'quote_number_format', '"PRE"YYYY0000');
|
||||
|
||||
select has_column('company', 'legal_disclaimer');
|
||||
select col_type_is('company', 'legal_disclaimer', 'text');
|
||||
select col_not_null('company', 'legal_disclaimer');
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
-- Test compute_new_quote_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_quote_amount', array ['integer', 'new_quote_product[]']);
|
||||
select function_lang_is('numerus', 'compute_new_quote_amount', array ['integer', 'new_quote_product[]'], 'plpgsql');
|
||||
select function_returns('numerus', 'compute_new_quote_amount', array ['integer', 'new_quote_product[]'], 'new_quote_amount');
|
||||
select isnt_definer('numerus', 'compute_new_quote_amount', array ['integer', 'new_quote_product[]']);
|
||||
select volatility_is('numerus', 'compute_new_quote_amount', array ['integer', 'new_quote_product[]'], 'stable');
|
||||
select function_privs_are('numerus', 'compute_new_quote_amount', array ['integer', 'new_quote_product[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'compute_new_quote_amount', array ['integer', 'new_quote_product[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'compute_new_quote_amount', array ['integer', 'new_quote_product[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'compute_new_quote_amount', array ['integer', 'new_quote_product[]'], 'authenticator', array []::text[]);
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate tax cascade;
|
||||
truncate tax_class cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into tax_class (tax_class_id, company_id, name)
|
||||
values (11, 1, 'tax')
|
||||
;
|
||||
|
||||
insert into tax (tax_id, company_id, tax_class_id, name, rate)
|
||||
values (2, 1, 11, 'IRPF -15 %', -0.15)
|
||||
, (3, 1, 11, 'IVA 4 %', 0.04)
|
||||
, (4, 1, 11, 'IVA 10 %', 0.10)
|
||||
, (5, 1, 11, 'IVA 21 %', 0.21)
|
||||
;
|
||||
|
||||
select is(
|
||||
compute_new_quote_amount(1, '{}'),
|
||||
'(0.00,"{}",0.00)'::new_quote_amount
|
||||
);
|
||||
|
||||
select is(
|
||||
compute_new_quote_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_quote_amount
|
||||
);
|
||||
|
||||
select is(
|
||||
compute_new_quote_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_quote_amount
|
||||
);
|
||||
|
||||
select is(
|
||||
compute_new_quote_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_quote_amount
|
||||
);
|
||||
|
||||
select is(
|
||||
compute_new_quote_amount(1, '{"(6,P,D,7.77,8,0.0,\"{}\")"}'),
|
||||
'(62.16,"{}",62.16)'::new_quote_amount
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,173 @@
|
|||
-- Test edit_quote
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(16);
|
||||
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
select has_function('numerus', 'edit_quote', array ['uuid', 'text', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'edited_quote_product[]']);
|
||||
select function_lang_is('numerus', 'edit_quote', array ['uuid', 'text', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'edited_quote_product[]'], 'plpgsql');
|
||||
select function_returns('numerus', 'edit_quote', array ['uuid', 'text', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'edited_quote_product[]'], 'uuid');
|
||||
select isnt_definer('numerus', 'edit_quote', array ['uuid', 'text', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'edited_quote_product[]']);
|
||||
select volatility_is('numerus', 'edit_quote', array ['uuid', 'text', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'edited_quote_product[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'edit_quote', array ['uuid', 'text', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'edited_quote_product[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'edit_quote', array ['uuid', 'text', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'edited_quote_product[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'edit_quote', array ['uuid', 'text', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'edited_quote_product[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'edit_quote', array ['uuid', 'text', 'integer', 'text', 'text', 'integer', 'tag_name[]', 'edited_quote_product[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate quote_product_tax cascade;
|
||||
truncate quote_product cascade;
|
||||
truncate quote_payment_method cascade;
|
||||
truncate quote_contact cascade;
|
||||
truncate quote cascade;
|
||||
truncate contact cascade;
|
||||
truncate product cascade;
|
||||
truncate tax cascade;
|
||||
truncate tax_class cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
, (112, 1, 'bank', 'send money to my bank account')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into tax_class (tax_class_id, company_id, name)
|
||||
values (11, 1, 'tax')
|
||||
;
|
||||
|
||||
insert into tax (tax_id, company_id, tax_class_id, name, rate)
|
||||
values (3, 1, 11, 'IRPF -15 %', -0.15)
|
||||
, (4, 1, 11, 'IVA 21 %', 0.21)
|
||||
;
|
||||
|
||||
insert into product (product_id, company_id, name, price)
|
||||
values ( 7, 1, 'Product 1.1', 1212)
|
||||
, ( 8, 1, 'Product 2.2', 2424)
|
||||
, ( 9, 1, 'Product 3.3', 3636)
|
||||
;
|
||||
|
||||
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||
values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
|
||||
, (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
|
||||
;
|
||||
|
||||
insert into quote (quote_id, company_id, slug, quote_number, quote_date, currency_code, tags)
|
||||
values (15, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'QUO1', '2023-03-10', 'EUR', '{tag1}')
|
||||
, (16, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'QUO2', '2023-03-09', 'EUR', '{tag2}')
|
||||
, (17, 1, '0b899316-f7d0-4175-a9d5-cea855844716', 'QUO3', '2023-03-11', 'EUR', '{}')
|
||||
;
|
||||
|
||||
insert into quote_payment_method (quote_id, payment_method_id)
|
||||
values (15, 111)
|
||||
, (16, 111)
|
||||
;
|
||||
|
||||
insert into quote_contact (quote_id, contact_id)
|
||||
values (15, 12)
|
||||
, (16, 13)
|
||||
;
|
||||
|
||||
insert into quote_product (quote_product_id, quote_id, name, price)
|
||||
values (19, 15, 'P1.0', 1100)
|
||||
, (20, 15, 'P2.0', 2200)
|
||||
, (21, 16, 'P1.1', 1111)
|
||||
, (22, 16, 'P2.1', 2211)
|
||||
, (23, 17, 'P3.1', 3311)
|
||||
;
|
||||
|
||||
insert into quote_product_product (quote_product_id, product_id)
|
||||
values (19, 7)
|
||||
, (20, 8)
|
||||
, (21, 7)
|
||||
, (22, 8)
|
||||
;
|
||||
|
||||
insert into quote_product_tax (quote_product_id, tax_id, tax_rate)
|
||||
values (19, 4, 0.21)
|
||||
, (20, 4, 0.21)
|
||||
, (21, 3, -0.07)
|
||||
, (21, 4, 0.21)
|
||||
, (22, 3, -0.15)
|
||||
;
|
||||
|
||||
select lives_ok(
|
||||
$$ select edit_quote('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'accepted', null, 'Terms 1', 'Notes 1', 112, array['tag1'], '{"(20,,p1.0,D1,11.01,2,0.50,{4})","(,,p1.3,D3,33.33,3,0.05,{3})"}') $$,
|
||||
'Should be able to edit the first invoice'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select edit_quote('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'sent', 12, 'Terms 2', 'Notes 2', null, array['tag1', 'tag3'], '{"(21,7,P1.1,,11.11,1,0.0,{3})","(22,8,p2.1,D2,24.00,3,0.75,\"{3,4}\")","(,9,p3.3,,36.36,2,0.05,{4})"}') $$,
|
||||
'Should be able to edit the second quote'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select edit_quote('0b899316-f7d0-4175-a9d5-cea855844716', 'rejected', 13, '', '', 111, '{}', '{"(23,,p3.1,,41.41,1,0.0,{})"}') $$,
|
||||
'Should be able to edit the third quote'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_number, quote_date, contact_id, quote_status, terms_and_conditions, notes, tags, payment_method_id from quote left join quote_contact using (quote_id) left join quote_payment_method using (quote_id) $$,
|
||||
$$ values ('QUO1', '2023-03-10'::date, null, 'accepted', 'Terms 1', 'Notes 1', '{tag1}'::tag_name[], 112)
|
||||
, ('QUO2', '2023-03-09'::date, 12, 'sent', 'Terms 2', 'Notes 2', '{tag1,tag3}'::tag_name[], null)
|
||||
, ('QUO3', '2023-03-11'::date, 13, 'rejected', '', '', '{}'::tag_name[], 111)
|
||||
$$,
|
||||
'Should have updated all quotes'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_number, name, description, price, quantity, discount_rate from quote_product join quote using (quote_id) $$,
|
||||
$$ values ('QUO1', 'p1.0', 'D1', 1101, 2, 0.50)
|
||||
, ('QUO1', 'p1.3', 'D3', 3333, 3, 0.05)
|
||||
, ('QUO2', 'P1.1', '', 1111, 1, 0.00)
|
||||
, ('QUO2', 'p2.1', 'D2', 2400, 3, 0.75)
|
||||
, ('QUO2', 'p3.3', '', 3636, 2, 0.05)
|
||||
, ('QUO3', 'p3.1', '', 4141, 1, 0.00)
|
||||
$$,
|
||||
'Should have updated all existing quote products, added new ones, and removed the ones not give to the function'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_number, product_id, name from quote_product left join quote_product_product using (quote_product_id) join quote using (quote_id) $$,
|
||||
$$ values ('QUO1', NULL, 'p1.0')
|
||||
, ('QUO1', NULL, 'p1.3')
|
||||
, ('QUO2', 7, 'P1.1')
|
||||
, ('QUO2', 8, 'p2.1')
|
||||
, ('QUO2', 9, 'p3.3')
|
||||
, ('QUO3', NULL, 'p3.1')
|
||||
$$,
|
||||
'Should have updated all existing quote products id, added new ones, and removed the ones not give to the function'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_number, name, tax_id, tax_rate from quote_product_tax join quote_product using (quote_product_id) join quote using (quote_id) $$,
|
||||
$$ values ('QUO1', 'p1.0', 4, 0.21)
|
||||
, ('QUO1', 'p1.3', 3, -0.15)
|
||||
, ('QUO2', 'P1.1', 3, -0.15)
|
||||
, ('QUO2', 'p2.1', 3, -0.15)
|
||||
, ('QUO2', 'p2.1', 4, 0.21)
|
||||
, ('QUO2', 'p3.3', 4, 0.21)
|
||||
$$,
|
||||
'Should have updated all quote product taxes, added new ones, and removed the ones not given to the function'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,26 @@
|
|||
-- Test edited_quote_product
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(10);
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
select has_composite('numerus', 'edited_quote_product', 'Composite type numerus.edited_quote_product should exist');
|
||||
select columns_are('numerus', 'edited_quote_product', array['quote_product_id', 'product_id', 'name', 'description', 'price', 'quantity', 'discount_rate', 'tax']);
|
||||
select col_type_is('numerus'::name, 'edited_quote_product'::name, 'quote_product_id'::name, 'integer');
|
||||
select col_type_is('numerus'::name, 'edited_quote_product'::name, 'product_id'::name, 'integer');
|
||||
select col_type_is('numerus'::name, 'edited_quote_product'::name, 'name'::name, 'text');
|
||||
select col_type_is('numerus'::name, 'edited_quote_product'::name, 'description'::name, 'text');
|
||||
select col_type_is('numerus'::name, 'edited_quote_product'::name, 'price'::name, 'text');
|
||||
select col_type_is('numerus'::name, 'edited_quote_product'::name, 'quantity'::name, 'integer');
|
||||
select col_type_is('numerus'::name, 'edited_quote_product'::name, 'discount_rate'::name, 'discount_rate');
|
||||
select col_type_is('numerus'::name, 'edited_quote_product'::name, 'tax'::name, 'integer[]');
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -64,10 +64,10 @@ values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '',
|
|||
;
|
||||
|
||||
insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code, payment_method_id)
|
||||
values ( 8, 1, 'I1', current_date, 7, 'EUR', '111')
|
||||
, ( 9, 1, 'I2', current_date, 7, 'EUR', '111')
|
||||
, (10, 1, 'I3', current_date, 7, 'EUR', '111')
|
||||
, (11, 1, 'I4', current_date, 7, 'EUR', '111')
|
||||
values ( 8, 1, 'I1', current_date, 7, 'EUR', 111)
|
||||
, ( 9, 1, 'I2', current_date, 7, 'EUR', 111)
|
||||
, (10, 1, 'I3', current_date, 7, 'EUR', 111)
|
||||
, (11, 1, 'I4', current_date, 7, 'EUR', 111)
|
||||
;
|
||||
|
||||
insert into invoice_product (invoice_product_id, invoice_id, name, price, quantity, discount_rate)
|
||||
|
|
|
@ -5,7 +5,7 @@ reset client_min_messages;
|
|||
|
||||
begin;
|
||||
|
||||
select plan(25);
|
||||
select plan(28);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
|
@ -19,6 +19,8 @@ select table_privs_are('invoice_number_counter', 'admin', array ['SELECT', 'INSE
|
|||
select table_privs_are('invoice_number_counter', 'authenticator', array []::text[]);
|
||||
|
||||
select has_column('invoice_number_counter', 'company_id');
|
||||
select col_is_fk('invoice_number_counter', 'company_id');
|
||||
select fk_ok('invoice_number_counter', 'company_id', 'company', 'company_id');
|
||||
select col_type_is('invoice_number_counter', 'company_id', 'integer');
|
||||
select col_not_null('invoice_number_counter', 'company_id');
|
||||
select col_hasnt_default('invoice_number_counter', 'company_id');
|
||||
|
@ -66,11 +68,6 @@ values (2, 1)
|
|||
, (4, 5)
|
||||
;
|
||||
|
||||
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||
values (6, 2, 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
|
||||
, (8, 4, 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
|
||||
;
|
||||
|
||||
insert into invoice_number_counter (company_id, year, currval)
|
||||
values (2, 2010, 6)
|
||||
, (2, 2011, 8)
|
||||
|
@ -119,11 +116,19 @@ reset role;
|
|||
|
||||
select lives_ok( $$
|
||||
insert into invoice_number_counter (company_id, year, currval)
|
||||
values (2, 2008, 0)
|
||||
values (2, 2009, 0)
|
||||
$$,
|
||||
'Should allow starting a counter from zero'
|
||||
);
|
||||
|
||||
select throws_ok( $$
|
||||
insert into invoice_number_counter (company_id, year, currval)
|
||||
values (2, 2008, -1)
|
||||
$$,
|
||||
'23514', 'new row for relation "invoice_number_counter" violates check constraint "counter_zero_or_positive"',
|
||||
'Should not allow starting a counter from a negative value'
|
||||
);
|
||||
|
||||
select throws_ok( $$
|
||||
insert into invoice_number_counter (company_id, year, currval)
|
||||
values (2, -2008, 1)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
-- Test new_quote_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_quote_amount', 'Composite type numerus.new_quote_amount should exist');
|
||||
select columns_are('numerus', 'new_quote_amount', array['subtotal', 'taxes', 'total']);
|
||||
select col_type_is('numerus'::name, 'new_quote_amount'::name, 'subtotal'::name, 'text');
|
||||
select col_type_is('numerus'::name, 'new_quote_amount'::name, 'taxes'::name, 'text[]');
|
||||
select col_type_is('numerus'::name, 'new_quote_amount'::name, 'total'::name, 'text');
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,25 @@
|
|||
-- Test new_quote_product
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(9);
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
select has_composite('numerus', 'new_quote_product', 'Composite type numerus.new_quote_product should exist');
|
||||
select columns_are('numerus', 'new_quote_product', array['product_id', 'name', 'description', 'price', 'quantity', 'discount_rate', 'tax']);
|
||||
select col_type_is('numerus'::name, 'new_quote_product'::name, 'product_id'::name, 'integer');
|
||||
select col_type_is('numerus'::name, 'new_quote_product'::name, 'name'::name, 'text');
|
||||
select col_type_is('numerus'::name, 'new_quote_product'::name, 'description'::name, 'text');
|
||||
select col_type_is('numerus'::name, 'new_quote_product'::name, 'price'::name, 'text');
|
||||
select col_type_is('numerus'::name, 'new_quote_product'::name, 'quantity'::name, 'integer');
|
||||
select col_type_is('numerus'::name, 'new_quote_product'::name, 'discount_rate'::name, 'discount_rate');
|
||||
select col_type_is('numerus'::name, 'new_quote_product'::name, 'tax'::name, 'integer[]');
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,60 @@
|
|||
-- Test next_quote_number
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(17);
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
select has_function('numerus', 'next_quote_number', array ['integer', 'date']);
|
||||
select function_lang_is('numerus', 'next_quote_number', array ['integer', 'date'], 'plpgsql');
|
||||
select function_returns('numerus', 'next_quote_number', array ['integer', 'date'], 'text');
|
||||
select isnt_definer('numerus', 'next_quote_number', array ['integer', 'date']);
|
||||
select volatility_is('numerus', 'next_quote_number', array ['integer', 'date'], 'volatile');
|
||||
select function_privs_are('numerus', 'next_quote_number', array ['integer', 'date'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'next_quote_number', array ['integer', 'date'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'next_quote_number', array ['integer', 'date'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'next_quote_number', array ['integer', 'date'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate quote_number_counter cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, quote_number_format, default_payment_method_id)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', '"Q"YYYY0000', 111)
|
||||
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', '"QUO"000-YY', 222)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
, (222, 2, 'cash', 'cash')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into quote_number_counter (company_id, year, currval)
|
||||
values (1, 2010, 5)
|
||||
, (2, 2010, 6)
|
||||
;
|
||||
|
||||
select is( next_quote_number(1, '2010-12-25'), 'Q20100006' );
|
||||
select is( next_quote_number(2, '2010-12-25'), 'QUO007-10' );
|
||||
select is( next_quote_number(1, '2010-10-17'), 'Q20100007' );
|
||||
select is( next_quote_number(2, '2010-10-17'), 'QUO008-10' );
|
||||
select is( next_quote_number(1, '2011-12-25'), 'Q20110001' );
|
||||
select is( next_quote_number(2, '2012-12-25'), 'QUO001-12' );
|
||||
select is( next_quote_number(1, '2011-12-25'), 'Q20110002' );
|
||||
select is( next_quote_number(2, '2012-12-25'), 'QUO002-12' );
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,110 @@
|
|||
-- Test quote_amount
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(12);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_view('quote_amount');
|
||||
select table_privs_are('quote_amount', 'guest', array[]::text[]);
|
||||
select table_privs_are('quote_amount', 'invoicer', array['SELECT']);
|
||||
select table_privs_are('quote_amount', 'admin', array['SELECT']);
|
||||
select table_privs_are('quote_amount', 'authenticator', array[]::text[]);
|
||||
|
||||
select has_column('quote_amount', 'quote_id');
|
||||
select col_type_is('quote_amount', 'quote_id', 'integer');
|
||||
|
||||
select has_column('quote_amount', 'subtotal');
|
||||
select col_type_is('quote_amount', 'subtotal', 'integer');
|
||||
|
||||
select has_column('quote_amount', 'total');
|
||||
select col_type_is('quote_amount', 'total', 'integer');
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate quote_product_tax cascade;
|
||||
truncate quote_product cascade;
|
||||
truncate quote cascade;
|
||||
truncate contact cascade;
|
||||
truncate tax cascade;
|
||||
truncate tax_class cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into tax_class (tax_class_id, company_id, name)
|
||||
values (11, 1, 'tax')
|
||||
;
|
||||
|
||||
insert into tax (tax_id, company_id, tax_class_id, name, rate)
|
||||
values (2, 1, 11, 'IRPF -15 %', -0.15)
|
||||
, (3, 1, 11, 'IVA 4 %', 0.04)
|
||||
, (4, 1, 11, 'IVA 10 %', 0.10)
|
||||
, (5, 1, 11, 'IVA 21 %', 0.21)
|
||||
;
|
||||
|
||||
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
|
||||
values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
|
||||
;
|
||||
|
||||
insert into quote (quote_id, company_id, quote_number, quote_date, currency_code)
|
||||
values ( 8, 1, 'I1', current_date, 'EUR')
|
||||
, ( 9, 1, 'I2', current_date, 'EUR')
|
||||
, (10, 1, 'I3', current_date, 'EUR')
|
||||
, (11, 1, 'I4', current_date, 'EUR')
|
||||
;
|
||||
|
||||
insert into quote_product (quote_product_id, quote_id, name, price, quantity, discount_rate)
|
||||
values (12, 8, 'P', 100, 1, 0.0)
|
||||
, (13, 8, 'P', 200, 2, 0.1)
|
||||
, (14, 9, 'P', 222, 3, 0.0)
|
||||
, (15, 9, 'P', 333, 4, 0.2)
|
||||
, (16, 10, 'P', 444, 5, 0.0)
|
||||
, (17, 10, 'P', 555, 6, 0.1)
|
||||
, (18, 11, 'P', 777, 8, 0.0)
|
||||
;
|
||||
|
||||
insert into quote_product_tax (quote_product_id, tax_id, tax_rate)
|
||||
values (12, 2, -0.15)
|
||||
, (12, 5, 0.21)
|
||||
, (13, 3, 0.04)
|
||||
, (14, 4, 0.10)
|
||||
, (14, 5, 0.21)
|
||||
, (14, 2, -0.07)
|
||||
, (15, 4, 0.10)
|
||||
, (16, 4, 0.10)
|
||||
, (16, 5, 0.21)
|
||||
, (17, 5, 0.21)
|
||||
, (17, 3, 0.04)
|
||||
;
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_id, subtotal, total from quote_amount $$,
|
||||
$$ values ( 8, 460, 480)
|
||||
, ( 9, 1732, 1999)
|
||||
, (10, 5217, 6654)
|
||||
, (11, 6216, 6216)
|
||||
$$,
|
||||
'Should compute the amount for all taxes in the quoted products.'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,145 @@
|
|||
-- Test quote_number_counter
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(28);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_table('quote_number_counter');
|
||||
select has_pk('quote_number_counter' );
|
||||
select col_is_pk('quote_number_counter', array['company_id', 'year']);
|
||||
|
||||
select table_privs_are('quote_number_counter', 'guest', array []::text[]);
|
||||
select table_privs_are('quote_number_counter', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE']);
|
||||
select table_privs_are('quote_number_counter', 'admin', array ['SELECT', 'INSERT', 'UPDATE']);
|
||||
select table_privs_are('quote_number_counter', 'authenticator', array []::text[]);
|
||||
|
||||
select has_column('quote_number_counter', 'company_id');
|
||||
select col_is_fk('quote_number_counter', 'company_id');
|
||||
select fk_ok('quote_number_counter', 'company_id', 'company', 'company_id');
|
||||
select col_type_is('quote_number_counter', 'company_id', 'integer');
|
||||
select col_not_null('quote_number_counter', 'company_id');
|
||||
select col_hasnt_default('quote_number_counter', 'company_id');
|
||||
|
||||
select has_column('quote_number_counter', 'year');
|
||||
select col_type_is('quote_number_counter', 'year', 'integer');
|
||||
select col_not_null('quote_number_counter', 'year');
|
||||
select col_hasnt_default('quote_number_counter', 'year');
|
||||
|
||||
select has_column('quote_number_counter', 'currval');
|
||||
select col_type_is('quote_number_counter', 'currval', 'integer');
|
||||
select col_not_null('quote_number_counter', 'currval');
|
||||
select col_hasnt_default('quote_number_counter', 'currval');
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate quote_number_counter cascade;
|
||||
truncate company_user cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
truncate auth."user" cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
|
||||
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
|
||||
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
|
||||
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (444, 4, 'cash', 'cash')
|
||||
, (222, 2, 'cash', 'cash')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into company_user (company_id, user_id)
|
||||
values (2, 1)
|
||||
, (4, 5)
|
||||
;
|
||||
|
||||
insert into quote_number_counter (company_id, year, currval)
|
||||
values (2, 2010, 6)
|
||||
, (2, 2011, 8)
|
||||
, (4, 2010, 8)
|
||||
, (4, 2012, 10)
|
||||
;
|
||||
|
||||
|
||||
prepare counter_data as
|
||||
select company_id, year, currval
|
||||
from quote_number_counter
|
||||
;
|
||||
|
||||
set role invoicer;
|
||||
select is_empty('counter_data', 'Should show no data when cookie is not set yet');
|
||||
reset role;
|
||||
|
||||
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
|
||||
select bag_eq(
|
||||
'counter_data',
|
||||
$$ values (2, 2010, 6)
|
||||
, (2, 2011, 8)
|
||||
$$,
|
||||
'Should only list quote numbers of the companies where demo@tandem.blog is user of'
|
||||
);
|
||||
reset role;
|
||||
|
||||
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
|
||||
select bag_eq(
|
||||
'counter_data',
|
||||
$$ values (4, 2010, 8)
|
||||
, (4, 2012, 10)
|
||||
$$,
|
||||
'Should only list quote numbers of the companies where admin@tandem.blog is user of'
|
||||
);
|
||||
reset role;
|
||||
|
||||
select set_cookie('not-a-cookie');
|
||||
select throws_ok(
|
||||
'counter_data',
|
||||
'42501', 'permission denied for table quote_number_counter',
|
||||
'Should not allow select to guest users'
|
||||
);
|
||||
reset role;
|
||||
|
||||
|
||||
select lives_ok( $$
|
||||
insert into quote_number_counter (company_id, year, currval)
|
||||
values (2, 2008, 0)
|
||||
$$,
|
||||
'Should allow starting a counter from zero'
|
||||
);
|
||||
|
||||
select throws_ok( $$
|
||||
insert into quote_number_counter (company_id, year, currval)
|
||||
values (2, 2009, -1)
|
||||
$$,
|
||||
'23514', 'new row for relation "quote_number_counter" violates check constraint "counter_zero_or_positive"',
|
||||
'Should not allow starting a counter from a negative value'
|
||||
);
|
||||
|
||||
select throws_ok( $$
|
||||
insert into quote_number_counter (company_id, year, currval)
|
||||
values (2, -2008, 1)
|
||||
$$,
|
||||
'23514', 'new row for relation "quote_number_counter" violates check constraint "year_always_positive"',
|
||||
'Should not allow counters for quotes issued before Jesus Christ was born'
|
||||
);
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
-- Test quote_product_amount
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(12);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_view('quote_product_amount');
|
||||
select table_privs_are('quote_product_amount', 'guest', array[]::text[]);
|
||||
select table_privs_are('quote_product_amount', 'invoicer', array['SELECT']);
|
||||
select table_privs_are('quote_product_amount', 'admin', array['SELECT']);
|
||||
select table_privs_are('quote_product_amount', 'authenticator', array[]::text[]);
|
||||
|
||||
select has_column('quote_product_amount', 'quote_product_id');
|
||||
select col_type_is('quote_product_amount', 'quote_product_id', 'integer');
|
||||
|
||||
select has_column('quote_product_amount', 'subtotal');
|
||||
select col_type_is('quote_product_amount', 'subtotal', 'integer');
|
||||
|
||||
select has_column('quote_product_amount', 'total');
|
||||
select col_type_is('quote_product_amount', 'total', 'integer');
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate quote_product_tax cascade;
|
||||
truncate quote_product cascade;
|
||||
truncate quote cascade;
|
||||
truncate tax cascade;
|
||||
truncate tax_class cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into tax_class (tax_class_id, company_id, name)
|
||||
values (11, 1, 'tax')
|
||||
;
|
||||
|
||||
insert into tax (tax_id, company_id, tax_class_id, name, rate)
|
||||
values (2, 1, 11, 'IRPF -15 %', -0.15)
|
||||
, (3, 1, 11, 'IVA 4 %', 0.04)
|
||||
, (4, 1, 11, 'IVA 10 %', 0.10)
|
||||
, (5, 1, 11, 'IVA 21 %', 0.21)
|
||||
;
|
||||
|
||||
insert into quote (quote_id, company_id, quote_number, quote_date, currency_code)
|
||||
values ( 8, 1, 'I1', current_date, 'EUR')
|
||||
, ( 9, 1, 'I2', current_date, 'EUR')
|
||||
, (10, 1, 'I3', current_date, 'EUR')
|
||||
, (11, 1, 'I4', current_date, 'EUR')
|
||||
;
|
||||
|
||||
insert into quote_product (quote_product_id, quote_id, name, price, quantity, discount_rate)
|
||||
values (12, 8, 'P', 100, 1, 0.0)
|
||||
, (13, 8, 'P', 200, 2, 0.1)
|
||||
, (14, 9, 'P', 222, 3, 0.0)
|
||||
, (15, 9, 'P', 333, 4, 0.2)
|
||||
, (16, 10, 'P', 444, 5, 0.0)
|
||||
, (17, 10, 'P', 555, 6, 0.1)
|
||||
, (18, 11, 'P', 777, 8, 0.0)
|
||||
;
|
||||
|
||||
insert into quote_product_tax (quote_product_id, tax_id, tax_rate)
|
||||
values (12, 2, -0.15)
|
||||
, (12, 5, 0.21)
|
||||
, (13, 3, 0.04)
|
||||
, (14, 4, 0.10)
|
||||
, (14, 5, 0.21)
|
||||
, (14, 2, -0.07)
|
||||
, (15, 4, 0.10)
|
||||
, (16, 4, 0.10)
|
||||
, (16, 5, 0.21)
|
||||
, (17, 5, 0.21)
|
||||
, (17, 3, 0.04)
|
||||
;
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_product_id, subtotal, total from quote_product_amount $$,
|
||||
$$ values (12, 100, 106)
|
||||
, (13, 360, 374)
|
||||
, (14, 666, 826)
|
||||
, (15, 1066, 1173)
|
||||
, (16, 2220, 2908)
|
||||
, (17, 2997, 3746)
|
||||
, (18, 6216, 6216)
|
||||
$$,
|
||||
'Should compute the subtotal and total for all products.'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -40,7 +40,7 @@ select col_hasnt_default('quote_product_tax', 'tax_rate');
|
|||
set client_min_messages to warning;
|
||||
truncate quote_product_tax cascade;
|
||||
truncate quote_product cascade;
|
||||
truncate invoice cascade;
|
||||
truncate quote cascade;
|
||||
truncate tax cascade;
|
||||
truncate tax_class cascade;
|
||||
truncate company_user cascade;
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
-- Test quote_tax_amount
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(12);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_view('quote_tax_amount');
|
||||
select table_privs_are('quote_tax_amount', 'guest', array[]::text[]);
|
||||
select table_privs_are('quote_tax_amount', 'invoicer', array['SELECT']);
|
||||
select table_privs_are('quote_tax_amount', 'admin', array['SELECT']);
|
||||
select table_privs_are('quote_tax_amount', 'authenticator', array[]::text[]);
|
||||
|
||||
select has_column('quote_tax_amount', 'quote_id');
|
||||
select col_type_is('quote_tax_amount', 'quote_id', 'integer');
|
||||
|
||||
select has_column('quote_tax_amount', 'tax_id');
|
||||
select col_type_is('quote_tax_amount', 'tax_id', 'integer');
|
||||
|
||||
select has_column('quote_tax_amount', 'amount');
|
||||
select col_type_is('quote_tax_amount', 'amount', 'integer');
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate quote_product_tax cascade;
|
||||
truncate quote_product cascade;
|
||||
truncate quote cascade;
|
||||
truncate tax cascade;
|
||||
truncate tax_class cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into tax_class (tax_class_id, company_id, name)
|
||||
values (11, 1, 'tax')
|
||||
;
|
||||
|
||||
insert into tax (tax_id, company_id, tax_class_id, name, rate)
|
||||
values (2, 1, 11, 'IRPF -15 %', -0.15)
|
||||
, (3, 1, 11, 'IVA 4 %', 0.04)
|
||||
, (4, 1, 11, 'IVA 10 %', 0.10)
|
||||
, (5, 1, 11, 'IVA 21 %', 0.21)
|
||||
;
|
||||
|
||||
insert into quote (quote_id, company_id, quote_number, quote_date, currency_code)
|
||||
values ( 8, 1, 'I1', current_date, 'EUR')
|
||||
, ( 9, 1, 'I2', current_date, 'EUR')
|
||||
, (10, 1, 'I3', current_date, 'EUR')
|
||||
, (11, 1, 'I4', current_date, 'EUR')
|
||||
;
|
||||
|
||||
insert into quote_product (quote_product_id, quote_id, name, price, quantity, discount_rate)
|
||||
values (12, 8, 'P', 100, 1, 0.0)
|
||||
, (13, 8, 'P', 200, 2, 0.1)
|
||||
, (14, 9, 'P', 222, 3, 0.0)
|
||||
, (15, 9, 'P', 333, 4, 0.2)
|
||||
, (16, 10, 'P', 444, 5, 0.0)
|
||||
, (17, 10, 'P', 555, 6, 0.1)
|
||||
, (18, 11, 'P', 777, 8, 0.0)
|
||||
;
|
||||
|
||||
insert into quote_product_tax (quote_product_id, tax_id, tax_rate)
|
||||
values (12, 2, -0.15)
|
||||
, (12, 5, 0.21)
|
||||
, (13, 3, 0.04)
|
||||
, (14, 4, 0.10)
|
||||
, (14, 5, 0.21)
|
||||
, (14, 2, -0.07)
|
||||
, (15, 4, 0.10)
|
||||
, (16, 4, 0.10)
|
||||
, (16, 5, 0.21)
|
||||
, (17, 5, 0.21)
|
||||
, (17, 3, 0.04)
|
||||
;
|
||||
|
||||
select bag_eq(
|
||||
$$ select quote_id, tax_id, amount from quote_tax_amount $$,
|
||||
$$ values ( 8, 2, -15)
|
||||
, ( 8, 3, 14)
|
||||
, ( 8, 5, 21)
|
||||
, ( 9, 2, -47)
|
||||
, ( 9, 4, 174)
|
||||
, ( 9, 5, 140)
|
||||
, (10, 3, 120)
|
||||
, (10, 4, 222)
|
||||
, (10, 5, 1095)
|
||||
$$,
|
||||
'Should compute the amount for all taxes in the quoted products.'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:add_quote on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.add_quote(integer, date, integer, text, text, integer, numerus.tag_name[], numerus.new_quote_product[])', 'execute');
|
||||
|
||||
rollback;
|
|
@ -17,6 +17,7 @@ select company_id
|
|||
, country_code
|
||||
, currency_code
|
||||
, invoice_number_format
|
||||
, quote_number_format
|
||||
, legal_disclaimer
|
||||
, created_at
|
||||
from numerus.company
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:compute_new_quote_amount on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.compute_new_quote_amount(integer, numerus.new_quote_product[])', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:edit_quote on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.edit_quote(uuid, text, integer, text, text, integer, numerus.tag_name[], numerus.edited_quote_product[])', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:edited_quote_product on pg
|
||||
|
||||
begin;
|
||||
|
||||
select pg_catalog.has_type_privilege('numerus.edited_quote_product', 'usage');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:new_quote_amount on pg
|
||||
|
||||
begin;
|
||||
|
||||
select pg_catalog.has_type_privilege('numerus.new_invoice_amount', 'usage');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:new_quote_product on pg
|
||||
|
||||
begin;
|
||||
|
||||
select pg_catalog.has_type_privilege('numerus.new_quote_product', 'usage');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:next_quote_number on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.next_quote_number(integer, date)', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,11 @@
|
|||
-- Verify numerus:quote_amount on pg
|
||||
|
||||
begin;
|
||||
|
||||
select quote_id
|
||||
, subtotal
|
||||
, total
|
||||
from numerus.quote_amount
|
||||
where false;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,14 @@
|
|||
-- Verify numerus:quote_number_counter on pg
|
||||
|
||||
begin;
|
||||
|
||||
select company_id
|
||||
, year
|
||||
, currval
|
||||
from numerus.quote_number_counter
|
||||
where false;
|
||||
|
||||
select 1 / count(*) from pg_class where oid = 'numerus.quote_number_counter'::regclass and relrowsecurity;
|
||||
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.quote_number_counter'::regclass;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,11 @@
|
|||
-- Verify numerus:quote_product_amount on pg
|
||||
|
||||
begin;
|
||||
|
||||
select quote_product_id
|
||||
, subtotal
|
||||
, total
|
||||
from numerus.quote_product_amount
|
||||
where false;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,11 @@
|
|||
-- Verify numerus:quote_tax_amount on pg
|
||||
|
||||
begin;
|
||||
|
||||
select quote_id
|
||||
, tax_id
|
||||
, amount
|
||||
from numerus.quote_tax_amount
|
||||
where false;
|
||||
|
||||
rollback;
|
|
@ -577,28 +577,33 @@ main > nav {
|
|||
|
||||
/* Invoice */
|
||||
|
||||
.new-quote-product input,
|
||||
.new-invoice-product input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-quote-product,
|
||||
.new-invoice-product {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.new-quote-product .delete-product,
|
||||
.new-invoice-product .delete-product {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: .75rem;
|
||||
}
|
||||
|
||||
.new-quote-product .input:nth-of-type(5),
|
||||
.new-invoice-product .input:nth-of-type(5) {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 4;
|
||||
|
||||
}
|
||||
|
||||
.new-quote-product textarea,
|
||||
.new-invoice-product textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -608,23 +613,28 @@ main > nav {
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.quote-download,
|
||||
.invoice-download {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quote-download a,
|
||||
.invoice-download a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.quote-status,
|
||||
.invoice-status {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quote-status summary,
|
||||
.invoice-status summary {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.quote-status ul,
|
||||
.invoice-status ul {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -635,29 +645,35 @@ main > nav {
|
|||
gap: 1rem;
|
||||
}
|
||||
|
||||
.quote-status button,
|
||||
.invoice-status button {
|
||||
border: 0;
|
||||
min-width: 15rem;
|
||||
}
|
||||
|
||||
[class^='quote-status-'],
|
||||
[class^='invoice-status-'] {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quote-status-created,
|
||||
.invoice-status-created {
|
||||
background-color: var(--numerus--color--light-blue);
|
||||
}
|
||||
|
||||
.quote-status-sent,
|
||||
.invoice-status-sent {
|
||||
background-color: var(--numerus--color--hay);
|
||||
}
|
||||
|
||||
.quote-status-accepted,
|
||||
.invoice-status-paid {
|
||||
background-color: var(--numerus--color--light-green);
|
||||
}
|
||||
|
||||
.quote-status-rejected,
|
||||
.invoice-status-unpaid {
|
||||
background-color: var(--numerus--color--rosy);
|
||||
}
|
||||
|
@ -670,12 +686,14 @@ main > nav {
|
|||
right: 0;
|
||||
}
|
||||
|
||||
.invoice-data, .product-data, .expenses-data {
|
||||
.invoice-data, .quote-data, .product-data, .expenses-data {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.quote-data .input:last-child,
|
||||
.quote-data .input:nth-child(6),
|
||||
.invoice-data .input:last-child {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 5;
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
<nav aria-label="{{( pgettext "Main" "title" )}}" data-hx-target="main" data-hx-boost="true">
|
||||
<ul>
|
||||
<li><a href="{{ companyURI "/" }}">{{( pgettext "Dashboard" "nav" )}}</a></li>
|
||||
<li><a href="{{ companyURI "/quotes" }}">{{( pgettext "Quotations" "nav" )}}</a></li>
|
||||
<li><a href="{{ companyURI "/invoices" }}">{{( pgettext "Invoices" "nav" )}}</a></li>
|
||||
<li><a href="{{ companyURI "/expenses" }}">{{( pgettext "Expenses" "nav" )}}</a></li>
|
||||
<li><a href="{{ companyURI "/products" }}">{{( pgettext "Products" "nav" )}}</a></li>
|
||||
|
|
|
@ -165,6 +165,29 @@
|
|||
</fieldset>
|
||||
{{- end }}
|
||||
|
||||
{{ define "quote-product-form" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.quoteProductForm*/ -}}
|
||||
<fieldset class="new-quote-product"
|
||||
data-hx-select="unset"
|
||||
data-hx-vals='{"index": {{ .Index }}}'
|
||||
data-hx-include="[name='product.quantity.{{ .Index }}']"
|
||||
>
|
||||
<button type="submit" class="icon delete-product"
|
||||
formnovalidate
|
||||
name="action" value="remove-product.{{ .Index }}"
|
||||
aria-label="{{( gettext "Delete product from quotation" )}}"
|
||||
><i class="ri-delete-back-2-line"></i></button>
|
||||
{{ template "hidden-field" .QuoteProductId }}
|
||||
{{ template "hidden-field" .ProductId }}
|
||||
{{ template "input-field" .Name }}
|
||||
{{ template "input-field" .Price }}
|
||||
{{ template "input-field" .Quantity }}
|
||||
{{ template "input-field" .Discount }}
|
||||
{{ template "input-field" .Description | addInputAttr `rows="1"`}}
|
||||
{{ template "select-field" .Tax }}
|
||||
</fieldset>
|
||||
{{- end }}
|
||||
|
||||
{{ define "filters-toggle" -}}
|
||||
<button id="filters-toggle" x-cloak x-data="{}"
|
||||
@click="document.body.classList.toggle('filters-visible')"
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
{{ define "title" -}}
|
||||
{{ printf ( pgettext "Edit Quotation “%s”" "title" ) .Number }}
|
||||
{{- end }}
|
||||
|
||||
{{ define "breadcrumbs" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editQuotationPage*/ -}}
|
||||
<nav data-hx-target="main" data-hx-boost="true">
|
||||
<p>
|
||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||
<a href="{{ companyURI "/quotes"}}">{{( pgettext "Quotations" "title" )}}</a> /
|
||||
<a>{{ .Number }}</a>
|
||||
</p>
|
||||
</nav>
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editQuotationPage*/ -}}
|
||||
<section id="quote-dialog-content" data-hx-target="main">
|
||||
<h2>{{ printf (pgettext "Edit Quotation “%s”" "title") .Number }}</h2>
|
||||
<form method="POST" action="{{ companyURI "/quotes/" }}{{ .Slug }}/edit" data-hx-boost="true">
|
||||
{{ csrfToken }}
|
||||
|
||||
{{ with .Form -}}
|
||||
{{ if .RemovedProduct -}}
|
||||
<div role="alert">
|
||||
{{ with .RemovedProduct -}}
|
||||
<p>{{printf (gettext "Product “%s” removed") .Name}}</p>
|
||||
<button type="submit"
|
||||
formnovalidate
|
||||
name="action" value="restore-product"
|
||||
>{{( pgettext "Undo" "action" )}}</button>
|
||||
{{ template "hidden-field" .QuoteProductId }}
|
||||
{{ template "hidden-field" .ProductId }}
|
||||
{{ template "hidden-field" .Name }}
|
||||
{{ template "hidden-field" .Price }}
|
||||
{{ template "hidden-field" .Quantity }}
|
||||
{{ template "hidden-field" .Discount }}
|
||||
{{ template "hidden-field" .Description }}
|
||||
{{ template "hidden-select-field" .Tax }}
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
|
||||
<div class="quote-data">
|
||||
{{ template "select-field" .Customer }}
|
||||
{{ template "hidden-field" .Date }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
{{ template "select-field" .PaymentMethod }}
|
||||
{{ template "select-field" .QuoteStatus }}
|
||||
{{ template "input-field" .TermsAndConditions }}
|
||||
{{ template "input-field" .Notes }}
|
||||
</div>
|
||||
|
||||
{{- range $product := .Products }}
|
||||
{{ template "quote-product-form" . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
<table id="quote-summary">
|
||||
<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 class="button-bar">
|
||||
<button formnovalidate
|
||||
name="action" value="select-products"
|
||||
data-hx-get="{{ companyURI "/quotes/product-form" }}"
|
||||
data-hx-target="#quote-summary" data-hx-swap="beforebegin"
|
||||
data-hx-select="unset"
|
||||
data-hx-vals="js:{index: document.querySelectorAll('.new-quote-product').length}"
|
||||
type="submit">{{( pgettext "Add products" "action" )}}</button>
|
||||
<button formnovalidate
|
||||
id="recompute-button"
|
||||
name="action" value="update"
|
||||
type="submit">{{( pgettext "Update" "action" )}}</button>
|
||||
<button class="primary" name="_method" value="PUT"
|
||||
formaction="{{ companyURI "/quotes" }}/{{ .Slug }}"
|
||||
type="submit">{{( pgettext "Save" "action" )}}</button>
|
||||
</fieldset>
|
||||
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.body.addEventListener('recompute', function () {
|
||||
document.getElementById('recompute-button').click();
|
||||
});
|
||||
</script>
|
||||
{{- end }}
|
|
@ -0,0 +1,142 @@
|
|||
{{ define "title" -}}
|
||||
{{( pgettext "Quotations" "title" )}}
|
||||
{{- end }}
|
||||
|
||||
{{ define "breadcrumbs" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.QuotesIndexPage*/ -}}
|
||||
<nav data-hx-target="main" data-hx-boost="true">
|
||||
<p>
|
||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||
<a>{{( pgettext "Quotations" "title" )}}</a>
|
||||
</p>
|
||||
|
||||
<form id="batch-form" action="{{ companyURI "/quotes/batch" }}" method="post">
|
||||
{{ csrfToken }}
|
||||
<p>
|
||||
{{ template "filters-toggle" }}
|
||||
<button type="submit"
|
||||
name="action" value="download"
|
||||
>{{( pgettext "Download quotations" "action" )}}</button>
|
||||
<a class="primary button"
|
||||
href="{{ companyURI "/quotes/new" }}">{{( pgettext "New quotation" "action" )}}</a>
|
||||
</p>
|
||||
</form>
|
||||
</nav>
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.QuotesIndexPage*/ -}}
|
||||
<form class="filters" method="GET" action="{{ companyURI "/quotes"}}"
|
||||
data-hx-target="main" data-hx-boost="true" data-hx-trigger="change,search,submit"
|
||||
aria-labelledby="filters-toggle"
|
||||
>
|
||||
{{ with .Filters }}
|
||||
{{ template "select-field" .Customer }}
|
||||
{{ template "select-field" .QuoteStatus }}
|
||||
{{ template "input-field" .FromDate }}
|
||||
{{ template "input-field" .ToDate }}
|
||||
{{ template "input-field" .QuoteNumber }}
|
||||
{{ template "tags-field" .Tags | addTagsAttr (print `data-conditions="` .TagsCondition.Name `-field"`) }}
|
||||
{{ template "toggle-field" .TagsCondition }}
|
||||
{{ end }}
|
||||
<noscript>
|
||||
<button type="submit">{{( pgettext "Filter" "action" )}}</button>
|
||||
</noscript>
|
||||
</form>
|
||||
<table id="quote-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{( pgettext "All" "quote" )}}</th>
|
||||
<th>{{( pgettext "Date" "title" )}}</th>
|
||||
<th>{{( pgettext "Quotation Num." "title" )}}</th>
|
||||
<th>{{( pgettext "Customer" "title" )}}</th>
|
||||
<th>{{( pgettext "Status" "title" )}}</th>
|
||||
<th>{{( pgettext "Tags" "title" )}}</th>
|
||||
<th>{{( pgettext "Amount" "title" )}}</th>
|
||||
<th>{{( pgettext "Download" "title" )}}</th>
|
||||
<th>{{( pgettext "Actions" "title" )}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ with .Quotes }}
|
||||
{{- range $quote := . }}
|
||||
<tr>
|
||||
{{ $title := .Number | printf (pgettext "Select quotation %v" "action") }}
|
||||
<td><input type="checkbox" form="batch-form"
|
||||
name="quote" value="{{ .Slug }}"
|
||||
aria-label="{{ $title }}"
|
||||
title="{{ $title }}"/></td>
|
||||
<td>{{ .Date|formatDate }}</td>
|
||||
<td><a href="{{ companyURI "/quotes/"}}{{ .Slug }}" data-hx-target="main"
|
||||
data-hx-boost="true">{{ .Number }}</a></td>
|
||||
<td>{{ .CustomerName }}</td>
|
||||
<td>
|
||||
<details class="quote-status menu">
|
||||
<summary class="quote-status-{{ .Status }}">{{ .StatusLabel }}</summary>
|
||||
<form action="{{companyURI "/quotes/"}}{{ .Slug }}" method="POST" data-hx-boost="true">
|
||||
{{ csrfToken }}
|
||||
{{ putMethod }}
|
||||
<input type="hidden" name="quick" value="status">
|
||||
<ul role="menu">
|
||||
{{- range $status, $name := $.QuoteStatuses }}
|
||||
{{- if ne $status $quote.Status }}
|
||||
<li role="presentation">
|
||||
<button role="menuitem" type="submit"
|
||||
name="quote_status" value="{{ $status }}"
|
||||
class="quote-status-{{ $status }}"
|
||||
>{{ $name }}</button>
|
||||
</li>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</ul>
|
||||
</form>
|
||||
</details>
|
||||
</td>
|
||||
<td data-hx-get="{{companyURI "/quotes/"}}{{ .Slug }}/tags/edit"
|
||||
data-hx-target="this"
|
||||
data-hx-swap="outerHTML"
|
||||
>
|
||||
{{- range $index, $tag := .Tags }}
|
||||
{{- if gt $index 0 }}, {{ end -}}
|
||||
{{ . }}
|
||||
{{- end }}
|
||||
</td>
|
||||
<td class="numeric">{{ .Total|formatPrice }}</td>
|
||||
<td class="quote-download"><a href="{{ companyURI "/quotes/"}}{{ .Slug }}.pdf"
|
||||
download="{{ .Number}}.pdf"
|
||||
title="{{( pgettext "Download quotation" "action" )}}"
|
||||
aria-label="{{( pgettext "Download quotation" "action" )}}"><i
|
||||
class="ri-download-line"></i></a></td>
|
||||
<td class="actions">
|
||||
<details class="menu">
|
||||
<summary><i class="ri-more-line"></i></summary>
|
||||
<ul role="menu" class="action-menu">
|
||||
<li role="presentation">
|
||||
<a role="menuitem" href="{{ companyURI "/quotes"}}/{{ .Slug }}/edit"
|
||||
data-hx-target="main" data-hx-boost="true"
|
||||
>
|
||||
<i class="ri-edit-line"></i>
|
||||
{{( pgettext "Edit" "action" )}}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a role="menuitem" href="{{ companyURI "/quotes/new"}}?duplicate={{ .Slug }}"
|
||||
data-hx-target="main" data-hx-boost="true"
|
||||
>
|
||||
<i class="ri-file-copy-line"></i>
|
||||
{{( pgettext "Duplicate" "action" )}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td colspan="9">{{( gettext "No quotations added yet." )}}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{- end }}
|
|
@ -0,0 +1,101 @@
|
|||
{{ define "title" -}}
|
||||
{{( pgettext "New Quotation" "title" )}}
|
||||
{{- end }}
|
||||
|
||||
{{ define "breadcrumbs" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newQuotePage*/ -}}
|
||||
<nav data-hx-target="main" data-hx-boost="true">
|
||||
<p>
|
||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||
<a href="{{ companyURI "/quotes"}}">{{( pgettext "Quotations" "title" )}}</a> /
|
||||
<a>{{( pgettext "New Quotation" "title" )}}</a>
|
||||
</p>
|
||||
</nav>
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newQuotePage*/ -}}
|
||||
<section id="quote-dialog-content" data-hx-target="main">
|
||||
<h2>{{(pgettext "New Quotation" "title")}}</h2>
|
||||
<form method="POST" action="{{ companyURI "/quotes/new" }}" data-hx-boost="true">
|
||||
{{ csrfToken }}
|
||||
|
||||
{{ with .Form -}}
|
||||
{{ if .RemovedProduct -}}
|
||||
<div role="alert">
|
||||
{{ with .RemovedProduct -}}
|
||||
<p>{{printf (gettext "Product “%s” removed") .Name}}</p>
|
||||
<button type="submit"
|
||||
formnovalidate
|
||||
name="action" value="restore-product"
|
||||
>{{( pgettext "Undo" "action" )}}</button>
|
||||
{{ template "hidden-field" .QuoteProductId }}
|
||||
{{ template "hidden-field" .ProductId }}
|
||||
{{ template "hidden-field" .Name }}
|
||||
{{ template "hidden-field" .Price }}
|
||||
{{ template "hidden-field" .Quantity }}
|
||||
{{ template "hidden-field" .Discount }}
|
||||
{{ template "hidden-field" .Description }}
|
||||
{{ template "hidden-select-field" .Tax }}
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
|
||||
<div class="quote-data">
|
||||
{{ template "hidden-select-field" .QuoteStatus }}
|
||||
{{ template "select-field" .Customer }}
|
||||
{{ template "input-field" .Date }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
{{ template "select-field" .PaymentMethod }}
|
||||
{{ template "input-field" .TermsAndConditions }}
|
||||
{{ template "input-field" .Notes }}
|
||||
</div>
|
||||
{{- range $product := .Products }}
|
||||
{{ template "quote-product-form" . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
<table id="quote-summary">
|
||||
<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 class="button-bar">
|
||||
<button formnovalidate
|
||||
name="action" value="select-products"
|
||||
data-hx-get="{{ companyURI "/quotes/product-form" }}"
|
||||
data-hx-target="#quote-summary" data-hx-swap="beforebegin"
|
||||
data-hx-select="unset"
|
||||
data-hx-vals="js:{index: document.querySelectorAll('.new-quote-product').length}"
|
||||
type="submit">{{( pgettext "Add products" "action" )}}</button>
|
||||
<button formnovalidate
|
||||
id="recompute-button"
|
||||
name="action" value="update"
|
||||
type="submit">{{( pgettext "Update" "action" )}}</button>
|
||||
<button class="primary" name="action" value="add"
|
||||
formaction="{{ companyURI "/quotes" }}"
|
||||
type="submit">{{( pgettext "Save" "action" )}}</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.body.addEventListener('recompute', function () {
|
||||
document.getElementById('recompute-button').click();
|
||||
});
|
||||
</script>
|
||||
{{- end }}
|
|
@ -0,0 +1,4 @@
|
|||
{{ define "content" }}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.quoteProductForm*/ -}}
|
||||
{{ template "quote-product-form" . }}
|
||||
{{- end }}
|
|
@ -0,0 +1,76 @@
|
|||
{{ define "title" -}}
|
||||
{{( pgettext "Add Products to Quotation" "title" )}}
|
||||
{{- end }}
|
||||
|
||||
{{ define "breadcrumbs" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newQuoteProductsPage*/ -}}
|
||||
<nav data-hx-target="main" data-hx-boost="true">
|
||||
<p>
|
||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||
<a href="{{ companyURI "/quotes"}}">{{( pgettext "Quotations" "title" )}}</a> /
|
||||
{{ if eq .Form.Number "" }}
|
||||
<a>{{( pgettext "New Quotation" "title" )}}</a>
|
||||
{{ else }}
|
||||
<a>{{ .Form.Number }}</a>
|
||||
{{ end }}
|
||||
</p>
|
||||
</nav>
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newQuotationProductsPage*/ -}}
|
||||
<section id="quote-dialog-content" data-hx-target="this">
|
||||
<h2>{{(pgettext "Add Products to Quotation" "title")}}</h2>
|
||||
<form method="POST" action="{{ .Action }}" data-hx-boost="true" data-hx-select="#quote-dialog-content">
|
||||
{{ csrfToken }}
|
||||
|
||||
{{- with .Form }}
|
||||
{{ template "hidden-select-field" .Customer }}
|
||||
{{ template "hidden-field" .Date }}
|
||||
{{ template "hidden-field" .Notes }}
|
||||
{{ template "hidden-field" .Tags }}
|
||||
|
||||
{{- range $product := .Products }}
|
||||
{{ template "hidden-field" .QuoteProductId }}
|
||||
{{ template "hidden-field" .ProductId }}
|
||||
{{ template "hidden-field" .Name }}
|
||||
{{ template "hidden-field" .Description }}
|
||||
{{ template "hidden-field" .Price }}
|
||||
{{ template "hidden-field" .Quantity }}
|
||||
{{ template "hidden-field" .Discount }}
|
||||
{{ template "hidden-select-field" .Tax }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{( pgettext "All" "product" )}}</th>
|
||||
<th>{{( pgettext "Name" "title" )}}</th>
|
||||
<th>{{( pgettext "Price" "title" )}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ with .Products }}
|
||||
{{- range . }}
|
||||
<tr>
|
||||
<td><input type="checkbox" name="slug" id="product-{{ .Slug }}" value="{{.Slug}}"></td>
|
||||
<td><label for="product-{{ .Slug }}">{{ .Name }}</label></td>
|
||||
<td class="numeric">{{ .Price | formatPrice }}</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td colspan="4">{{( gettext "No products added yet." )}}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<fieldset>
|
||||
<button class="primary" type="submit"
|
||||
name="action" value="add-products">{{( pgettext "Add products" "action" )}}</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
{{- end }}
|
|
@ -0,0 +1,126 @@
|
|||
{{ define "title" -}}
|
||||
{{ .Number | printf ( pgettext "Quotation %s" "title" )}}
|
||||
{{- end }}
|
||||
|
||||
{{ define "breadcrumbs" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.quote*/ -}}
|
||||
<nav>
|
||||
<p data-hx-target="main" data-hx-boost="true">
|
||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||
<a href="{{ companyURI "/quotes"}}">{{( pgettext "Quotations" "title" )}}</a> /
|
||||
<a>{{ .Number }}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="button primary"
|
||||
data-hx-target="main" data-hx-boost="true"
|
||||
href="{{ companyURI "/quotes/new"}}?duplicate={{ .Slug }}">{{( pgettext "Duplicate" "action" )}}</a>
|
||||
<a class="button primary"
|
||||
data-hx-target="main" data-hx-boost="true"
|
||||
href="{{ companyURI "/quotes/"}}{{ .Slug }}/edit">{{( pgettext "Edit" "action" )}}</a>
|
||||
<a class="primary button"
|
||||
href="{{ companyURI "/quotes/" }}{{ .Slug }}.pdf"
|
||||
download="{{ .Number}}.pdf">{{( pgettext "Download quotation" "action" )}}</a>
|
||||
</p>
|
||||
</nav>
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.quote*/ -}}
|
||||
<link rel="stylesheet" type="text/css" href="/static/invoice.css">
|
||||
<article class="invoice">
|
||||
<header>
|
||||
<div>
|
||||
<h1>{{ .Number | printf ( pgettext "Quotation %s" "title" )}}</h1>
|
||||
<p class="date">{{( pgettext "Date" "title" )}} {{ .Date | formatDate }}</p>
|
||||
</div>
|
||||
|
||||
<address class="quoter">
|
||||
{{ .Quoter.Name }}<br>
|
||||
{{ .Quoter.VATIN }}<br>
|
||||
{{ .Quoter.Address }}<br>
|
||||
{{ .Quoter.City }} ({{ .Quoter.PostalCode}}), {{ .Quoter.Province }}<br>
|
||||
{{ .Quoter.Email }}<br>
|
||||
{{ .Quoter.Phone }}<br>
|
||||
</address>
|
||||
|
||||
<p class="legal">{{ .LegalDisclaimer }}</p>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
<address class="quotee">
|
||||
{{ .Quotee.Name }}<br>
|
||||
{{ .Quotee.VATIN }}<br>
|
||||
{{ .Quotee.Address }}<br>
|
||||
{{ .Quotee.City }} ({{ .Quotee.PostalCode}}), {{ .Quotee.Province }}<br>
|
||||
</address>
|
||||
|
||||
{{- $columns := 5 | add (len .TaxClasses) | add (boolToInt .HasDiscounts) -}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{( pgettext "Concept" "title" )}}</th>
|
||||
<th class="numeric">{{( pgettext "Price" "title" )}}</th>
|
||||
{{ if .HasDiscounts -}}
|
||||
<th class="numeric">{{( pgettext "Discount" "title" )}}</th>
|
||||
{{ end -}}
|
||||
<th class="numeric">{{( pgettext "Units" "title" )}}</th>
|
||||
<th class="numeric">{{( pgettext "Subtotal" "title" )}}</th>
|
||||
{{ range $class := .TaxClasses -}}
|
||||
<th class="numeric">{{ . }}</th>
|
||||
{{ end -}}
|
||||
<th class="numeric">{{( pgettext "Total" "title" )}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{ $lastIndex := len .Products | sub 1 }}
|
||||
{{ range $index, $product := .Products -}}
|
||||
<tbody>
|
||||
{{- if .Description }}
|
||||
<tr class="name">
|
||||
<td colspan="{{ $columns }}">{{ .Name }}</td>
|
||||
</tr>
|
||||
{{ end -}}
|
||||
<tr>
|
||||
{{- if .Description }}
|
||||
<td>{{ .Description }}</td>
|
||||
{{- else }}
|
||||
<td>{{ .Name }}</td>
|
||||
{{- end -}}
|
||||
<td class="numeric">{{ .Price | formatPrice }}</td>
|
||||
{{ if $.HasDiscounts -}}
|
||||
<td class="numeric">{{ $product.Discount | formatPercent }}</td>
|
||||
{{ end -}}
|
||||
<td class="numeric">{{ .Quantity }}</td>
|
||||
<td class="numeric">{{ .Subtotal | formatPrice }}</td>
|
||||
{{ range $class := $.TaxClasses -}}
|
||||
<td class="numeric">{{ index $product.Taxes $class | formatPercent }}</td>
|
||||
{{ end -}}
|
||||
<td class="numeric">{{ .Total | formatPrice }}</td>
|
||||
</tr>
|
||||
{{ if (eq $index $lastIndex) }}
|
||||
<tr class="tfoot separator">
|
||||
<th scope="row" colspan="{{ $columns | sub 1 }}">{{( pgettext "Tax Base" "title" )}}</th>
|
||||
<td class="numeric">{{ $.Subtotal | formatPrice }}</td>
|
||||
</tr>
|
||||
{{ range $tax := $.Taxes -}}
|
||||
<tr class="tfoot">
|
||||
<th scope="row" colspan="{{ $columns | sub 1 }}">{{ index . 0 }}</th>
|
||||
<td class="numeric">{{ index . 1 | formatPrice }}</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
<tr class="tfoot">
|
||||
<th scope="row" colspan="{{ $columns | sub 1 }}">{{( pgettext "Total" "title" )}}</th>
|
||||
<td class="numeric">{{ $.Total | formatPrice }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
{{- end }}
|
||||
</table>
|
||||
|
||||
{{ if .Notes -}}
|
||||
<p class="notes">{{ .Notes }}</p>
|
||||
{{- end }}
|
||||
<p class="payment-instructions">{{ .PaymentInstructions }}</p>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
{{- end}}
|
Loading…
Reference in New Issue