Compare commits

..

3 Commits

Author SHA1 Message Date
jordi fita mas b48a974086 Add expenses statuses
We only want two statuses for expense: not yet paid (pending), and paid.
Thus, it is a bit different from quotes and invoices, because expenses
do not pass throw the “workflow” of created→sent→{pending,paid}. That’s
way in this case the status field is already in the new expense form,
instead of hidden, and by pending is not equivalent to created but
unpaid (i.e., the same status color).

With the new select field in the form, the file field no longer can
span two columns or it would be alone on the next row.

Closes #67.
2023-07-11 15:33:26 +02:00
jordi fita mas b7578a56df Refactor getting the invoice status options in a single function 2023-07-11 15:26:23 +02:00
jordi fita mas fa97f53dd7 Change link color to full blue
This is mostly to have better contrast with the alternative row color
(#EDE95E), because with Firefox’s default link color (#1B6ACB), on Linux
at least, the contrast ratio is 4,5:1.

Closes #62.
2023-07-11 13:52:04 +02:00
36 changed files with 693 additions and 103 deletions

View File

@ -8,24 +8,27 @@
-- requires: parse_price -- requires: parse_price
-- requires: tax -- requires: tax
-- requires: tag_name -- requires: tag_name
-- requires: expense_status
-- requires: expense_expense_status
begin; begin;
set search_path to numerus, public; set search_path to numerus, public;
create or replace function add_expense(company integer, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as create or replace function add_expense(company integer, status text, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$ $$
declare declare
eid integer; eid integer;
eslug uuid; eslug uuid;
begin begin
insert into expense (company_id, contact_id, invoice_number, invoice_date, amount, currency_code, tags) insert into expense (company_id, contact_id, invoice_number, invoice_date, amount, currency_code, expense_status, tags)
select company_id select company_id
, contact_id , contact_id
, invoice_number , invoice_number
, invoice_date , invoice_date
, parse_price(amount, currency.decimal_digits) , parse_price(amount, currency.decimal_digits)
, currency_code , currency_code
, status
, tags , tags
from company from company
join currency using (currency_code) join currency using (currency_code)
@ -43,8 +46,10 @@ end;
$$ $$
language plpgsql; language plpgsql;
revoke execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) from public; revoke execute on function add_expense(integer, text, date, integer, text, text, integer[], tag_name[]) from public;
grant execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) to invoicer; grant execute on function add_expense(integer, text, date, integer, text, text, integer[], tag_name[]) to invoicer;
grant execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) to admin; grant execute on function add_expense(integer, text, date, integer, text, text, integer[], tag_name[]) to admin;
drop function if exists add_expense(integer, date, integer, text, text, integer[], tag_name[]);
commit; commit;

50
deploy/add_expense@v1.sql Normal file
View File

@ -0,0 +1,50 @@
-- Deploy numerus:add_expense to pg
-- requires: schema_numerus
-- requires: expense
-- requires: expense_tax
-- requires: tax
-- requires: company
-- requires: currency
-- requires: parse_price
-- requires: tax
-- requires: tag_name
begin;
set search_path to numerus, public;
create or replace function add_expense(company integer, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$
declare
eid integer;
eslug uuid;
begin
insert into expense (company_id, contact_id, invoice_number, invoice_date, amount, currency_code, tags)
select company_id
, contact_id
, invoice_number
, invoice_date
, parse_price(amount, currency.decimal_digits)
, currency_code
, tags
from company
join currency using (currency_code)
where company.company_id = add_expense.company
returning expense_id, slug
into eid, eslug;
insert into expense_tax (expense_id, tax_id, tax_rate)
select eid, tax_id, tax.rate
from tax
join unnest(taxes) as etax(tax_id) using (tax_id);
return eslug;
end;
$$
language plpgsql;
revoke execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) from public;
grant execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) to invoicer;
grant execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) to admin;
commit;

View File

@ -0,0 +1,22 @@
-- Deploy numerus:available_expense_status to pg
-- requires: schema_numerus
-- requires: expense_status
-- requires: expense_status_i18n
begin;
set search_path to numerus;
insert into expense_status (expense_status, name)
values ('pending', 'Pending')
, ('paid', 'Paid')
;
insert into expense_status_i18n (expense_status, lang_tag, name)
values ('pending', 'ca', 'Pendent')
, ('paid', 'ca', 'Pagada')
, ('pending', 'es', 'Pendiente')
, ('paid', 'es', 'Pagada')
;
commit;

View File

@ -5,12 +5,14 @@
-- requires: parse_price -- requires: parse_price
-- requires: tax -- requires: tax
-- requires: tag_name -- requires: tag_name
-- requires: expense_status
-- requires: expense_expense_status
begin; begin;
set search_path to numerus, public; set search_path to numerus, public;
create or replace function edit_expense(expense_slug uuid, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as create or replace function edit_expense(expense_slug uuid, status text, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$ $$
declare declare
eid integer; eid integer;
@ -20,6 +22,7 @@ begin
, contact_id = edit_expense.contact_id , contact_id = edit_expense.contact_id
, invoice_number = edit_expense.invoice_number , invoice_number = edit_expense.invoice_number
, amount = parse_price(edit_expense.amount, decimal_digits) , amount = parse_price(edit_expense.amount, decimal_digits)
, expense_status = status
, tags = edit_expense.tags , tags = edit_expense.tags
from currency from currency
where slug = expense_slug where slug = expense_slug
@ -43,8 +46,8 @@ end;
$$ $$
language plpgsql; language plpgsql;
revoke execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) from public; revoke execute on function edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]) from public;
grant execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) to invoicer; grant execute on function edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]) to invoicer;
grant execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) to admin; grant execute on function edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]) to admin;
commit; commit;

View File

