diff --git a/deploy/add_expense.sql b/deploy/add_expense.sql index ef4ed0b..907f996 100644 --- a/deploy/add_expense.sql +++ b/deploy/add_expense.sql @@ -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; diff --git a/deploy/add_expense@v1.sql b/deploy/add_expense@v1.sql new file mode 100644 index 0000000..ef4ed0b --- /dev/null +++ b/deploy/add_expense@v1.sql @@ -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; diff --git a/deploy/available_expense_status.sql b/deploy/available_expense_status.sql new file mode 100644 index 0000000..9348782 --- /dev/null +++ b/deploy/available_expense_status.sql @@ -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; diff --git a/deploy/edit_expense.sql b/deploy/edit_expense.sql index ed99dcc..b6d443b 100644 --- a/deploy/edit_expense.sql +++ b/deploy/edit_expense.sql @@ -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; diff --git a/deploy/edit_expense@v1.sql b/deploy/edit_expense@v1.sql new file mode 100644 index 0000000..ed99dcc --- /dev/null +++ b/deploy/edit_expense@v1.sql @@ -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; diff --git a/deploy/expense_expense_status.sql b/deploy/expense_expense_status.sql new file mode 100644 index 0000000..e4be3bb --- /dev/null +++ b/deploy/expense_expense_status.sql @@ -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; diff --git a/deploy/expense_status.sql b/deploy/expense_status.sql new file mode 100644 index 0000000..ea8a7ea --- /dev/null +++ b/deploy/expense_status.sql @@ -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; diff --git a/deploy/expense_status_i18n.sql b/deploy/expense_status_i18n.sql new file mode 100644 index 0000000..d1a58a5 --- /dev/null +++ b/deploy/expense_status_i18n.sql @@ -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; diff --git a/pkg/expenses.go b/pkg/expenses.go index 865a1b8..d753bd8 100644 --- a/pkg/expenses.go +++ b/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) diff --git a/revert/add_expense.sql b/revert/add_expense.sql index c9a600b..36a194d 100644 --- a/revert/add_expense.sql +++ b/revert/add_expense.sql @@ -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; diff --git a/revert/add_expense@v1.sql b/revert/add_expense@v1.sql new file mode 100644 index 0000000..c9a600b --- /dev/null +++ b/revert/add_expense@v1.sql @@ -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; diff --git a/revert/available_expense_status.sql b/revert/available_expense_status.sql new file mode 100644 index 0000000..dbd531b --- /dev/null +++ b/revert/available_expense_status.sql @@ -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; diff --git a/revert/edit_expense.sql b/revert/edit_expense.sql index 3ec4a19..0f8541b 100644 --- a/revert/edit_expense.sql +++ b/revert/edit_expense.sql @@ -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; diff --git a/revert/edit_expense@v1.sql b/revert/edit_expense@v1.sql new file mode 100644 index 0000000..3ec4a19 --- /dev/null +++ b/revert/edit_expense@v1.sql @@ -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; diff --git a/revert/expense_expense_status.sql b/revert/expense_expense_status.sql new file mode 100644 index 0000000..77b7d5c --- /dev/null +++ b/revert/expense_expense_status.sql @@ -0,0 +1,9 @@ +-- Revert numerus:expense_expense_status from pg + +begin; + +alter table numerus.expense +drop column if exists expense_status +; + +commit; diff --git a/revert/expense_status.sql b/revert/expense_status.sql new file mode 100644 index 0000000..610caef --- /dev/null +++ b/revert/expense_status.sql @@ -0,0 +1,7 @@ +-- Revert numerus:expense_status from pg + +begin; + +drop table if exists numerus.expense_status; + +commit; diff --git a/revert/expense_status_i18n.sql b/revert/expense_status_i18n.sql new file mode 100644 index 0000000..96ef4d6 --- /dev/null +++ b/revert/expense_status_i18n.sql @@ -0,0 +1,7 @@ +-- Revert numerus:expense_status_i18n from pg + +begin; + +drop table if exists numerus.expense_status_i18n; + +commit; diff --git a/sqitch.plan b/sqitch.plan index 7e3936f..2d0575c 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -115,3 +115,10 @@ input_is_valid [schema_public roles] 2023-07-03T08:42:46Z jordi fita mas # 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 # Add functions to massively import customer data @v1 2023-07-03T09:32:51Z jordi fita mas # Tag version 1 + +expense_status [schema_numerus roles] 2023-07-11T11:56:39Z jordi fita mas # Add the relation of expense status +expense_status_i18n [schema_numerus roles expense_status language] 2023-07-11T12:09:48Z jordi fita mas # 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 # Add the list of available expense status +expense_expense_status [expense] 2023-07-11T12:28:58Z jordi fita mas # Add expense_status to expense relation +add_expense [add_expense@v1 expense_status expense_expense_status] 2023-07-11T13:16:16Z jordi fita mas # 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 # Add expense_status parameter to edit_expense diff --git a/test/add_expense.sql b/test/add_expense.sql index da75fb0..e89fdbd 100644 --- a/test/add_expense.sql +++ b/test/add_expense.sql @@ -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' ); diff --git a/test/edit_expense.sql b/test/edit_expense.sql index 8d33e1e..05a412a 100644 --- a/test/edit_expense.sql +++ b/test/edit_expense.sql @@ -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' ); diff --git a/test/expense.sql b/test/expense.sql index f0e5768..72f4749 100644 --- a/test/expense.sql +++ b/test/expense.sql @@ -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'); diff --git a/test/expense_status.sql b/test/expense_status.sql new file mode 100644 index 0000000..69e90b3 --- /dev/null +++ b/test/expense_status.sql @@ -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; diff --git a/test/expense_status_i18n.sql b/test/expense_status_i18n.sql new file mode 100644 index 0000000..4b52f1b --- /dev/null +++ b/test/expense_status_i18n.sql @@ -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; + diff --git a/verify/add_expense.sql b/verify/add_expense.sql index 8f3ff07..3221d5f 100644 --- a/verify/add_expense.sql +++ b/verify/add_expense.sql @@ -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; diff --git a/verify/add_expense@v1.sql b/verify/add_expense@v1.sql new file mode 100644 index 0000000..8f3ff07 --- /dev/null +++ b/verify/add_expense@v1.sql @@ -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; diff --git a/verify/available_expense_status.sql b/verify/available_expense_status.sql new file mode 100644 index 0000000..b4dab7e --- /dev/null +++ b/verify/available_expense_status.sql @@ -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; diff --git a/verify/edit_expense.sql b/verify/edit_expense.sql index 60e6663..822b876 100644 --- a/verify/edit_expense.sql +++ b/verify/edit_expense.sql @@ -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; diff --git a/verify/edit_expense@v1.sql b/verify/edit_expense@v1.sql new file mode 100644 index 0000000..60e6663 --- /dev/null +++ b/verify/edit_expense@v1.sql @@ -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; diff --git a/verify/expense_expense_status.sql b/verify/expense_expense_status.sql new file mode 100644 index 0000000..5db4648 --- /dev/null +++ b/verify/expense_expense_status.sql @@ -0,0 +1,10 @@ +-- Verify numerus:expense_expense_status on pg + +begin; + +select expense_status +from numerus.expense +where false +; + +rollback; diff --git a/verify/expense_status.sql b/verify/expense_status.sql new file mode 100644 index 0000000..93f0dba --- /dev/null +++ b/verify/expense_status.sql @@ -0,0 +1,10 @@ +-- Verify numerus:expense_status on pg + +begin; + +select expense_status + , name +from numerus.expense_status +where false; + +rollback; diff --git a/verify/expense_status_i18n.sql b/verify/expense_status_i18n.sql new file mode 100644 index 0000000..f5323f1 --- /dev/null +++ b/verify/expense_status_i18n.sql @@ -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; diff --git a/web/static/numerus.css b/web/static/numerus.css index b13e4d9..4f989df 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -614,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; @@ -635,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; @@ -658,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); } @@ -703,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; diff --git a/web/template/expenses/edit.gohtml b/web/template/expenses/edit.gohtml index 7998bf7..6f00255 100644 --- a/web/template/expenses/edit.gohtml +++ b/web/template/expenses/edit.gohtml @@ -18,20 +18,22 @@ {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editExpensePage*/ -}}

{{ printf (pgettext "Edit Expense “%s”" "title") .Slug }}

-
+ {{ csrfToken }} {{ putMethod }} {{ with .Form -}} -
- {{ 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 }} -
+
+ {{ 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 }} +
{{- end }}
diff --git a/web/template/expenses/index.gohtml b/web/template/expenses/index.gohtml index e79018a..a1cdc07 100644 --- a/web/template/expenses/index.gohtml +++ b/web/template/expenses/index.gohtml @@ -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 @@ {{( pgettext "Contact" "title" )}} {{( pgettext "Invoice Date" "title" )}} {{( pgettext "Invoice Number" "title" )}} + {{( pgettext "Status" "title" )}} {{( pgettext "Tags" "title" )}} {{( pgettext "Amount" "title" )}} {{( pgettext "Download" "title" )}} @@ -50,11 +52,33 @@ {{ with .Expenses }} - {{- range . }} + {{- range $expense := . }} {{ .InvoicerName }} {{ .InvoiceDate|formatDate }} {{ .InvoiceNumber }} + + + {{(pgettext "New Expense" "title")}}
{{ csrfToken }} -
- {{ 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 }} -
+
+ {{ 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 }} +