Compare commits
3 Commits
1164210d84
...
b48a974086
Author | SHA1 | Date |
---|---|---|
jordi fita mas | b48a974086 | |
jordi fita mas | b7578a56df | |
jordi fita mas | fa97f53dd7 |
|
@ -8,24 +8,27 @@
|
|||
-- requires: parse_price
|
||||
-- requires: tax
|
||||
-- requires: tag_name
|
||||
-- requires: expense_status
|
||||
-- requires: expense_expense_status
|
||||
|
||||
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
|
||||
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
|
||||
eid integer;
|
||||
eslug uuid;
|
||||
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
|
||||
, contact_id
|
||||
, invoice_number
|
||||
, invoice_date
|
||||
, parse_price(amount, currency.decimal_digits)
|
||||
, currency_code
|
||||
, status
|
||||
, tags
|
||||
from company
|
||||
join currency using (currency_code)
|
||||
|
@ -43,8 +46,10 @@ 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;
|
||||
revoke execute on function add_expense(integer, text, date, integer, text, text, integer[], tag_name[]) from public;
|
||||
grant execute on function add_expense(integer, text, 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 admin;
|
||||
|
||||
drop function if exists add_expense(integer, date, integer, text, text, integer[], tag_name[]);
|
||||
|
||||
commit;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -5,12 +5,14 @@
|
|||
-- requires: parse_price
|
||||
-- requires: tax
|
||||
-- requires: tag_name
|
||||
-- requires: expense_status
|
||||
-- requires: expense_expense_status
|
||||
|
||||
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
|
||||
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
|
||||
eid integer;
|
||||
|
@ -20,6 +22,7 @@ begin
|
|||
, contact_id = edit_expense.contact_id
|
||||
, invoice_number = edit_expense.invoice_number
|
||||
, amount = parse_price(edit_expense.amount, decimal_digits)
|
||||
, expense_status = status
|
||||
, tags = edit_expense.tags
|
||||
from currency
|
||||
where slug = expense_slug
|
||||
|
@ -43,8 +46,8 @@ 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;
|
||||
revoke execute on function edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]) from public;
|
||||
grant execute on function edit_expense(uuid, text, 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 admin;
|
||||
|
||||
commit;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
132
pkg/expenses.go
132
pkg/expenses.go
|
@ -20,12 +20,15 @@ type ExpenseEntry struct {
|
|||
InvoicerName string
|
||||
OriginalFileName string
|
||||
Tags []string
|
||||
Status string
|
||||
StatusLabel string
|
||||
}
|
||||
|
||||
type expensesIndexPage struct {
|
||||
Expenses []*ExpenseEntry
|
||||
TotalAmount string
|
||||
Filters *expenseFilterForm
|
||||
Expenses []*ExpenseEntry
|
||||
TotalAmount string
|
||||
Filters *expenseFilterForm
|
||||
ExpenseStatuses map[string]string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
page := &expensesIndexPage{
|
||||
Expenses: mustCollectExpenseEntries(r.Context(), conn, filters),
|
||||
TotalAmount: mustComputeExpensesTotalAmount(r.Context(), conn, filters),
|
||||
Filters: filters,
|
||||
Expenses: mustCollectExpenseEntries(r.Context(), conn, locale, filters),
|
||||
TotalAmount: mustComputeExpensesTotalAmount(r.Context(), conn, filters),
|
||||
ExpenseStatuses: mustCollectExpenseStatuses(r.Context(), conn, locale),
|
||||
Filters: filters,
|
||||
}
|
||||
mustRenderMainTemplate(w, r, "expenses/index.gohtml", page)
|
||||
}
|
||||
|
||||
func mustCollectExpenseEntries(ctx context.Context, conn *Conn, filters *expenseFilterForm) []*ExpenseEntry {
|
||||
where, args := filters.BuildQuery(nil)
|
||||
func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale, filters *expenseFilterForm) []*ExpenseEntry {
|
||||
where, args := filters.BuildQuery([]interface{}{locale.Language.String()})
|
||||
rows := conn.MustQuery(ctx, fmt.Sprintf(`
|
||||
select expense.slug
|
||||
, invoice_date
|
||||
|
@ -55,9 +59,12 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, filters *expense
|
|||
, contact.name
|
||||
, coalesce(attachment.original_filename, '')
|
||||
, expense.tags
|
||||
, expense.expense_status
|
||||
, esi18n.name
|
||||
from expense
|
||||
left join expense_attachment as attachment using (expense_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)
|
||||
where (%s)
|
||||
order by invoice_date
|
||||
|
@ -67,7 +74,7 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, filters *expense
|
|||
var entries []*ExpenseEntry
|
||||
for rows.Next() {
|
||||
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)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
|
@ -79,6 +86,31 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, filters *expense
|
|||
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 {
|
||||
where, args := filters.BuildQuery(nil)
|
||||
return conn.MustGetText(ctx, "0", fmt.Sprintf(`
|
||||
|
@ -138,6 +170,7 @@ type expenseForm struct {
|
|||
Tax *SelectField
|
||||
Amount *InputField
|
||||
File *FileField
|
||||
ExpenseStatus *SelectField
|
||||
Tags *TagsField
|
||||
}
|
||||
|
||||
|
@ -183,6 +216,13 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
|||
Label: pgettext("input", "File", locale),
|
||||
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{
|
||||
Name: "tags",
|
||||
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 {
|
||||
if err := r.ParseMultipartForm(form.File.MaxSize); err != nil {
|
||||
return err
|
||||
|
@ -202,6 +252,7 @@ func (form *expenseForm) Parse(r *http.Request) error {
|
|||
if err := form.File.FillValue(r); err != nil {
|
||||
return err
|
||||
}
|
||||
form.ExpenseStatus.FillValue(r)
|
||||
form.Tags.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
@ -217,16 +268,20 @@ func (form *expenseForm) Validate() bool {
|
|||
}
|
||||
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.CheckValidSelectOption(form.ExpenseStatus, gettext("Selected expense status is not valid.", form.locale))
|
||||
return validator.AllOK()
|
||||
}
|
||||
|
||||
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
|
||||
, invoice_number
|
||||
, invoice_date
|
||||
, to_price(amount, decimal_digits)
|
||||
, array_agg(tax_id)
|
||||
, expense_status
|
||||
, tags
|
||||
from expense
|
||||
left join expense_tax using (expense_id)
|
||||
|
@ -237,6 +292,7 @@ func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
|
|||
, invoice_date
|
||||
, amount
|
||||
, decimal_digits
|
||||
, expense_status
|
||||
, tags
|
||||
`, slug).Scan(
|
||||
form.Invoicer,
|
||||
|
@ -244,7 +300,12 @@ func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
|
|||
form.InvoiceDate,
|
||||
form.Amount,
|
||||
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) {
|
||||
conn := getConn(r)
|
||||
|
@ -267,7 +328,7 @@ func HandleAddExpense(w http.ResponseWriter, r *http.Request, _ httprouter.Param
|
|||
return
|
||||
}
|
||||
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 {
|
||||
conn.MustQuery(r.Context(), "select attach_to_expense($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
|
||||
}
|
||||
|
@ -287,23 +348,31 @@ func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprout
|
|||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
slug := params[0].Value
|
||||
if !form.Validate() {
|
||||
if !IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
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)
|
||||
}
|
||||
mustRenderEditExpenseForm(w, r, slug, form)
|
||||
return
|
||||
htmxRedirect(w, r, companyURI(mustGetCompany(r), "/expenses"))
|
||||
} else {
|
||||
slug := params[0].Value
|
||||
if !form.Validate() {
|
||||
if !IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
mustRenderEditExpenseForm(w, r, slug, form)
|
||||
return
|
||||
}
|
||||
taxes := mustSliceAtoi(form.Tax.Selected)
|
||||
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)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
htmxRedirect(w, r, companyURI(company, "/expenses"))
|
||||
}
|
||||
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 == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
htmxRedirect(w, r, companyURI(company, "/expenses"))
|
||||
}
|
||||
|
||||
type expenseFilterForm struct {
|
||||
|
@ -313,6 +382,7 @@ type expenseFilterForm struct {
|
|||
InvoiceNumber *InputField
|
||||
FromDate *InputField
|
||||
ToDate *InputField
|
||||
ExpenseStatus *SelectField
|
||||
Tags *TagsField
|
||||
TagsCondition *ToggleField
|
||||
}
|
||||
|
@ -346,6 +416,12 @@ func newExpenseFilterForm(ctx context.Context, conn *Conn, locale *Locale, compa
|
|||
Name: "tags",
|
||||
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{
|
||||
Name: "tags_condition",
|
||||
Label: pgettext("input", "Tags Condition", locale),
|
||||
|
@ -372,6 +448,7 @@ func (form *expenseFilterForm) Parse(r *http.Request) error {
|
|||
form.InvoiceNumber.FillValue(r)
|
||||
form.FromDate.FillValue(r)
|
||||
form.ToDate.FillValue(r)
|
||||
form.ExpenseStatus.FillValue(r)
|
||||
form.Tags.FillValue(r)
|
||||
form.TagsCondition.FillValue(r)
|
||||
return nil
|
||||
|
@ -398,6 +475,7 @@ func (form *expenseFilterForm) BuildQuery(args []interface{}) (string, []interfa
|
|||
customerId, _ := strconv.Atoi(form.Contact.Selected[0])
|
||||
return customerId
|
||||
})
|
||||
maybeAppendWhere("expense.expense_status = $%d", form.ExpenseStatus.String(), nil)
|
||||
maybeAppendWhere("invoice_number = $%d", form.InvoiceNumber.String(), nil)
|
||||
maybeAppendWhere("invoice_date >= $%d", form.FromDate.String(), nil)
|
||||
maybeAppendWhere("invoice_date <= $%d", form.ToDate.String(), nil)
|
||||
|
|
|
@ -152,7 +152,7 @@ func newInvoiceFilterForm(ctx context.Context, conn *Conn, locale *Locale, compa
|
|||
Name: "invoice_status",
|
||||
Label: pgettext("input", "Invoice 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{
|
||||
Name: "number",
|
||||
|
@ -691,7 +691,7 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
|||
Required: true,
|
||||
Label: pgettext("input", "Invoice Status", locale),
|
||||
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{
|
||||
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 {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
|
|
|
@ -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;
|
||||
|
||||
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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
||||
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;
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,9 @@
|
|||
-- Revert numerus:expense_expense_status from pg
|
||||
|
||||
begin;
|
||||
|
||||
alter table numerus.expense
|
||||
drop column if exists expense_status
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:expense_status from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists numerus.expense_status;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:expense_status_i18n from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists numerus.expense_status_i18n;
|
||||
|
||||
commit;
|
|
@ -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
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -9,15 +9,15 @@ select plan(14);
|
|||
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
select has_function('numerus', 'add_expense', array ['integer', '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_returns('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'uuid');
|
||||
select isnt_definer('numerus', 'add_expense', array ['integer', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']);
|
||||
select volatility_is('numerus', 'add_expense', array ['integer', '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', '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', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
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', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'plpgsql');
|
||||
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', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']);
|
||||
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', 'text', '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[]'], 'invoicer', 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', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
|
@ -64,25 +64,25 @@ values (12, 1, 'Contact 2.1')
|
|||
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select company_id, invoice_number, invoice_date, contact_id, amount, currency_code, tags, created_at from expense $$,
|
||||
$$ values (1, 'Invoice 1', '2023-05-02'::date, 12, 1111, 'EUR', '{tag1,tag2}'::tag_name[], current_timestamp)
|
||||
, (1, 'Invoice 2', '2023-05-03'::date, 13, 2222, 'EUR', '{}'::tag_name[], current_timestamp)
|
||||
, (2, 'Invoice 3', '2023-05-04'::date, 15, 3333, 'USD', '{tag3}'::tag_name[], current_timestamp)
|
||||
$$ 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', 'pending', '{tag1,tag2}'::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', 'pending', '{tag3}'::tag_name[], current_timestamp)
|
||||
$$,
|
||||
'Should have created all expenses'
|
||||
);
|
||||
|
|
|
@ -9,15 +9,15 @@ select plan(13);
|
|||
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
select has_function('numerus', 'edit_expense', array ['uuid', '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_returns('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'uuid');
|
||||
select isnt_definer('numerus', 'edit_expense', array ['uuid', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']);
|
||||
select volatility_is('numerus', 'edit_expense', array ['uuid', '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', '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', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
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', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'plpgsql');
|
||||
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', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]']);
|
||||
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', 'text', '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[]'], 'invoicer', 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', 'text', 'date', 'integer', 'text', 'text', 'integer[]', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
|
@ -58,9 +58,9 @@ values (12, 1, 'Contact 2.1')
|
|||
, (13, 1, 'Contact 2.2')
|
||||
;
|
||||
|
||||
insert into expense (expense_id, company_id, slug, invoice_number, invoice_date, contact_id, amount, currency_code, tags)
|
||||
values (15, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'INV1', '2023-05-04', 12, 111, 'EUR', '{tag1}')
|
||||
, (16, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'INV2', '2023-05-05', 13, 222, 'EUR', '{tag2}')
|
||||
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', 'pending', '{tag1}')
|
||||
, (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)
|
||||
|
@ -70,19 +70,19 @@ values (15, 4, 0.21)
|
|||
;
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select invoice_number, invoice_date, contact_id, amount, tags from expense $$,
|
||||
$$ values ('INV11', '2023-05-06'::date, 13, 112, '{tag1}'::tag_name[])
|
||||
, ('INV22', '2023-05-07'::date, 12, 333, '{tag1,tag3}'::tag_name[])
|
||||
$$ select invoice_number, invoice_date, contact_id, amount, expense_status, tags from expense $$,
|
||||
$$ values ('INV11', '2023-05-06'::date, 13, 112, 'paid', '{tag1}'::tag_name[])
|
||||
, ('INV22', '2023-05-07'::date, 12, 333, 'pending', '{tag1,tag3}'::tag_name[])
|
||||
$$,
|
||||
'Should have updated all expenses'
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@ reset client_min_messages;
|
|||
|
||||
begin;
|
||||
|
||||
select plan(67);
|
||||
select plan(74);
|
||||
|
||||
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_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 col_is_fk('expense', 'currency_code');
|
||||
select fk_ok('expense', 'currency_code', 'currency', 'currency_code');
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
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;
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,10 @@
|
|||
-- Verify numerus:expense_expense_status on pg
|
||||
|
||||
begin;
|
||||
|
||||
select expense_status
|
||||
from numerus.expense
|
||||
where false
|
||||
;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,10 @@
|
|||
-- Verify numerus:expense_status on pg
|
||||
|
||||
begin;
|
||||
|
||||
select expense_status
|
||||
, name
|
||||
from numerus.expense_status
|
||||
where false;
|
||||
|
||||
rollback;
|
|
@ -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;
|
|
@ -194,6 +194,10 @@ p, h1, h2, h3, h4, h5, h6 {
|
|||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
:any-link {
|
||||
color: #0000ff;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
accent-color: var(--numerus--color--black);
|
||||
}
|
||||
|
@ -610,16 +614,19 @@ main > nav {
|
|||
}
|
||||
|
||||
.quote-status,
|
||||
.expense-status,
|
||||
.invoice-status {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quote-status summary,
|
||||
.expense-status summary,
|
||||
.invoice-status summary {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.quote-status ul,
|
||||
.expense-status ul,
|
||||
.invoice-status ul {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -631,12 +638,14 @@ main > nav {
|
|||
}
|
||||
|
||||
.quote-status button,
|
||||
.expense-status button,
|
||||
.invoice-status button {
|
||||
border: 0;
|
||||
min-width: 15rem;
|
||||
}
|
||||
|
||||
[class^='quote-status-'],
|
||||
[class^='expense-status-'],
|
||||
[class^='invoice-status-'] {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
|
@ -654,11 +663,13 @@ main > nav {
|
|||
}
|
||||
|
||||
.quote-status-accepted,
|
||||
.expense-status-paid,
|
||||
.invoice-status-paid {
|
||||
background-color: var(--numerus--color--light-green);
|
||||
}
|
||||
|
||||
.quote-status-rejected,
|
||||
.expense-status-pending,
|
||||
.invoice-status-unpaid {
|
||||
background-color: var(--numerus--color--rosy);
|
||||
}
|
||||
|
@ -699,11 +710,6 @@ main > nav {
|
|||
|
||||
/* expenses */
|
||||
|
||||
.expenses-data div:last-child {
|
||||
grid-column-start: 3;
|
||||
grid-column-end: 5;
|
||||
}
|
||||
|
||||
/* product */
|
||||
.product-data div:last-child {
|
||||
grid-column-start: 1;
|
||||
|
|
|
@ -18,20 +18,22 @@
|
|||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editExpensePage*/ -}}
|
||||
<section id="new-expense-dialog-content" data-hx-target="main">
|
||||
<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 }}
|
||||
{{ putMethod }}
|
||||
|
||||
{{ with .Form -}}
|
||||
<div class="expenses-data">
|
||||
{{ template "select-field" .Invoicer }}
|
||||
{{ template "input-field" .InvoiceNumber }}
|
||||
{{ template "input-field" .InvoiceDate }}
|
||||
{{ template "input-field" .Amount }}
|
||||
{{ template "select-field" .Tax }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
{{ template "file-field" .File }}
|
||||
</div>
|
||||
<div class="expenses-data">
|
||||
{{ template "select-field" .Invoicer }}
|
||||
{{ template "input-field" .InvoiceNumber }}
|
||||
{{ template "input-field" .InvoiceDate }}
|
||||
{{ template "input-field" .Amount }}
|
||||
{{ template "select-field" .Tax }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
{{ template "select-field" .ExpenseStatus }}
|
||||
{{ template "file-field" .File }}
|
||||
</div>
|
||||
{{- end }}
|
||||
|
||||
<fieldset>
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
>
|
||||
{{ with .Filters }}
|
||||
{{ template "select-field" .Contact }}
|
||||
{{ template "select-field" .ExpenseStatus }}
|
||||
{{ template "input-field" .FromDate }}
|
||||
{{ template "input-field" .ToDate }}
|
||||
{{ template "input-field" .InvoiceNumber }}
|
||||
|
@ -42,6 +43,7 @@
|
|||
<th>{{( pgettext "Contact" "title" )}}</th>
|
||||
<th>{{( pgettext "Invoice Date" "title" )}}</th>
|
||||
<th>{{( pgettext "Invoice Number" "title" )}}</th>
|
||||
<th>{{( pgettext "Status" "title" )}}</th>
|
||||
<th>{{( pgettext "Tags" "title" )}}</th>
|
||||
<th>{{( pgettext "Amount" "title" )}}</th>
|
||||
<th>{{( pgettext "Download" "title" )}}</th>
|
||||
|
@ -50,11 +52,33 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{{ with .Expenses }}
|
||||
{{- range . }}
|
||||
{{- range $expense := . }}
|
||||
<tr>
|
||||
<td>{{ .InvoicerName }}</td>
|
||||
<td>{{ .InvoiceDate|formatDate }}</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
|
||||
data-hx-get="{{companyURI "/expenses/"}}{{ .Slug }}/tags/edit"
|
||||
data-hx-target="this"
|
||||
|
|
|
@ -20,15 +20,16 @@
|
|||
<h2>{{(pgettext "New Expense" "title")}}</h2>
|
||||
<form enctype="multipart/form-data" method="POST" action="{{ companyURI "/expenses" }}" data-hx-boost="true">
|
||||
{{ csrfToken }}
|
||||
<div class="expenses-data">
|
||||
{{ template "select-field" .Invoicer }}
|
||||
{{ template "input-field" .InvoiceNumber }}
|
||||
{{ template "input-field" .InvoiceDate }}
|
||||
{{ template "input-field" .Amount }}
|
||||
{{ template "select-field" .Tax }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
{{ template "file-field" .File }}
|
||||
</div>
|
||||
<div class="expenses-data">
|
||||
{{ template "select-field" .Invoicer }}
|
||||
{{ template "input-field" .InvoiceNumber }}
|
||||
{{ template "input-field" .InvoiceDate }}
|
||||
{{ template "input-field" .Amount }}
|
||||
{{ template "select-field" .Tax }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
{{ template "select-field" .ExpenseStatus }}
|
||||
{{ template "file-field" .File }}
|
||||
</div>
|
||||
<fieldset>
|
||||
<button class="primary" type="submit">{{( pgettext "Save" "action" )}}</button>
|
||||
</fieldset>
|
||||
|
|
Loading…
Reference in New Issue