@ -0,0 +1,50 @@
-- Deploy numerus:edit_expense to pg
-- requires: schema_numerus
-- requires: expense
-- requires: currency
-- requires: parse_price
-- requires: tax
-- requires: tag_name
begin;
set search_path to numerus, public;
create or replace function edit_expense(expense_slug uuid, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$
declare
eid integer;
begin
update expense
set invoice_date = edit_expense.invoice_date
, contact_id = edit_expense.contact_id
, invoice_number = edit_expense.invoice_number
, amount = parse_price(edit_expense.amount, decimal_digits)
, tags = edit_expense.tags
from currency
where slug = expense_slug
and currency.currency_code = expense.currency_code
returning expense_id
into eid;
if eid is null then
return null;
end if;
delete from expense_tax where expense_id = eid;
insert into expense_tax (expense_id, tax_id, tax_rate)
select eid, tax_id, tax.rate
from tax
join unnest(taxes) as etax(tax_id) using (tax_id);
return expense_slug;
end;
$$
language plpgsql;
revoke execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) from public;
grant execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) to invoicer;
grant execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) to admin;
commit;

View File

@ -0,0 +1,12 @@
-- Deploy numerus:expense_expense_status to pg
-- requires: expense
begin;
set search_path to numerus, public;
alter table expense
add column expense_status text not null default 'pending' references expense_status
;
commit;

17
deploy/expense_status.sql Normal file
View File

@ -0,0 +1,17 @@
-- Deploy numerus:expense_status to pg
-- requires: schema_numerus
-- requires: roles
begin;
set search_path to numerus, public;
create table expense_status (
expense_status text primary key,
name text not null
);
grant select on table expense_status to invoicer;
grant select on table expense_status to admin;
commit;

View File

@ -0,0 +1,21 @@
-- Deploy numerus:expense_status_i18n to pg
-- requires: schema_numerus
-- requires: roles
-- requires: expense_status
-- requires: language
begin;
set search_path to numerus, public;
create table expense_status_i18n (
expense_status text not null references expense_status,
lang_tag text not null references language,
name text not null,
primary key (expense_status, lang_tag)
);
grant select on table expense_status_i18n to invoicer;
grant select on table expense_status_i18n to admin;
commit;

View File

