From b48a9740865397b61139725d25af86a7ee94d194 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 11 Jul 2023 15:33:26 +0200 Subject: [PATCH] Add expenses statuses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- deploy/add_expense.sql | 15 ++-- deploy/add_expense@v1.sql | 50 +++++++++++ deploy/available_expense_status.sql | 22 +++++ deploy/edit_expense.sql | 11 ++- deploy/edit_expense@v1.sql | 50 +++++++++++ deploy/expense_expense_status.sql | 12 +++ deploy/expense_status.sql | 17 ++++ deploy/expense_status_i18n.sql | 21 +++++ pkg/expenses.go | 132 ++++++++++++++++++++++------ revert/add_expense.sql | 49 ++++++++++- revert/add_expense@v1.sql | 7 ++ revert/available_expense_status.sql | 10 +++ revert/edit_expense.sql | 49 ++++++++++- revert/edit_expense@v1.sql | 7 ++ revert/expense_expense_status.sql | 9 ++ revert/expense_status.sql | 7 ++ revert/expense_status_i18n.sql | 7 ++ sqitch.plan | 7 ++ test/add_expense.sql | 32 +++---- test/edit_expense.sql | 34 +++---- test/expense.sql | 10 ++- test/expense_status.sql | 33 +++++++ test/expense_status_i18n.sql | 44 ++++++++++ verify/add_expense.sql | 2 +- verify/add_expense@v1.sql | 7 ++ verify/available_expense_status.sql | 15 ++++ verify/edit_expense.sql | 2 +- verify/edit_expense@v1.sql | 7 ++ verify/expense_expense_status.sql | 10 +++ verify/expense_status.sql | 10 +++ verify/expense_status_i18n.sql | 11 +++ web/static/numerus.css | 12 +-- web/template/expenses/edit.gohtml | 22 ++--- web/template/expenses/index.gohtml | 26 +++++- web/template/expenses/new.gohtml | 19 ++-- 35 files changed, 677 insertions(+), 101 deletions(-) create mode 100644 deploy/add_expense@v1.sql create mode 100644 deploy/available_expense_status.sql create mode 100644 deploy/edit_expense@v1.sql create mode 100644 deploy/expense_expense_status.sql create mode 100644 deploy/expense_status.sql create mode 100644 deploy/expense_status_i18n.sql create mode 100644 revert/add_expense@v1.sql create mode 100644 revert/available_expense_status.sql create mode 100644 revert/edit_expense@v1.sql create mode 100644 revert/expense_expense_status.sql create mode 100644 revert/expense_status.sql create mode 100644 revert/expense_status_i18n.sql create mode 100644 test/expense_status.sql create mode 100644 test/expense_status_i18n.sql create mode 100644 verify/add_expense@v1.sql create mode 100644 verify/available_expense_status.sql create mode 100644 verify/edit_expense@v1.sql create mode 100644 verify/expense_expense_status.sql create mode 100644 verify/expense_status.sql create mode 100644 verify/expense_status_i18n.sql 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 }} +