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.
This commit is contained in:
parent
b7578a56df
commit
b48a974086
|
@ -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;
|
||||||
|
|
|
@ -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: 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;
|
||||||
|
|
|
@ -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
|
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),
|
||||||
Filters: filters,
|
ExpenseStatuses: mustCollectExpenseStatuses(r.Context(), conn, locale),
|
||||||
|
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,23 +348,31 @@ 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
|
||||||
}
|
}
|
||||||
slug := params[0].Value
|
if r.FormValue("quick") == "status" {
|
||||||
if !form.Validate() {
|
slug := conn.MustGetText(r.Context(), "", "update expense set expense_status = $1 where slug = $2 returning slug", form.ExpenseStatus, params[0].Value)
|
||||||
if !IsHTMxRequest(r) {
|
if slug == "" {
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
mustRenderEditExpenseForm(w, r, slug, form)
|
htmxRedirect(w, r, companyURI(mustGetCompany(r), "/expenses"))
|
||||||
return
|
} 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 {
|
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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
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;
|
||||||
|
|
|
@ -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
|
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
|
||||||
|
|
|
@ -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'
|
||||||
);
|
);
|
||||||
|
|
|
@ -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'
|
||||||
);
|
);
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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;
|
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;
|
||||||
|
|
|
@ -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;
|
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;
|
||||||
|
|
|
@ -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;
|
|
@ -614,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;
|
||||||
|
@ -635,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;
|
||||||
|
@ -658,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);
|
||||||
}
|
}
|
||||||
|
@ -703,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;
|
||||||
|
|
|
@ -18,20 +18,22 @@
|
||||||
{{- /*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 }}
|
||||||
|
|
||||||
{{ with .Form -}}
|
{{ with .Form -}}
|
||||||
<div class="expenses-data">
|
<div class="expenses-data">
|
||||||
{{ template "select-field" .Invoicer }}
|
{{ template "select-field" .Invoicer }}
|
||||||
{{ template "input-field" .InvoiceNumber }}
|
{{ template "input-field" .InvoiceNumber }}
|
||||||
{{ template "input-field" .InvoiceDate }}
|
{{ template "input-field" .InvoiceDate }}
|
||||||
{{ 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 "file-field" .File }}
|
{{ template "select-field" .ExpenseStatus }}
|
||||||
</div>
|
{{ template "file-field" .File }}
|
||||||
|
</div>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -20,15 +20,16 @@
|
||||||
<h2>{{(pgettext "New Expense" "title")}}</h2>
|
<h2>{{(pgettext "New Expense" "title")}}</h2>
|
||||||
<form enctype="multipart/form-data" method="POST" action="{{ companyURI "/expenses" }}" data-hx-boost="true">
|
<form enctype="multipart/form-data" method="POST" action="{{ companyURI "/expenses" }}" data-hx-boost="true">
|
||||||
{{ csrfToken }}
|
{{ csrfToken }}
|
||||||
<div class="expenses-data">
|
<div class="expenses-data">
|
||||||
{{ template "select-field" .Invoicer }}
|
{{ template "select-field" .Invoicer }}
|
||||||
{{ template "input-field" .InvoiceNumber }}
|
{{ template "input-field" .InvoiceNumber }}
|
||||||
{{ template "input-field" .InvoiceDate }}
|
{{ template "input-field" .InvoiceDate }}
|
||||||
{{ 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 "file-field" .File }}
|
{{ template "select-field" .ExpenseStatus }}
|
||||||
</div>
|
{{ template "file-field" .File }}
|
||||||
|
</div>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<button class="primary" type="submit">{{( pgettext "Save" "action" )}}</button>
|
<button class="primary" type="submit">{{( pgettext "Save" "action" )}}</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
Loading…
Reference in New Issue