@ -20,12 +20,15 @@ type ExpenseEntry struct {
InvoicerName string InvoicerName string
OriginalFileName string OriginalFileName string
Tags []string Tags []string
Status string
StatusLabel string
} }
type expensesIndexPage struct { type expensesIndexPage struct {
Expenses []*ExpenseEntry Expenses []*ExpenseEntry
TotalAmount string TotalAmount string
Filters *expenseFilterForm Filters *expenseFilterForm
ExpenseStatuses map[string]string
} }
func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@ -38,15 +41,16 @@ func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
return return
} }
page := &expensesIndexPage{ page := &expensesIndexPage{
Expenses: mustCollectExpenseEntries(r.Context(), conn, filters), Expenses: mustCollectExpenseEntries(r.Context(), conn, locale, filters),
TotalAmount: mustComputeExpensesTotalAmount(r.Context(), conn, filters), TotalAmount: mustComputeExpensesTotalAmount(r.Context(), conn, filters),
ExpenseStatuses: mustCollectExpenseStatuses(r.Context(), conn, locale),
Filters: filters, Filters: filters,
} }
mustRenderMainTemplate(w, r, "expenses/index.gohtml", page) mustRenderMainTemplate(w, r, "expenses/index.gohtml", page)
} }
func mustCollectExpenseEntries(ctx context.Context, conn *Conn, filters *expenseFilterForm) []*ExpenseEntry { func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale, filters *expenseFilterForm) []*ExpenseEntry {
where, args := filters.BuildQuery(nil) where, args := filters.BuildQuery([]interface{}{locale.Language.String()})
rows := conn.MustQuery(ctx, fmt.Sprintf(` rows := conn.MustQuery(ctx, fmt.Sprintf(`
select expense.slug select expense.slug
, invoice_date , invoice_date
@ -55,9 +59,12 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, filters *expense
, contact.name , contact.name
, coalesce(attachment.original_filename, '') , coalesce(attachment.original_filename, '')
, expense.tags , expense.tags
, expense.expense_status
, esi18n.name
from expense from expense
left join expense_attachment as attachment using (expense_id) left join expense_attachment as attachment using (expense_id)
join contact using (contact_id) join contact using (contact_id)
join expense_status_i18n esi18n on expense.expense_status = esi18n.expense_status and esi18n.lang_tag = $1
join currency using (currency_code) join currency using (currency_code)
where (%s) where (%s)
order by invoice_date order by invoice_date
@ -67,7 +74,7 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, filters *expense
var entries []*ExpenseEntry var entries []*ExpenseEntry
for rows.Next() { for rows.Next() {
entry := &ExpenseEntry{} entry := &ExpenseEntry{}
if err := rows.Scan(&entry.Slug, &entry.InvoiceDate, &entry.InvoiceNumber, &entry.Amount, &entry.InvoicerName, &entry.OriginalFileName, &entry.Tags); err != nil { if err := rows.Scan(&entry.Slug, &entry.InvoiceDate, &entry.InvoiceNumber, &entry.Amount, &entry.InvoicerName, &entry.OriginalFileName, &entry.Tags, &entry.Status, &entry.StatusLabel); err != nil {
panic(err) panic(err)
} }
entries = append(entries, entry) entries = append(entries, entry)
@ -79,6 +86,31 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, filters *expense
return entries return entries
} }
func mustCollectExpenseStatuses(ctx context.Context, conn *Conn, locale *Locale) map[string]string {
rows := conn.MustQuery(ctx, `
select expense_status.expense_status
, esi18n.name
from expense_status
join expense_status_i18n esi18n using(expense_status)
where esi18n.lang_tag = $1
order by expense_status`, locale.Language.String())
defer rows.Close()
statuses := map[string]string{}
for rows.Next() {
var key, name string
if err := rows.Scan(&key, &name); err != nil {
panic(err)
}
statuses[key] = name
}
if rows.Err() != nil {
panic(rows.Err())
}
return statuses
}
func mustComputeExpensesTotalAmount(ctx context.Context, conn *Conn, filters *expenseFilterForm) string { func mustComputeExpensesTotalAmount(ctx context.Context, conn *Conn, filters *expenseFilterForm) string {
where, args := filters.BuildQuery(nil) where, args := filters.BuildQuery(nil)
return conn.MustGetText(ctx, "0", fmt.Sprintf(` return conn.MustGetText(ctx, "0", fmt.Sprintf(`
@ -138,6 +170,7 @@ type expenseForm struct {
Tax *SelectField Tax *SelectField
Amount *InputField Amount *InputField
File *FileField File *FileField
ExpenseStatus *SelectField
Tags *TagsField Tags *TagsField
} }
@ -183,6 +216,13 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Label: pgettext("input", "File", locale), Label: pgettext("input", "File", locale),
MaxSize: 1 << 20, MaxSize: 1 << 20,
}, },
ExpenseStatus: &SelectField{
Name: "expense_status",
Required: true,
Label: pgettext("input", "Expense Status", locale),
Selected: []string{"pending"},
Options: mustGetExpenseStatusOptions(ctx, conn, locale),
},
Tags: &TagsField{ Tags: &TagsField{
Name: "tags", Name: "tags",
Label: pgettext("input", "Tags", locale), Label: pgettext("input", "Tags", locale),
@ -190,6 +230,16 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
} }
} }
func mustGetExpenseStatusOptions(ctx context.Context, conn *Conn, locale *Locale) []*SelectOption {
return MustGetOptions(ctx, conn, `
select expense_status.expense_status
, esi18n.name
from expense_status
join expense_status_i18n esi18n using(expense_status)
where esi18n.lang_tag = $1
order by expense_status`, locale.Language.String())
}
func (form *expenseForm) Parse(r *http.Request) error { func (form *expenseForm) Parse(r *http.Request) error {
if err := r.ParseMultipartForm(form.File.MaxSize); err != nil { if err := r.ParseMultipartForm(form.File.MaxSize); err != nil {
return err return err
@ -202,6 +252,7 @@ func (form *expenseForm) Parse(r *http.Request) error {
if err := form.File.FillValue(r); err != nil { if err := form.File.FillValue(r); err != nil {
return err return err
} }
form.ExpenseStatus.FillValue(r)
form.Tags.FillValue(r) form.Tags.FillValue(r)
return nil return nil
} }
@ -217,16 +268,20 @@ func (form *expenseForm) Validate() bool {
} }
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale)) validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale)) validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
validator.CheckValidSelectOption(form.ExpenseStatus, gettext("Selected expense status is not valid.", form.locale))
return validator.AllOK() return validator.AllOK()
} }
func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool { func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
return !notFoundErrorOrPanic(conn.QueryRow(ctx, ` selectedExpenseStatus := form.ExpenseStatus.Selected
form.ExpenseStatus.Clear()
if notFoundErrorOrPanic(conn.QueryRow(ctx, `
select contact_id select contact_id
, invoice_number , invoice_number
, invoice_date , invoice_date
, to_price(amount, decimal_digits) , to_price(amount, decimal_digits)
, array_agg(tax_id) , array_agg(tax_id)
, expense_status
, tags , tags
from expense from expense
left join expense_tax using (expense_id) left join expense_tax using (expense_id)
@ -237,6 +292,7 @@ func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
, invoice_date , invoice_date
, amount , amount
, decimal_digits , decimal_digits
, expense_status
, tags , tags
`, slug).Scan( `, slug).Scan(
form.Invoicer, form.Invoicer,
@ -244,7 +300,12 @@ func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
form.InvoiceDate, form.InvoiceDate,
form.Amount, form.Amount,
form.Tax, form.Tax,
form.Tags)) form.ExpenseStatus,
form.Tags)) {
form.ExpenseStatus.Selected = selectedExpenseStatus
return false
}
return true
} }
func HandleAddExpense(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func HandleAddExpense(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r) conn := getConn(r)
@ -267,7 +328,7 @@ func HandleAddExpense(w http.ResponseWriter, r *http.Request, _ httprouter.Param
return return
} }
taxes := mustSliceAtoi(form.Tax.Selected) taxes := mustSliceAtoi(form.Tax.Selected)
slug := conn.MustGetText(r.Context(), "", "select add_expense($1, $2, $3, $4, $5, $6, $7)", company.Id, form.InvoiceDate, form.Invoicer, form.InvoiceNumber, form.Amount, taxes, form.Tags) slug := conn.MustGetText(r.Context(), "", "select add_expense($1, $2, $3, $4, $5, $6, $7, $8)", company.Id, form.ExpenseStatus, form.InvoiceDate, form.Invoicer, form.InvoiceNumber, form.Amount, taxes, form.Tags)
if len(form.File.Content) > 0 { if len(form.File.Content) > 0 {
conn.MustQuery(r.Context(), "select attach_to_expense($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content) conn.MustQuery(r.Context(), "select attach_to_expense($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
} }
@ -287,6 +348,13 @@ func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprout
http.Error(w, err.Error(), http.StatusForbidden) http.Error(w, err.Error(), http.StatusForbidden)
return return
} }
if r.FormValue("quick") == "status" {
slug := conn.MustGetText(r.Context(), "", "update expense set expense_status = $1 where slug = $2 returning slug", form.ExpenseStatus, params[0].Value)
if slug == "" {
http.NotFound(w, r)
}
htmxRedirect(w, r, companyURI(mustGetCompany(r), "/expenses"))
} else {
slug := params[0].Value slug := params[0].Value
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { if !IsHTMxRequest(r) {
@ -296,7 +364,7 @@ func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprout
return return
} }
taxes := mustSliceAtoi(form.Tax.Selected) taxes := mustSliceAtoi(form.Tax.Selected)
if found := conn.MustGetText(r.Context(), "", "select edit_expense($1, $2, $3, $4, $5, $6, $7)", slug, form.InvoiceDate, form.Invoicer, form.InvoiceNumber, form.Amount, taxes, form.Tags); found == "" { if found := conn.MustGetText(r.Context(), "", "select edit_expense($1, $2, $3, $4, $5, $6, $7, $8)", slug, form.ExpenseStatus, form.InvoiceDate, form.Invoicer, form.InvoiceNumber, form.Amount, taxes, form.Tags); found == "" {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@ -304,6 +372,7 @@ func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprout
conn.MustQuery(r.Context(), "select attach_to_expense($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content) conn.MustQuery(r.Context(), "select attach_to_expense($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
} }
htmxRedirect(w, r, companyURI(company, "/expenses")) htmxRedirect(w, r, companyURI(company, "/expenses"))
}
} }
type expenseFilterForm struct { type expenseFilterForm struct {
@ -313,6 +382,7 @@ type expenseFilterForm struct {
InvoiceNumber *InputField InvoiceNumber *InputField
FromDate *InputField FromDate *InputField
ToDate *InputField ToDate *InputField
ExpenseStatus *SelectField
Tags *TagsField Tags *TagsField
TagsCondition *ToggleField TagsCondition *ToggleField
} }
@ -346,6 +416,12 @@ func newExpenseFilterForm(ctx context.Context, conn *Conn, locale *Locale, compa
Name: "tags", Name: "tags",
Label: pgettext("input", "Tags", locale), Label: pgettext("input", "Tags", locale),
}, },
ExpenseStatus: &SelectField{
Name: "expense_status",
Label: pgettext("input", "Expense Status", locale),
EmptyLabel: gettext("All status", locale),
Options: mustGetExpenseStatusOptions(ctx, conn, locale),
},
TagsCondition: &ToggleField{ TagsCondition: &ToggleField{
Name: "tags_condition", Name: "tags_condition",
Label: pgettext("input", "Tags Condition", locale), Label: pgettext("input", "Tags Condition", locale),
@ -372,6 +448,7 @@ func (form *expenseFilterForm) Parse(r *http.Request) error {
form.InvoiceNumber.FillValue(r) form.InvoiceNumber.FillValue(r)
form.FromDate.FillValue(r) form.FromDate.FillValue(r)
form.ToDate.FillValue(r) form.ToDate.FillValue(r)
form.ExpenseStatus.FillValue(r)
form.Tags.FillValue(r) form.Tags.FillValue(r)
form.TagsCondition.FillValue(r) form.TagsCondition.FillValue(r)
return nil return nil
@ -398,6 +475,7 @@ func (form *expenseFilterForm) BuildQuery(args []interface{}) (string, []interfa
customerId, _ := strconv.Atoi(form.Contact.Selected[0]) customerId, _ := strconv.Atoi(form.Contact.Selected[0])
return customerId return customerId
}) })
maybeAppendWhere("expense.expense_status = $%d", form.ExpenseStatus.String(), nil)
maybeAppendWhere("invoice_number = $%d", form.InvoiceNumber.String(), nil) maybeAppendWhere("invoice_number = $%d", form.InvoiceNumber.String(), nil)
maybeAppendWhere("invoice_date >= $%d", form.FromDate.String(), nil) maybeAppendWhere("invoice_date >= $%d", form.FromDate.String(), nil)
maybeAppendWhere("invoice_date <= $%d", form.ToDate.String(), nil) maybeAppendWhere("invoice_date <= $%d", form.ToDate.String(), nil)

View File

@ -152,7 +152,7 @@ func newInvoiceFilterForm(ctx context.Context, conn *Conn, locale *Locale, compa
Name: "invoice_status", Name: "invoice_status",
Label: pgettext("input", "Invoice Status", locale), Label: pgettext("input", "Invoice Status", locale),
EmptyLabel: gettext("All status", locale), EmptyLabel: gettext("All status", locale),
Options: MustGetOptions(ctx, conn, "select invoice_status.invoice_status, isi18n.name from invoice_status join invoice_status_i18n isi18n using(invoice_status) where isi18n.lang_tag = $1 order by invoice_status", locale.Language.String()), Options: mustGetInvoiceStatusOptions(ctx, conn, locale),
}, },
InvoiceNumber: &InputField{ InvoiceNumber: &InputField{
Name: "number", Name: "number",
@ -691,7 +691,7 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Required: true, Required: true,
Label: pgettext("input", "Invoice Status", locale), Label: pgettext("input", "Invoice Status", locale),
Selected: []string{"created"}, Selected: []string{"created"},
Options: MustGetOptions(ctx, conn, "select invoice_status.invoice_status, isi18n.name from invoice_status join invoice_status_i18n isi18n using(invoice_status) where isi18n.lang_tag = $1 order by invoice_status", locale.Language.String()), Options: mustGetInvoiceStatusOptions(ctx, conn, locale),
}, },
Customer: &SelectField{ Customer: &SelectField{
Name: "customer", Name: "customer",
@ -724,6 +724,16 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
} }
} }
func mustGetInvoiceStatusOptions(ctx context.Context, conn *Conn, locale *Locale) []*SelectOption {
return MustGetOptions(ctx, conn, `
select invoice_status.invoice_status
, isi18n.name
from invoice_status
join invoice_status_i18n isi18n using(invoice_status)
where isi18n.lang_tag = $1
order by invoice_status`, locale.Language.String())
}
func (form *invoiceForm) Parse(r *http.Request) error { func (form *invoiceForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
return err return err

View File

@ -1,7 +1,52 @@
-- Revert numerus:add_expense from pg -- Deploy numerus:add_expense to pg
-- requires: schema_numerus
-- requires: expense
-- requires: expense_tax
-- requires: tax
-- requires: company
-- requires: currency
-- requires: parse_price
-- requires: tax
-- requires: tag_name
begin; begin;
drop function if exists numerus.add_expense(integer, date, integer, text, text, integer[], numerus.tag_name[]); set search_path to numerus, public;
drop function if exists add_expense(integer, text, date, integer, text, text, integer[], tag_name[]);
create or replace function add_expense(company integer, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$
declare
eid integer;
eslug uuid;
begin
insert into expense (company_id, contact_id, invoice_number, invoice_date, amount, currency_code, tags)
select company_id
, contact_id
, invoice_number
, invoice_date
, parse_price(amount, currency.decimal_digits)
, currency_code
, tags
from company
join currency using (currency_code)
where company.company_id = add_expense.company
returning expense_id, slug
into eid, eslug;
insert into expense_tax (expense_id, tax_id, tax_rate)
select eid, tax_id, tax.rate
from tax
join unnest(taxes) as etax(tax_id) using (tax_id);
return eslug;
end;
$$
language plpgsql;
revoke execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) from public;
grant execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) to invoicer;
grant execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) to admin;
commit; commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:add_expense from pg
begin;
drop function if exists numerus.add_expense(integer, date, integer, text, text, integer[], numerus.tag_name[]);
commit;

View File

@ -0,0 +1,10 @@
-- Revert numerus:available_expense_status from pg
begin;
set search_path to numerus;
delete from expense_status_i18n;
delete from expense_status;
commit;

View File

@ -1,7 +1,52 @@
-- Revert numerus:edit_expense from pg -- Deploy numerus:edit_expense to pg
-- requires: schema_numerus
-- requires: expense
-- requires: currency
-- requires: parse_price
-- requires: tax
-- requires: tag_name
begin; begin;
drop function if exists numerus.edit_expense(uuid, date, integer, text, text, integer[], numerus.tag_name[]); set search_path to numerus, public;
drop function if exists edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]);
create or replace function edit_expense(expense_slug uuid, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$
declare
eid integer;
begin
update expense
set invoice_date = edit_expense.invoice_date
, contact_id = edit_expense.contact_id
, invoice_number = edit_expense.invoice_number
, amount = parse_price(edit_expense.amount, decimal_digits)
, tags = edit_expense.tags
from currency
where slug = expense_slug
and currency.currency_code = expense.currency_code
returning expense_id
into eid;
if eid is null then
return null;
end if;
delete from expense_tax where expense_id = eid;
insert into expense_tax (expense_id, tax_id, tax_rate)
select eid, tax_id, tax.rate
from tax
join unnest(taxes) as etax(tax_id) using (tax_id);
return expense_slug;
end;
$$
language plpgsql;
revoke execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) from public;
grant execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) to invoicer;
grant execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) to admin;
commit; commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:edit_expense from pg
begin;
drop function if exists numerus.edit_expense(uuid, date, integer, text, text, integer[], numerus.tag_name[]);
commit;

View File

@ -0,0 +1,9 @@
-- Revert numerus:expense_expense_status from pg
begin;
alter table numerus.expense
drop column if exists expense_status
;
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:expense_status from pg
begin;
drop table if exists numerus.expense_status;
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:expense_status_i18n from pg
begin;
drop table if exists numerus.expense_status_i18n;
commit;

View File

@ -115,3 +115,10 @@ input_is_valid [schema_public roles] 2023-07-03T08:42:46Z jordi fita mas <jordi@
input_is_valid_phone [schema_public roles extension_pg_libphonenumber] 2023-07-03T08:59:36Z jordi fita mas <jordi@tandem.blog> # add function to validate phone number inputs input_is_valid_phone [schema_public roles extension_pg_libphonenumber] 2023-07-03T08:59:36Z jordi fita mas <jordi@tandem.blog> # add function to validate phone number inputs
import_contact [schema_numerus roles contact contact_web contact_phone contact_email contact_iban contact_swift contact_tax_details input_is_valid input_is_valid_phone] 2023-07-02T18:50:22Z jordi fita mas <jordi@tandem.blog> # Add functions to massively import customer data import_contact [schema_numerus roles contact contact_web contact_phone contact_email contact_iban contact_swift contact_tax_details input_is_valid input_is_valid_phone] 2023-07-02T18:50:22Z jordi fita mas <jordi@tandem.blog> # Add functions to massively import customer data
@v1 2023-07-03T09:32:51Z jordi fita mas <jordi@tandem.blog> # Tag version 1 @v1 2023-07-03T09:32:51Z jordi fita mas <jordi@tandem.blog> # Tag version 1
expense_status [schema_numerus roles] 2023-07-11T11:56:39Z jordi fita mas <jordi@tandem.blog> # Add the relation of expense status
expense_status_i18n [schema_numerus roles expense_status language] 2023-07-11T12:09:48Z jordi fita mas <jordi@tandem.blog> # Add relation for expense status translatable texts
available_expense_status [schema_numerus expense_status expense_status_i18n] 2023-07-11T12:13:45Z jordi fita mas <jordi@tandem.blog> # Add the list of available expense status
expense_expense_status [expense] 2023-07-11T12:28:58Z jordi fita mas <jordi@tandem.blog> # Add expense_status to expense relation
add_expense [add_expense@v1 expense_status expense_expense_status] 2023-07-11T13:16:16Z jordi fita mas <jordi@tandem.blog> # Add expense_status parameter to add_expense
edit_expense [edit_expense@v1 expense_status expense_expense_status] 2023-07-11T13:21:17Z jordi fita mas <jordi@tandem.blog> # Add expense_status parameter to edit_expense

View File

@ -9,15 +9,15 @@ select plan(14);
set search_path to auth, numerus, public; set search_path to auth, numerus, public;
select has_function('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']); select has_function('numerus', 'add_expense', array ['integer', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']);
select function_lang_is('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'plpgsql'); select function_lang_is('numerus', 'add_expense', array ['integer', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'plpgsql');
select function_returns('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'uuid'); select function_returns('numerus', 'add_expense', array ['integer', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'uuid');
select isnt_definer('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']); select isnt_definer('numerus', 'add_expense', array ['integer', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']);
select volatility_is('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'volatile'); select volatility_is('numerus', 'add_expense', array ['integer', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'volatile');
select function_privs_are('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'guest', array []::text[]); select function_privs_are('numerus', 'add_expense', array ['integer', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'invoicer', array ['EXECUTE']); select function_privs_are('numerus', 'add_expense', array ['integer', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'admin', array ['EXECUTE']); select function_privs_are('numerus', 'add_expense', array ['integer', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'authenticator', array []::text[]); select function_privs_are('numerus', 'add_expense', array ['integer', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'authenticator', array []::text[]);
set client_min_messages to warning; set client_min_messages to warning;
@ -64,25 +64,25 @@ values (12, 1, 'Contact 2.1')
select lives_ok( select lives_ok(
$$ select add_expense(1, '2023-05-02', 12, 'Invoice 1', '11.11', '{4}', '{tag1,tag2}') $$, $$ select add_expense(1, 'pending', '2023-05-02', 12, 'Invoice 1', '11.11', '{4}', '{tag1,tag2}') $$,
'Should be able to insert an expense for the first company with a tax' 'Should be able to insert an expense for the first company with a tax'
); );
select lives_ok( select lives_ok(
$$ select add_expense(1, '2023-05-03', 13, 'Invoice 2', '22.22', '{4,3}', '{}') $$, $$ select add_expense(1, 'paid', '2023-05-03', 13, 'Invoice 2', '22.22', '{4,3}', '{}') $$,
'Should be able to insert a second expense for the first company with two taxes' 'Should be able to insert a second expense for the first company with two taxes'
); );
select lives_ok( select lives_ok(
$$ select add_expense(2, '2023-05-04', 15, 'Invoice 3', '33.33', '{}', '{tag3}') $$, $$ select add_expense(2, 'pending', '2023-05-04', 15, 'Invoice 3', '33.33', '{}', '{tag3}') $$,
'Should be able to insert an invoice for the second company with no tax' 'Should be able to insert an invoice for the second company with no tax'
); );
select bag_eq( select bag_eq(
$$ select company_id, invoice_number, invoice_date, contact_id, amount, currency_code, tags, created_at from expense $$, $$ select company_id, invoice_number, invoice_date, contact_id, amount, currency_code, expense_status, tags, created_at from expense $$,
$$ values (1, 'Invoice 1', '2023-05-02'::date, 12, 1111, 'EUR', '{tag1,tag2}'::tag_name[], current_timestamp) $$ values (1, 'Invoice 1', '2023-05-02'::date, 12, 1111, 'EUR', 'pending', '{tag1,tag2}'::tag_name[], current_timestamp)
, (1, 'Invoice 2', '2023-05-03'::date, 13, 2222, 'EUR', '{}'::tag_name[], current_timestamp) , (1, 'Invoice 2', '2023-05-03'::date, 13, 2222, 'EUR', 'paid', '{}'::tag_name[], current_timestamp)
, (2, 'Invoice 3', '2023-05-04'::date, 15, 3333, 'USD', '{tag3}'::tag_name[], current_timestamp) , (2, 'Invoice 3', '2023-05-04'::date, 15, 3333, 'USD', 'pending', '{tag3}'::tag_name[], current_timestamp)
$$, $$,
'Should have created all expenses' 'Should have created all expenses'
); );

View File

@ -9,15 +9,15 @@ select plan(13);
set search_path to auth, numerus, public; set search_path to auth, numerus, public;
select has_function('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']); select has_function('numerus', 'edit_expense', array ['uuid', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']);
select function_lang_is('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'plpgsql'); select function_lang_is('numerus', 'edit_expense', array ['uuid', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'plpgsql');
select function_returns('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'uuid'); select function_returns('numerus', 'edit_expense', array ['uuid', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'uuid');
select isnt_definer('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']); select isnt_definer('numerus', 'edit_expense', array ['uuid', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']);
select volatility_is('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'volatile'); select volatility_is('numerus', 'edit_expense', array ['uuid', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'volatile');
select function_privs_are('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'guest', array []::text[]); select function_privs_are('numerus', 'edit_expense', array ['uuid', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'invoicer', array ['EXECUTE']); select function_privs_are('numerus', 'edit_expense', array ['uuid', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'admin', array ['EXECUTE']); select function_privs_are('numerus', 'edit_expense', array ['uuid', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'authenticator', array []::text[]); select function_privs_are('numerus', 'edit_expense', array ['uuid', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'authenticator', array []::text[]);
set client_min_messages to warning; set client_min_messages to warning;
@ -58,9 +58,9 @@ values (12, 1, 'Contact 2.1')
, (13, 1, 'Contact 2.2') , (13, 1, 'Contact 2.2')
; ;
insert into expense (expense_id, company_id, slug, invoice_number, invoice_date, contact_id, amount, currency_code, tags) insert into expense (expense_id, company_id, slug, invoice_number, invoice_date, contact_id, amount, currency_code, expense_status, tags)
values (15, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'INV1', '2023-05-04', 12, 111, 'EUR', '{tag1}') values (15, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'INV1', '2023-05-04', 12, 111, 'EUR', 'pending', '{tag1}')
, (16, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'INV2', '2023-05-05', 13, 222, 'EUR', '{tag2}') , (16, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'INV2', '2023-05-05', 13, 222, 'EUR', 'paid', '{tag2}')
; ;
insert into expense_tax (expense_id, tax_id, tax_rate) insert into expense_tax (expense_id, tax_id, tax_rate)
@ -70,19 +70,19 @@ values (15, 4, 0.21)
; ;
select lives_ok( select lives_ok(
$$ select edit_expense('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', '2023-05-06', 13, 'INV11', '1.12', '{4}', array['tag1']) $$, $$ select edit_expense('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'paid', '2023-05-06', 13, 'INV11', '1.12', '{4}', array['tag1']) $$,
'Should be able to edit the first expense' 'Should be able to edit the first expense'
); );
select lives_ok( select lives_ok(
$$ select edit_expense('b57b980b-247b-4be4-a0b7-03a7819c53ae', '2023-05-07', 12, 'INV22', '3.33', '{4,3}', array['tag1', 'tag3']) $$, $$ select edit_expense('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'pending', '2023-05-07', 12, 'INV22', '3.33', '{4,3}', array['tag1', 'tag3']) $$,
'Should be able to edit the second expense' 'Should be able to edit the second expense'
); );
select bag_eq( select bag_eq(
$$ select invoice_number, invoice_date, contact_id, amount, tags from expense $$, $$ select invoice_number, invoice_date, contact_id, amount, expense_status, tags from expense $$,
$$ values ('INV11', '2023-05-06'::date, 13, 112, '{tag1}'::tag_name[]) $$ values ('INV11', '2023-05-06'::date, 13, 112, 'paid', '{tag1}'::tag_name[])
, ('INV22', '2023-05-07'::date, 12, 333, '{tag1,tag3}'::tag_name[]) , ('INV22', '2023-05-07'::date, 12, 333, 'pending', '{tag1,tag3}'::tag_name[])
$$, $$,
'Should have updated all expenses' 'Should have updated all expenses'
); );

View File

@ -5,7 +5,7 @@ reset client_min_messages;
begin; begin;
select plan(67); select plan(74);
set search_path to numerus, auth, public; set search_path to numerus, auth, public;
@ -65,6 +65,14 @@ select col_type_is('expense', 'amount', 'integer');
select col_not_null('expense', 'amount'); select col_not_null('expense', 'amount');
select col_hasnt_default('expense', 'amount'); select col_hasnt_default('expense', 'amount');
select has_column('expense', 'expense_status');
select col_is_fk('expense', 'expense_status');
select fk_ok('expense', 'expense_status', 'expense_status', 'expense_status');
select col_type_is('expense', 'expense_status', 'text');
select col_not_null('expense', 'expense_status');
select col_has_default('expense', 'expense_status');
select col_default_is('expense', 'expense_status', 'pending');
select has_column('expense', 'currency_code'); select has_column('expense', 'currency_code');
select col_is_fk('expense', 'currency_code'); select col_is_fk('expense', 'currency_code');
select fk_ok('expense', 'currency_code', 'currency', 'currency_code'); select fk_ok('expense', 'currency_code', 'currency', 'currency_code');

33
test/expense_status.sql Normal file
View File

@ -0,0 +1,33 @@
-- Test expense_status
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(15);
set search_path to numerus, public;
select has_table('expense_status');
select has_pk('expense_status' );
select table_privs_are('expense_status', 'guest', array []::text[]);
select table_privs_are('expense_status', 'invoicer', array ['SELECT']);
select table_privs_are('expense_status', 'admin', array ['SELECT']);
select table_privs_are('expense_status', 'authenticator', array []::text[]);
select has_column('expense_status', 'expense_status');
select col_is_pk('expense_status', 'expense_status');
select col_type_is('expense_status', 'expense_status', 'text');
select col_not_null('expense_status', 'expense_status');
select col_hasnt_default('expense_status', 'expense_status');
select has_column('expense_status', 'name');
select col_type_is('expense_status', 'name', 'text');
select col_not_null('expense_status', 'name');
select col_hasnt_default('expense_status', 'name');
select *
from finish();
rollback;

View File

@ -0,0 +1,44 @@
-- Test expense_status_i18n
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(23);
set search_path to numerus, public;
select has_table('expense_status_i18n');
select has_pk('expense_status_i18n' );
select col_is_pk('expense_status_i18n', array['expense_status', 'lang_tag']);
select table_privs_are('expense_status_i18n', 'guest', array []::text[]);
select table_privs_are('expense_status_i18n', 'invoicer', array ['SELECT']);
select table_privs_are('expense_status_i18n', 'admin', array ['SELECT']);
select table_privs_are('expense_status_i18n', 'authenticator', array []::text[]);
select has_column('expense_status_i18n', 'expense_status');
select col_is_fk('expense_status_i18n', 'expense_status');
select fk_ok('expense_status_i18n', 'expense_status', 'expense_status', 'expense_status');
select col_type_is('expense_status_i18n', 'expense_status', 'text');
select col_not_null('expense_status_i18n', 'expense_status');
select col_hasnt_default('expense_status_i18n', 'expense_status');
select has_column('expense_status_i18n', 'lang_tag');
select col_is_fk('expense_status_i18n', 'lang_tag');
select fk_ok('expense_status_i18n', 'lang_tag', 'language', 'lang_tag');
select col_type_is('expense_status_i18n', 'lang_tag', 'text');
select col_not_null('expense_status_i18n', 'lang_tag');
select col_hasnt_default('expense_status_i18n', 'lang_tag');
select has_column('expense_status_i18n', 'name');
select col_type_is('expense_status_i18n', 'name', 'text');
select col_not_null('expense_status_i18n', 'name');
select col_hasnt_default('expense_status_i18n', 'name');
select *
from finish();
rollback;

View File

@ -2,6 +2,6 @@
begin; begin;
select has_function_privilege('numerus.add_expense(integer, date, integer, text, text, integer[], numerus.tag_name[])', 'execute'); select has_function_privilege('numerus.add_expense(integer, text, date, integer, text, text, integer[], numerus.tag_name[])', 'execute');
rollback; rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:add_expense on pg
begin;
select has_function_privilege('numerus.add_expense(integer, date, integer, text, text, integer[], numerus.tag_name[])', 'execute');
rollback;

View File

@ -0,0 +1,15 @@
-- Verify numerus:available_expense_status on pg
begin;
set search_path to numerus;
select 1 / count(*) from expense_status where expense_status = 'pending' and name ='Pending';
select 1 / count(*) from expense_status where expense_status = 'paid' and name ='Paid';
select 1 / count(*) from expense_status_i18n where expense_status = 'pending' and name ='Pendent' and lang_tag = 'ca';
select 1 / count(*) from expense_status_i18n where expense_status = 'pending' and name ='Pendiente' and lang_tag = 'es';
select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'ca';
select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'es';
rollback;

View File

@ -2,6 +2,6 @@
begin; begin;
select has_function_privilege('numerus.edit_expense(uuid, date, integer, text, text, integer[], numerus.tag_name[])', 'execute'); select has_function_privilege('numerus.edit_expense(uuid, text, date, integer, text, text, integer[], numerus.tag_name[])', 'execute');
rollback; rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:edit_expense on pg
begin;
select has_function_privilege('numerus.edit_expense(uuid, date, integer, text, text, integer[], numerus.tag_name[])', 'execute');
rollback;

View File

@ -0,0 +1,10 @@
-- Verify numerus:expense_expense_status on pg
begin;
select expense_status
from numerus.expense
where false
;
rollback;

10
verify/expense_status.sql Normal file
View File

@ -0,0 +1,10 @@
-- Verify numerus:expense_status on pg
begin;
select expense_status
, name
from numerus.expense_status
where false;
rollback;

View File

@ -0,0 +1,11 @@
-- Verify numerus:expense_status_i18n on pg
begin;
select expense_status
, lang_tag
, name
from numerus.expense_status_i18n
where false;
rollback;

View File

@ -194,6 +194,10 @@ p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
:any-link {
color: #0000ff;
}
input[type="radio"] { input[type="radio"] {
accent-color: var(--numerus--color--black); accent-color: var(--numerus--color--black);
} }
@ -610,16 +614,19 @@ main > nav {
} }
.quote-status, .quote-status,
.expense-status,
.invoice-status { .invoice-status {
position: relative; position: relative;
} }
.quote-status summary, .quote-status summary,
.expense-status summary,
.invoice-status summary { .invoice-status summary {
height: 3rem; height: 3rem;
} }
.quote-status ul, .quote-status ul,
.expense-status ul,
.invoice-status ul { .invoice-status ul {
position: absolute; position: absolute;
top: 0; top: 0;
@ -631,12 +638,14 @@ main > nav {
} }
.quote-status button, .quote-status button,
.expense-status button,
.invoice-status button { .invoice-status button {
border: 0; border: 0;
min-width: 15rem; min-width: 15rem;
} }
[class^='quote-status-'], [class^='quote-status-'],
[class^='expense-status-'],
[class^='invoice-status-'] { [class^='invoice-status-'] {
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
@ -654,11 +663,13 @@ main > nav {
} }
.quote-status-accepted, .quote-status-accepted,
.expense-status-paid,
.invoice-status-paid { .invoice-status-paid {
background-color: var(--numerus--color--light-green); background-color: var(--numerus--color--light-green);
} }
.quote-status-rejected, .quote-status-rejected,
.expense-status-pending,
.invoice-status-unpaid { .invoice-status-unpaid {
background-color: var(--numerus--color--rosy); background-color: var(--numerus--color--rosy);
} }
@ -699,11 +710,6 @@ main > nav {
/* expenses */ /* expenses */
.expenses-data div:last-child {
grid-column-start: 3;
grid-column-end: 5;
}
/* product */ /* product */
.product-data div:last-child { .product-data div:last-child {
grid-column-start: 1; grid-column-start: 1;

View File

@ -18,7 +18,8 @@
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editExpensePage*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editExpensePage*/ -}}
<section id="new-expense-dialog-content" data-hx-target="main"> <section id="new-expense-dialog-content" data-hx-target="main">
<h2>{{ printf (pgettext "Edit Expense “%s”" "title") .Slug }}</h2> <h2>{{ printf (pgettext "Edit Expense “%s”" "title") .Slug }}</h2>
<form enctype="multipart/form-data" method="POST" action="{{ companyURI "/expenses/" }}{{ .Slug }}" data-hx-boost="true"> <form enctype="multipart/form-data" method="POST" action="{{ companyURI "/expenses/" }}{{ .Slug }}"
data-hx-boost="true">
{{ csrfToken }} {{ csrfToken }}
{{ putMethod }} {{ putMethod }}
@ -30,6 +31,7 @@
{{ template "input-field" .Amount }} {{ template "input-field" .Amount }}
{{ template "select-field" .Tax }} {{ template "select-field" .Tax }}
{{ template "tags-field" .Tags }} {{ template "tags-field" .Tags }}
{{ template "select-field" .ExpenseStatus }}
{{ template "file-field" .File }} {{ template "file-field" .File }}
</div> </div>
{{- end }} {{- end }}

View File

@ -26,6 +26,7 @@
> >
{{ with .Filters }} {{ with .Filters }}
{{ template "select-field" .Contact }} {{ template "select-field" .Contact }}
{{ template "select-field" .ExpenseStatus }}
{{ template "input-field" .FromDate }} {{ template "input-field" .FromDate }}
{{ template "input-field" .ToDate }} {{ template "input-field" .ToDate }}
{{ template "input-field" .InvoiceNumber }} {{ template "input-field" .InvoiceNumber }}
@ -42,6 +43,7 @@
<th>{{( pgettext "Contact" "title" )}}</th> <th>{{( pgettext "Contact" "title" )}}</th>
<th>{{( pgettext "Invoice Date" "title" )}}</th> <th>{{( pgettext "Invoice Date" "title" )}}</th>
<th>{{( pgettext "Invoice Number" "title" )}}</th> <th>{{( pgettext "Invoice Number" "title" )}}</th>
<th>{{( pgettext "Status" "title" )}}</th>
<th>{{( pgettext "Tags" "title" )}}</th> <th>{{( pgettext "Tags" "title" )}}</th>
<th>{{( pgettext "Amount" "title" )}}</th> <th>{{( pgettext "Amount" "title" )}}</th>
<th>{{( pgettext "Download" "title" )}}</th> <th>{{( pgettext "Download" "title" )}}</th>
@ -50,11 +52,33 @@
</thead> </thead>
<tbody> <tbody>
{{ with .Expenses }} {{ with .Expenses }}
{{- range . }} {{- range $expense := . }}
<tr> <tr>
<td>{{ .InvoicerName }}</td> <td>{{ .InvoicerName }}</td>
<td>{{ .InvoiceDate|formatDate }}</td> <td>{{ .InvoiceDate|formatDate }}</td>
<td>{{ .InvoiceNumber }}</td> <td>{{ .InvoiceNumber }}</td>
<td>
<details class="expense-status menu">
<summary class="expense-status-{{ .Status }}">{{ .StatusLabel }}</summary>
<form action="{{companyURI "/expenses/"}}{{ .Slug }}" enctype="multipart/form-data" method="POST" data-hx-boost="true">
{{ csrfToken }}
{{ putMethod }}
<input type="hidden" name="quick" value="status">
<ul role="menu">
{{- range $status, $name := $.ExpenseStatuses }}
{{- if ne $status $expense.Status }}
<li role="presentation">
<button role="menuitem" type="submit"
name="expense_status" value="{{ $status }}"
class="expense-status-{{ $status }}"
>{{ $name }}</button>
</li>
{{- end }}
{{- end }}
</ul>
</form>
</details>
</td>
<td <td
data-hx-get="{{companyURI "/expenses/"}}{{ .Slug }}/tags/edit" data-hx-get="{{companyURI "/expenses/"}}{{ .Slug }}/tags/edit"
data-hx-target="this" data-hx-target="this"

View File

@ -27,6 +27,7 @@
{{ template "input-field" .Amount }} {{ template "input-field" .Amount }}
{{ template "select-field" .Tax }} {{ template "select-field" .Tax }}
{{ template "tags-field" .Tags }} {{ template "tags-field" .Tags }}
{{ template "select-field" .ExpenseStatus }}
{{ template "file-field" .File }} {{ template "file-field" .File }}
</div> </div>
<fieldset> <fieldset>