Compute the total amount, base plus taxes, of all expenses

This works mostly like invoices: i have to “update” the expense form
to compute its total based on the subtotal and the selected taxes,
although in this case i do no need to compute the subtotal because that
is given by the user.

Nevertheless, i added a new function to compute that total because it
was already hairy enough for the dashboard, that also needs to compute
the tota, not just the base, and i wanted to test that function.

There is no need for a custom input type for that function as it only
needs a couple of simple domains.   I have created the output type,
though, because otherwise i would need to have records or “reuse” any
other “amount” output type, which would be confusing.\

Part of #68.
This commit is contained in:
jordi fita mas 2023-07-13 20:50:26 +02:00
parent 7d55e949fc
commit a7c1df20f0
16 changed files with 472 additions and 117 deletions

View File

@ -0,0 +1,65 @@
-- Deploy numerus:compute_new_expense_amount to pg
-- requires: schema_numerus
-- requires: roles
-- requires: company
-- requires: tax
-- requires: new_expense_amount
begin;
set search_path to numerus, public;
create or replace function compute_new_expense_amount(company_id integer, subtotal text, taxes integer[]) returns new_expense_amount as
$$
declare
result new_expense_amount;
begin
if array_length(taxes, 1) > 0 then
with line as (
select round(parse_price(subtotal, currency.decimal_digits)) as price
, tax_id
, decimal_digits
from unnest (taxes) as tax(tax_id)
join company on company.company_id = compute_new_expense_amount.company_id
join currency using (currency_code)
)
, tax_amount as (
select tax_id
, sum(round(price * tax.rate)::integer)::integer as amount
, decimal_digits
from line
join tax using (tax_id)
group by tax_id, decimal_digits
)
, tax_total as (
select sum(amount)::integer as amount
, array_agg(array[name, to_price(amount, decimal_digits)]) as taxes
from tax_amount
join tax using (tax_id)
)
select coalesce(tax_total.taxes, array[]::text[][])
, to_price(price::integer + coalesce(tax_total.amount, 0), decimal_digits) as total
from line, tax_total
into result.taxes, result.total;
else
select array[]::text[][]
, to_price(coalesce(parse_price(subtotal, decimal_digits), 0), decimal_digits)
from company
join currency using (currency_code)
where company.company_id = compute_new_expense_amount.company_id
into result.taxes, result.total
;
end if;
return result;
end;
$$
language plpgsql
stable
;
revoke execute on function compute_new_expense_amount(integer, text, integer[]) from public;
grant execute on function compute_new_expense_amount(integer, text, integer[]) to invoicer;
grant execute on function compute_new_expense_amount(integer, text, integer[]) to admin;
commit;

View File

@ -0,0 +1,13 @@
-- Deploy numerus:new_expense_amount to pg
-- requires: schema_numerus
begin;
set search_path to numerus, public;
create type new_expense_amount as (
taxes text[][],
total text
);
commit;

View File

@ -66,7 +66,7 @@ func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
rows := conn.MustQuery(r.Context(), fmt.Sprintf(` rows := conn.MustQuery(r.Context(), fmt.Sprintf(`
select to_price(0, decimal_digits) as sales select to_price(0, decimal_digits) as sales
, to_price(coalesce(invoice.total, 0), decimal_digits) as income , to_price(coalesce(invoice.total, 0), decimal_digits) as income
, to_price(coalesce(expense.total, 0), decimal_digits) as expenses , to_price(coalesce(expense.total, 0) + coalesce(expense_tax.vat, 0) + coalesce(expense_tax.irpf, 0), decimal_digits) as expenses
, to_price(coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0), decimal_digits) as vat , to_price(coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0), decimal_digits) as vat
, to_price(coalesce(invoice_tax.irpf, 0) + coalesce(expense_tax.irpf, 0), decimal_digits) as irpf , to_price(coalesce(invoice_tax.irpf, 0) + coalesce(expense_tax.irpf, 0), decimal_digits) as irpf
, to_price(coalesce(invoice.total, 0) - coalesce(expense.total, 0) - (coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0)) + coalesce(expense_tax.irpf, 0), decimal_digits) as net_income , to_price(coalesce(invoice.total, 0) - coalesce(expense.total, 0) - (coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0)) + coalesce(expense_tax.irpf, 0), decimal_digits) as net_income
@ -201,9 +201,21 @@ func buildDashboardChart(ctx context.Context, conn *Conn, locale *Locale, compan
) as invoice ) as invoice
left join ( left join (
select to_char(date.invoice_date, '%[3]s')::integer as date select to_char(date.invoice_date, '%[3]s')::integer as date
, sum(amount)::integer as total , sum(subtotal + taxes)::integer as total
from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date) from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date)
left join expense on expense.invoice_date = date.invoice_date and company_id = $1 left join (
select expense_id
, invoice_date
, expense.amount as subtotal
, coalesce(sum(tax.amount)::integer, 0) as taxes
from expense
left join expense_tax_amount as tax using (expense_id)
where company_id = $1
group by expense_id
, invoice_date
, expense.amount
) as expense
on expense.invoice_date = date.invoice_date
group by date group by date
) as expense using (date) ) as expense using (date)
order by date order by date

View File

@ -55,7 +55,7 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale,
select expense.slug select expense.slug
, invoice_date , invoice_date
, invoice_number , invoice_number
, to_price(amount, decimal_digits) , to_price(expense.amount + coalesce(sum(tax.amount)::integer, 0), decimal_digits)
, contact.name , contact.name
, coalesce(attachment.original_filename, '') , coalesce(attachment.original_filename, '')
, expense.tags , expense.tags
@ -63,10 +63,21 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale,
, esi18n.name , esi18n.name
from expense from expense
left join expense_attachment as attachment using (expense_id) left join expense_attachment as attachment using (expense_id)
left join expense_tax_amount as tax using (expense_id)
join contact using (contact_id) join contact using (contact_id)
join expense_status_i18n esi18n on expense.expense_status = esi18n.expense_status and esi18n.lang_tag = $1 join expense_status_i18n esi18n on expense.expense_status = esi18n.expense_status and esi18n.lang_tag = $1
join currency using (currency_code) join currency using (currency_code)
where (%s) where (%s)
group by expense.slug
, invoice_date
, invoice_number
, expense.amount
, decimal_digits
, contact.name
, attachment.original_filename
, expense.tags
, expense.expense_status
, esi18n.name
order by invoice_date order by invoice_date
`, where), args...) `, where), args...)
defer rows.Close() defer rows.Close()
@ -114,10 +125,20 @@ func mustCollectExpenseStatuses(ctx context.Context, conn *Conn, locale *Locale)
func mustComputeExpensesTotalAmount(ctx context.Context, conn *Conn, filters *expenseFilterForm) string { func mustComputeExpensesTotalAmount(ctx context.Context, conn *Conn, filters *expenseFilterForm) string {
where, args := filters.BuildQuery(nil) where, args := filters.BuildQuery(nil)
return conn.MustGetText(ctx, "0", fmt.Sprintf(` return conn.MustGetText(ctx, "0", fmt.Sprintf(`
select to_price(sum(amount)::integer, decimal_digits) select to_price(sum(subtotal + taxes)::integer, decimal_digits)
from expense from (
select expense_id
, expense.amount as subtotal
, coalesce(sum(tax.amount)::integer, 0) as taxes
, currency_code
from expense
left join expense_tax_amount as tax using (expense_id)
where (%s)
group by expense_id
, expense.amount
, currency_code
) as expense
join currency using (currency_code) join currency using (currency_code)
where (%s)
group by decimal_digits group by decimal_digits
`, where), args...) `, where), args...)
} }
@ -145,20 +166,40 @@ func ServeExpenseForm(w http.ResponseWriter, r *http.Request, params httprouter.
func mustRenderNewExpenseForm(w http.ResponseWriter, r *http.Request, form *expenseForm) { func mustRenderNewExpenseForm(w http.ResponseWriter, r *http.Request, form *expenseForm) {
locale := getLocale(r) locale := getLocale(r)
form.Invoicer.EmptyLabel = gettext("Select a contact.", locale) form.Invoicer.EmptyLabel = gettext("Select a contact.", locale)
mustRenderMainTemplate(w, r, "expenses/new.gohtml", form) page := newNewExpensePage(form, r)
mustRenderMainTemplate(w, r, "expenses/new.gohtml", page)
}
type newExpensePage struct {
Form *expenseForm
Taxes [][]string
Total string
}
func newNewExpensePage(form *expenseForm, r *http.Request) *newExpensePage {
page := &newExpensePage{
Form: form,
}
conn := getConn(r)
company := mustGetCompany(r)
err := conn.QueryRow(r.Context(), "select taxes, total from compute_new_expense_amount($1, $2, $3)", company.Id, form.Amount, form.Tax.Selected).Scan(&page.Taxes, &page.Total)
if err != nil {
panic(err)
}
return page
} }
func mustRenderEditExpenseForm(w http.ResponseWriter, r *http.Request, slug string, form *expenseForm) { func mustRenderEditExpenseForm(w http.ResponseWriter, r *http.Request, slug string, form *expenseForm) {
page := &editExpensePage{ page := &editExpensePage{
Slug: slug, newNewExpensePage(form, r),
Form: form, slug,
} }
mustRenderMainTemplate(w, r, "expenses/edit.gohtml", page) mustRenderMainTemplate(w, r, "expenses/edit.gohtml", page)
} }
type editExpensePage struct { type editExpensePage struct {
*newExpensePage
Slug string Slug string
Form *expenseForm
} }
type expenseForm struct { type expenseForm struct {
@ -175,6 +216,7 @@ type expenseForm struct {
} }
func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *expenseForm { func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *expenseForm {
triggerRecompute := template.HTMLAttr(`data-hx-on="change: this.dispatchEvent(new CustomEvent('recompute', {bubbles: true}))"`)
return &expenseForm{ return &expenseForm{
locale: locale, locale: locale,
company: company, company: company,
@ -200,6 +242,9 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Label: pgettext("input", "Taxes", locale), Label: pgettext("input", "Taxes", locale),
Multiple: true, Multiple: true,
Options: mustGetTaxOptions(ctx, conn, company), Options: mustGetTaxOptions(ctx, conn, company),
Attributes: []template.HTMLAttr{
triggerRecompute,
},
}, },
Amount: &InputField{ Amount: &InputField{
Name: "amount", Name: "amount",
@ -207,6 +252,7 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Type: "number", Type: "number",
Required: true, Required: true,
Attributes: []template.HTMLAttr{ Attributes: []template.HTMLAttr{
triggerRecompute,
`min="0"`, `min="0"`,
template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())), template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
}, },
@ -305,34 +351,6 @@ func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
} }
return true return true
} }
func HandleAddExpense(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
company := mustGetCompany(r)
form := newExpenseForm(r.Context(), conn, locale, company)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !form.Validate() {
if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderNewExpenseForm(w, r, form)
return
}
taxes := mustSliceAtoi(form.Tax.Selected)
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)
}
htmxRedirect(w, r, companyURI(company, "/expenses"))
}
func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r) conn := getConn(r)
locale := getLocale(r) locale := getLocale(r)
@ -541,3 +559,55 @@ func ServeExpenseAttachment(w http.ResponseWriter, r *http.Request, params httpr
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(content) w.Write(content)
} }
func HandleEditExpenseAction(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
slug := params[0].Value
actionUri := fmt.Sprintf("/invoices/%s/edit", slug)
handleExpenseAction(w, r, actionUri, func(w http.ResponseWriter, r *http.Request, form *expenseForm) {
mustRenderEditExpenseForm(w, r, slug, form)
})
}
func HandleNewExpenseAction(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
handleExpenseAction(w, r, "/expenses", mustRenderNewExpenseForm)
}
type renderExpenseFormFunc func(w http.ResponseWriter, r *http.Request, form *expenseForm)
func handleExpenseAction(w http.ResponseWriter, r *http.Request, action string, renderForm renderExpenseFormFunc) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
form := newExpenseForm(r.Context(), conn, locale, company)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
actionField := r.Form.Get("action")
switch actionField {
case "update":
// Nothing else to do
w.WriteHeader(http.StatusOK)
renderForm(w, r, form)
case "add":
if !form.Validate() {
if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
renderForm(w, r, form)
return
}
taxes := mustSliceAtoi(form.Tax.Selected)
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)
}
htmxRedirect(w, r, companyURI(company, action))
default:
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
}
}

View File

@ -50,8 +50,9 @@ func NewRouter(db *Db) http.Handler {
companyRouter.GET("/quotes/:slug/tags/edit", ServeEditQuoteTags) companyRouter.GET("/quotes/:slug/tags/edit", ServeEditQuoteTags)
companyRouter.GET("/search/products", HandleProductSearch) companyRouter.GET("/search/products", HandleProductSearch)
companyRouter.GET("/expenses", IndexExpenses) companyRouter.GET("/expenses", IndexExpenses)
companyRouter.POST("/expenses", HandleAddExpense) companyRouter.POST("/expenses", HandleNewExpenseAction)
companyRouter.GET("/expenses/:slug", ServeExpenseForm) companyRouter.GET("/expenses/:slug", ServeExpenseForm)
companyRouter.POST("/expenses/:slug", HandleEditExpenseAction)
companyRouter.PUT("/expenses/:slug", HandleUpdateExpense) companyRouter.PUT("/expenses/:slug", HandleUpdateExpense)
companyRouter.PUT("/expenses/:slug/tags", HandleUpdateExpenseTags) companyRouter.PUT("/expenses/:slug/tags", HandleUpdateExpenseTags)
companyRouter.GET("/expenses/:slug/tags/edit", ServeEditExpenseTags) companyRouter.GET("/expenses/:slug/tags/edit", ServeEditExpenseTags)

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-07-12 20:01+0200\n" "POT-Creation-Date: 2023-07-13 20:43+0200\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -105,12 +105,14 @@ msgstr "Subtotal"
#: web/template/invoices/view.gohtml:115 web/template/invoices/edit.gohtml:75 #: web/template/invoices/view.gohtml:115 web/template/invoices/edit.gohtml:75
#: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82 #: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82
#: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75 #: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75
#: web/template/expenses/new.gohtml:45 web/template/expenses/edit.gohtml:49
msgctxt "title" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93
#: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 #: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93
#: web/template/expenses/new.gohtml:55 web/template/expenses/edit.gohtml:59
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
msgstr "Actualitza" msgstr "Actualitza"
@ -118,7 +120,7 @@ msgstr "Actualitza"
#: web/template/invoices/new.gohtml:95 web/template/invoices/edit.gohtml:96 #: web/template/invoices/new.gohtml:95 web/template/invoices/edit.gohtml:96
#: web/template/quotes/new.gohtml:95 web/template/quotes/edit.gohtml:96 #: web/template/quotes/new.gohtml:95 web/template/quotes/edit.gohtml:96
#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53
#: web/template/expenses/new.gohtml:34 web/template/expenses/edit.gohtml:40 #: web/template/expenses/new.gohtml:58 web/template/expenses/edit.gohtml:62
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36
msgctxt "action" msgctxt "action"
msgid "Save" msgid "Save"
@ -719,37 +721,37 @@ msgid "Name"
msgstr "Nom" msgstr "Nom"
#: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:685 #: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:685
#: pkg/expenses.go:228 pkg/expenses.go:417 pkg/invoices.go:174 #: pkg/expenses.go:274 pkg/expenses.go:433 pkg/invoices.go:174
#: pkg/invoices.go:723 pkg/invoices.go:1295 pkg/contacts.go:145 #: pkg/invoices.go:723 pkg/invoices.go:1295 pkg/contacts.go:145
#: pkg/contacts.go:348 #: pkg/contacts.go:348
msgctxt "input" msgctxt "input"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
#: pkg/products.go:173 pkg/quote.go:178 pkg/expenses.go:427 pkg/invoices.go:178 #: pkg/products.go:173 pkg/quote.go:178 pkg/expenses.go:443 pkg/invoices.go:178
#: pkg/contacts.go:149 #: pkg/contacts.go:149
msgctxt "input" msgctxt "input"
msgid "Tags Condition" msgid "Tags Condition"
msgstr "Condició de les etiquetes" msgstr "Condició de les etiquetes"
#: pkg/products.go:177 pkg/quote.go:182 pkg/expenses.go:431 pkg/invoices.go:182 #: pkg/products.go:177 pkg/quote.go:182 pkg/expenses.go:447 pkg/invoices.go:182
#: pkg/contacts.go:153 #: pkg/contacts.go:153
msgctxt "tag condition" msgctxt "tag condition"
msgid "All" msgid "All"
msgstr "Totes" msgstr "Totes"
#: pkg/products.go:178 pkg/expenses.go:432 pkg/invoices.go:183 #: pkg/products.go:178 pkg/expenses.go:448 pkg/invoices.go:183
#: pkg/contacts.go:154 #: pkg/contacts.go:154
msgid "Invoices must have all the specified labels." msgid "Invoices must have all the specified labels."
msgstr "Les factures han de tenir totes les etiquetes." msgstr "Les factures han de tenir totes les etiquetes."
#: pkg/products.go:182 pkg/quote.go:187 pkg/expenses.go:436 pkg/invoices.go:187 #: pkg/products.go:182 pkg/quote.go:187 pkg/expenses.go:452 pkg/invoices.go:187
#: pkg/contacts.go:158 #: pkg/contacts.go:158
msgctxt "tag condition" msgctxt "tag condition"
msgid "Any" msgid "Any"
msgstr "Qualsevol" msgstr "Qualsevol"
#: pkg/products.go:183 pkg/expenses.go:437 pkg/invoices.go:188 #: pkg/products.go:183 pkg/expenses.go:453 pkg/invoices.go:188
#: pkg/contacts.go:159 #: pkg/contacts.go:159
msgid "Invoices must have at least one of the specified labels." msgid "Invoices must have at least one of the specified labels."
msgstr "Les factures han de tenir com a mínim una de les etiquetes." msgstr "Les factures han de tenir com a mínim una de les etiquetes."
@ -764,7 +766,7 @@ msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Preu" msgstr "Preu"
#: pkg/products.go:284 pkg/quote.go:925 pkg/expenses.go:200 #: pkg/products.go:284 pkg/quote.go:925 pkg/expenses.go:242
#: pkg/invoices.go:1040 #: pkg/invoices.go:1040
msgctxt "input" msgctxt "input"
msgid "Taxes" msgid "Taxes"
@ -783,12 +785,12 @@ msgstr "No podeu deixar el preu en blanc."
msgid "Price must be a number greater than zero." msgid "Price must be a number greater than zero."
msgstr "El preu ha de ser un número major a zero." msgstr "El preu ha de ser un número major a zero."
#: pkg/products.go:313 pkg/quote.go:984 pkg/expenses.go:264 pkg/expenses.go:269 #: pkg/products.go:313 pkg/quote.go:984 pkg/expenses.go:310
#: pkg/invoices.go:1099 #: pkg/invoices.go:1099
msgid "Selected tax is not valid." msgid "Selected tax is not valid."
msgstr "Heu seleccionat un impost que no és vàlid." msgstr "Heu seleccionat un impost que no és vàlid."
#: pkg/products.go:314 pkg/quote.go:985 pkg/expenses.go:265 pkg/expenses.go:270 #: pkg/products.go:314 pkg/quote.go:985 pkg/expenses.go:311
#: pkg/invoices.go:1100 #: pkg/invoices.go:1100
msgid "You can only select a tax of each class." msgid "You can only select a tax of each class."
msgstr "Només podeu seleccionar un impost de cada classe." msgstr "Només podeu seleccionar un impost de cada classe."
@ -1012,7 +1014,7 @@ msgctxt "input"
msgid "Quotation Status" msgid "Quotation Status"
msgstr "Estat del pressupost" msgstr "Estat del pressupost"
#: pkg/quote.go:154 pkg/expenses.go:422 pkg/invoices.go:154 #: pkg/quote.go:154 pkg/expenses.go:438 pkg/invoices.go:154
msgid "All status" msgid "All status"
msgstr "Tots els estats" msgstr "Tots els estats"
@ -1021,12 +1023,12 @@ msgctxt "input"
msgid "Quotation Number" msgid "Quotation Number"
msgstr "Número de pressupost" msgstr "Número de pressupost"
#: pkg/quote.go:164 pkg/expenses.go:407 pkg/invoices.go:164 #: pkg/quote.go:164 pkg/expenses.go:423 pkg/invoices.go:164
msgctxt "input" msgctxt "input"
msgid "From Date" msgid "From Date"
msgstr "A partir de la data" msgstr "A partir de la data"
#: pkg/quote.go:169 pkg/expenses.go:412 pkg/invoices.go:169 #: pkg/quote.go:169 pkg/expenses.go:428 pkg/invoices.go:169
msgctxt "input" msgctxt "input"
msgid "To Date" msgid "To Date"
msgstr "Fins la data" msgstr "Fins la data"
@ -1043,8 +1045,8 @@ msgstr "Els pressuposts han de tenir com a mínim una de les etiquetes."
msgid "quotations.zip" msgid "quotations.zip"
msgstr "pressuposts.zip" msgstr "pressuposts.zip"
#: pkg/quote.go:611 pkg/quote.go:1140 pkg/quote.go:1148 pkg/invoices.go:654 #: pkg/quote.go:611 pkg/quote.go:1140 pkg/quote.go:1148 pkg/expenses.go:611
#: pkg/invoices.go:1270 pkg/invoices.go:1278 #: pkg/invoices.go:654 pkg/invoices.go:1270 pkg/invoices.go:1278
msgid "Invalid action" msgid "Invalid action"
msgstr "Acció invàlida." msgstr "Acció invàlida."
@ -1198,65 +1200,65 @@ msgctxt "period option"
msgid "Previous year" msgid "Previous year"
msgstr "Any anterior" msgstr "Any anterior"
#: pkg/expenses.go:147 #: pkg/expenses.go:168
msgid "Select a contact." msgid "Select a contact."
msgstr "Escolliu un contacte." msgstr "Escolliu un contacte."
#: pkg/expenses.go:183 pkg/expenses.go:396 #: pkg/expenses.go:225 pkg/expenses.go:412
msgctxt "input" msgctxt "input"
msgid "Contact" msgid "Contact"
msgstr "Contacte" msgstr "Contacte"
#: pkg/expenses.go:189 #: pkg/expenses.go:231
msgctxt "input" msgctxt "input"
msgid "Invoice number" msgid "Invoice number"
msgstr "Número de factura" msgstr "Número de factura"
#: pkg/expenses.go:194 pkg/invoices.go:712 #: pkg/expenses.go:236 pkg/invoices.go:712
msgctxt "input" msgctxt "input"
msgid "Invoice Date" msgid "Invoice Date"
msgstr "Data de factura" msgstr "Data de factura"
#: pkg/expenses.go:206 #: pkg/expenses.go:251
msgctxt "input" msgctxt "input"
msgid "Amount" msgid "Amount"
msgstr "Import" msgstr "Import"
#: pkg/expenses.go:216 pkg/invoices.go:734 #: pkg/expenses.go:262 pkg/invoices.go:734
msgctxt "input" msgctxt "input"
msgid "File" msgid "File"
msgstr "Fitxer" msgstr "Fitxer"
#: pkg/expenses.go:222 pkg/expenses.go:421 #: pkg/expenses.go:268 pkg/expenses.go:437
msgctxt "input" msgctxt "input"
msgid "Expense Status" msgid "Expense Status"
msgstr "Estat de la despesa" msgstr "Estat de la despesa"
#: pkg/expenses.go:262 #: pkg/expenses.go:308
msgid "Selected contact is not valid." msgid "Selected contact is not valid."
msgstr "Heu seleccionat un contacte que no és vàlid." msgstr "Heu seleccionat un contacte que no és vàlid."
#: pkg/expenses.go:263 pkg/invoices.go:785 #: pkg/expenses.go:309 pkg/invoices.go:785
msgid "Invoice date must be a valid date." msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida." msgstr "La data de facturació ha de ser vàlida."
#: pkg/expenses.go:266 #: pkg/expenses.go:312
msgid "Amount can not be empty." msgid "Amount can not be empty."
msgstr "No podeu deixar limport en blanc." msgstr "No podeu deixar limport en blanc."
#: pkg/expenses.go:267 #: pkg/expenses.go:313
msgid "Amount must be a number greater than zero." msgid "Amount must be a number greater than zero."
msgstr "Limport ha de ser un número major a zero." msgstr "Limport ha de ser un número major a zero."
#: pkg/expenses.go:271 #: pkg/expenses.go:315
msgid "Selected expense status is not valid." msgid "Selected expense status is not valid."
msgstr "Heu seleccionat un estat de despesa que no és vàlid." msgstr "Heu seleccionat un estat de despesa que no és vàlid."
#: pkg/expenses.go:397 #: pkg/expenses.go:413
msgid "All contacts" msgid "All contacts"
msgstr "Tots els contactes" msgstr "Tots els contactes"
#: pkg/expenses.go:402 pkg/invoices.go:159 #: pkg/expenses.go:418 pkg/invoices.go:159
msgctxt "input" msgctxt "input"
msgid "Invoice Number" msgid "Invoice Number"
msgstr "Número de factura" msgstr "Número de factura"

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-07-12 20:01+0200\n" "POT-Creation-Date: 2023-07-13 20:43+0200\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -105,12 +105,14 @@ msgstr "Subtotal"
#: web/template/invoices/view.gohtml:115 web/template/invoices/edit.gohtml:75 #: web/template/invoices/view.gohtml:115 web/template/invoices/edit.gohtml:75
#: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82 #: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82
#: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75 #: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75
#: web/template/expenses/new.gohtml:45 web/template/expenses/edit.gohtml:49
msgctxt "title" msgctxt "title"
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93
#: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 #: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93
#: web/template/expenses/new.gohtml:55 web/template/expenses/edit.gohtml:59
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
msgstr "Actualizar" msgstr "Actualizar"
@ -118,7 +120,7 @@ msgstr "Actualizar"
#: web/template/invoices/new.gohtml:95 web/template/invoices/edit.gohtml:96 #: web/template/invoices/new.gohtml:95 web/template/invoices/edit.gohtml:96
#: web/template/quotes/new.gohtml:95 web/template/quotes/edit.gohtml:96 #: web/template/quotes/new.gohtml:95 web/template/quotes/edit.gohtml:96
#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53
#: web/template/expenses/new.gohtml:34 web/template/expenses/edit.gohtml:40 #: web/template/expenses/new.gohtml:58 web/template/expenses/edit.gohtml:62
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36
msgctxt "action" msgctxt "action"
msgid "Save" msgid "Save"
@ -719,37 +721,37 @@ msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:685 #: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:685
#: pkg/expenses.go:228 pkg/expenses.go:417 pkg/invoices.go:174 #: pkg/expenses.go:274 pkg/expenses.go:433 pkg/invoices.go:174
#: pkg/invoices.go:723 pkg/invoices.go:1295 pkg/contacts.go:145 #: pkg/invoices.go:723 pkg/invoices.go:1295 pkg/contacts.go:145
#: pkg/contacts.go:348 #: pkg/contacts.go:348
msgctxt "input" msgctxt "input"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
#: pkg/products.go:173 pkg/quote.go:178 pkg/expenses.go:427 pkg/invoices.go:178 #: pkg/products.go:173 pkg/quote.go:178 pkg/expenses.go:443 pkg/invoices.go:178
#: pkg/contacts.go:149 #: pkg/contacts.go:149
msgctxt "input" msgctxt "input"
msgid "Tags Condition" msgid "Tags Condition"
msgstr "Condición de las etiquetas" msgstr "Condición de las etiquetas"
#: pkg/products.go:177 pkg/quote.go:182 pkg/expenses.go:431 pkg/invoices.go:182 #: pkg/products.go:177 pkg/quote.go:182 pkg/expenses.go:447 pkg/invoices.go:182
#: pkg/contacts.go:153 #: pkg/contacts.go:153
msgctxt "tag condition" msgctxt "tag condition"
msgid "All" msgid "All"
msgstr "Todas" msgstr "Todas"
#: pkg/products.go:178 pkg/expenses.go:432 pkg/invoices.go:183 #: pkg/products.go:178 pkg/expenses.go:448 pkg/invoices.go:183
#: pkg/contacts.go:154 #: pkg/contacts.go:154
msgid "Invoices must have all the specified labels." msgid "Invoices must have all the specified labels."
msgstr "Las facturas deben tener todas las etiquetas." msgstr "Las facturas deben tener todas las etiquetas."
#: pkg/products.go:182 pkg/quote.go:187 pkg/expenses.go:436 pkg/invoices.go:187 #: pkg/products.go:182 pkg/quote.go:187 pkg/expenses.go:452 pkg/invoices.go:187
#: pkg/contacts.go:158 #: pkg/contacts.go:158
msgctxt "tag condition" msgctxt "tag condition"
msgid "Any" msgid "Any"
msgstr "Cualquiera" msgstr "Cualquiera"
#: pkg/products.go:183 pkg/expenses.go:437 pkg/invoices.go:188 #: pkg/products.go:183 pkg/expenses.go:453 pkg/invoices.go:188
#: pkg/contacts.go:159 #: pkg/contacts.go:159
msgid "Invoices must have at least one of the specified labels." msgid "Invoices must have at least one of the specified labels."
msgstr "Las facturas deben tener como mínimo una de las etiquetas." msgstr "Las facturas deben tener como mínimo una de las etiquetas."
@ -764,7 +766,7 @@ msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Precio" msgstr "Precio"
#: pkg/products.go:284 pkg/quote.go:925 pkg/expenses.go:200 #: pkg/products.go:284 pkg/quote.go:925 pkg/expenses.go:242
#: pkg/invoices.go:1040 #: pkg/invoices.go:1040
msgctxt "input" msgctxt "input"
msgid "Taxes" msgid "Taxes"
@ -783,12 +785,12 @@ msgstr "No podéis dejar el precio en blanco."
msgid "Price must be a number greater than zero." msgid "Price must be a number greater than zero."
msgstr "El precio tiene que ser un número mayor a cero." msgstr "El precio tiene que ser un número mayor a cero."
#: pkg/products.go:313 pkg/quote.go:984 pkg/expenses.go:264 pkg/expenses.go:269 #: pkg/products.go:313 pkg/quote.go:984 pkg/expenses.go:310
#: pkg/invoices.go:1099 #: pkg/invoices.go:1099
msgid "Selected tax is not valid." msgid "Selected tax is not valid."
msgstr "Habéis escogido un impuesto que no es válido." msgstr "Habéis escogido un impuesto que no es válido."
#: pkg/products.go:314 pkg/quote.go:985 pkg/expenses.go:265 pkg/expenses.go:270 #: pkg/products.go:314 pkg/quote.go:985 pkg/expenses.go:311
#: pkg/invoices.go:1100 #: pkg/invoices.go:1100
msgid "You can only select a tax of each class." msgid "You can only select a tax of each class."
msgstr "Solo podéis escoger un impuesto de cada clase." msgstr "Solo podéis escoger un impuesto de cada clase."
@ -1012,7 +1014,7 @@ msgctxt "input"
msgid "Quotation Status" msgid "Quotation Status"
msgstr "Estado del presupuesto" msgstr "Estado del presupuesto"
#: pkg/quote.go:154 pkg/expenses.go:422 pkg/invoices.go:154 #: pkg/quote.go:154 pkg/expenses.go:438 pkg/invoices.go:154
msgid "All status" msgid "All status"
msgstr "Todos los estados" msgstr "Todos los estados"
@ -1021,12 +1023,12 @@ msgctxt "input"
msgid "Quotation Number" msgid "Quotation Number"
msgstr "Número de presupuesto" msgstr "Número de presupuesto"
#: pkg/quote.go:164 pkg/expenses.go:407 pkg/invoices.go:164 #: pkg/quote.go:164 pkg/expenses.go:423 pkg/invoices.go:164
msgctxt "input" msgctxt "input"
msgid "From Date" msgid "From Date"
msgstr "A partir de la fecha" msgstr "A partir de la fecha"
#: pkg/quote.go:169 pkg/expenses.go:412 pkg/invoices.go:169 #: pkg/quote.go:169 pkg/expenses.go:428 pkg/invoices.go:169
msgctxt "input" msgctxt "input"
msgid "To Date" msgid "To Date"
msgstr "Hasta la fecha" msgstr "Hasta la fecha"
@ -1043,8 +1045,8 @@ msgstr "Los presupuestos deben tener como mínimo una de las etiquetas."
msgid "quotations.zip" msgid "quotations.zip"
msgstr "presupuestos.zip" msgstr "presupuestos.zip"
#: pkg/quote.go:611 pkg/quote.go:1140 pkg/quote.go:1148 pkg/invoices.go:654 #: pkg/quote.go:611 pkg/quote.go:1140 pkg/quote.go:1148 pkg/expenses.go:611
#: pkg/invoices.go:1270 pkg/invoices.go:1278 #: pkg/invoices.go:654 pkg/invoices.go:1270 pkg/invoices.go:1278
msgid "Invalid action" msgid "Invalid action"
msgstr "Acción inválida." msgstr "Acción inválida."
@ -1198,65 +1200,65 @@ msgctxt "period option"
msgid "Previous year" msgid "Previous year"
msgstr "Año anterior" msgstr "Año anterior"
#: pkg/expenses.go:147 #: pkg/expenses.go:168
msgid "Select a contact." msgid "Select a contact."
msgstr "Escoged un contacto" msgstr "Escoged un contacto"
#: pkg/expenses.go:183 pkg/expenses.go:396 #: pkg/expenses.go:225 pkg/expenses.go:412
msgctxt "input" msgctxt "input"
msgid "Contact" msgid "Contact"
msgstr "Contacto" msgstr "Contacto"
#: pkg/expenses.go:189 #: pkg/expenses.go:231
msgctxt "input" msgctxt "input"
msgid "Invoice number" msgid "Invoice number"
msgstr "Número de factura" msgstr "Número de factura"
#: pkg/expenses.go:194 pkg/invoices.go:712 #: pkg/expenses.go:236 pkg/invoices.go:712
msgctxt "input" msgctxt "input"
msgid "Invoice Date" msgid "Invoice Date"
msgstr "Fecha de factura" msgstr "Fecha de factura"
#: pkg/expenses.go:206 #: pkg/expenses.go:251
msgctxt "input" msgctxt "input"
msgid "Amount" msgid "Amount"
msgstr "Importe" msgstr "Importe"
#: pkg/expenses.go:216 pkg/invoices.go:734 #: pkg/expenses.go:262 pkg/invoices.go:734
msgctxt "input" msgctxt "input"
msgid "File" msgid "File"
msgstr "Archivo" msgstr "Archivo"
#: pkg/expenses.go:222 pkg/expenses.go:421 #: pkg/expenses.go:268 pkg/expenses.go:437
msgctxt "input" msgctxt "input"
msgid "Expense Status" msgid "Expense Status"
msgstr "Estado del gasto" msgstr "Estado del gasto"
#: pkg/expenses.go:262 #: pkg/expenses.go:308
msgid "Selected contact is not valid." msgid "Selected contact is not valid."
msgstr "Habéis escogido un contacto que no es válido." msgstr "Habéis escogido un contacto que no es válido."
#: pkg/expenses.go:263 pkg/invoices.go:785 #: pkg/expenses.go:309 pkg/invoices.go:785
msgid "Invoice date must be a valid date." msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida." msgstr "La fecha de factura debe ser válida."
#: pkg/expenses.go:266 #: pkg/expenses.go:312
msgid "Amount can not be empty." msgid "Amount can not be empty."
msgstr "No podéis dejar el importe en blanco." msgstr "No podéis dejar el importe en blanco."
#: pkg/expenses.go:267 #: pkg/expenses.go:313
msgid "Amount must be a number greater than zero." msgid "Amount must be a number greater than zero."
msgstr "El importe tiene que ser un número mayor a cero." msgstr "El importe tiene que ser un número mayor a cero."
#: pkg/expenses.go:271 #: pkg/expenses.go:315
msgid "Selected expense status is not valid." msgid "Selected expense status is not valid."
msgstr "Habéis escogido un estado de gasto que no es válido." msgstr "Habéis escogido un estado de gasto que no es válido."
#: pkg/expenses.go:397 #: pkg/expenses.go:413
msgid "All contacts" msgid "All contacts"
msgstr "Todos los contactos" msgstr "Todos los contactos"
#: pkg/expenses.go:402 pkg/invoices.go:159 #: pkg/expenses.go:418 pkg/invoices.go:159
msgctxt "input" msgctxt "input"
msgid "Invoice Number" msgid "Invoice Number"
msgstr "Número de factura" msgstr "Número de factura"

View File

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

View File

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

View File

@ -124,3 +124,5 @@ add_expense [add_expense@v1 expense_status expense_expense_status] 2023-07-11T13
edit_expense [edit_expense@v1 expense_status expense_expense_status] 2023-07-11T13:21:17Z jordi fita mas <jordi@tandem.blog> # Add expense_status parameter to edit_expense edit_expense [edit_expense@v1 expense_status expense_expense_status] 2023-07-11T13:21:17Z jordi fita mas <jordi@tandem.blog> # Add expense_status parameter to edit_expense
invoice_attachment [schema_numerus roles invoice] 2023-07-12T17:10:58Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice attachment invoice_attachment [schema_numerus roles invoice] 2023-07-12T17:10:58Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice attachment
attach_to_invoice [schema_numerus roles invoice invoice_attachment] 2023-07-12T17:21:19Z jordi fita mas <jordi@tandem.blog> # Add function to attachment a document to invoices attach_to_invoice [schema_numerus roles invoice invoice_attachment] 2023-07-12T17:21:19Z jordi fita mas <jordi@tandem.blog> # Add function to attachment a document to invoices
new_expense_amount [schema_numerus] 2023-07-13T17:45:33Z jordi fita mas <jordi@tandem.blog> # Add type to return when computing new expense amounts
compute_new_expense_amount [schema_numerus roles company tax new_expense_amount] 2023-07-13T17:34:12Z jordi fita mas <jordi@tandem.blog> # Add function to compute the taxes and total for a new expense

View File

@ -0,0 +1,80 @@
-- Test compute_new_expense_amount
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(14);
set search_path to numerus, auth, public;
select has_function('numerus', 'compute_new_expense_amount', array ['integer', 'text', 'integer[]']);
select function_lang_is('numerus', 'compute_new_expense_amount', array ['integer', 'text', 'integer[]'], 'plpgsql');
select function_returns('numerus', 'compute_new_expense_amount', array ['integer', 'text', 'integer[]'], 'new_expense_amount');
select isnt_definer('numerus', 'compute_new_expense_amount', array ['integer', 'text', 'integer[]']);
select volatility_is('numerus', 'compute_new_expense_amount', array ['integer', 'text', 'integer[]'], 'stable');
select function_privs_are('numerus', 'compute_new_expense_amount', array ['integer', 'text', 'integer[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'compute_new_expense_amount', array ['integer', 'text', 'integer[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'compute_new_expense_amount', array ['integer', 'text', 'integer[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'compute_new_expense_amount', array ['integer', 'text', 'integer[]'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate tax cascade;
truncate tax_class cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into tax_class (tax_class_id, company_id, name)
values (11, 1, 'tax')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (2, 1, 11, 'IRPF -15 %', -0.15)
, (3, 1, 11, 'IVA 4 %', 0.04)
, (4, 1, 11, 'IVA 10 %', 0.10)
, (5, 1, 11, 'IVA 21 %', 0.21)
;
select is(
compute_new_expense_amount(1, '', array[]::integer[]),
'("{}",0.00)'::new_expense_amount
);
select is(
compute_new_expense_amount(1, '4.60', array[2,5,3]),
'("{{IRPF -15 %,-0.69},{IVA 4 %,0.18},{IVA 21 %,0.97}}",5.06)'::new_expense_amount
);
select is(
compute_new_expense_amount(1, '17.32', array[2,4,5]),
'("{{IRPF -15 %,-2.60},{IVA 10 %,1.73},{IVA 21 %,3.64}}",20.09)'::new_expense_amount
);
select is(
compute_new_expense_amount(1, '52.17', array[3,4,5]),
'("{{IVA 4 %,2.09},{IVA 10 %,5.22},{IVA 21 %,10.96}}",70.44)'::new_expense_amount
);
select is(
compute_new_expense_amount(1, '62.16', array[]::integer[]),
'("{}",62.16)'::new_expense_amount
);
select *
from finish();
rollback;

View File

@ -0,0 +1,20 @@
-- Test new_expense_amount
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(4);
set search_path to numerus, public;
select has_composite('numerus', 'new_expense_amount', 'Composite type numerus.new_expense_amount should exist');
select columns_are('numerus', 'new_expense_amount', array['taxes', 'total']);
select col_type_is('numerus'::name, 'new_expense_amount'::name, 'taxes'::name, 'text[]');
select col_type_is('numerus'::name, 'new_expense_amount'::name, 'total'::name, 'text');
select *
from finish();
rollback;

View File

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

View File

@ -0,0 +1,7 @@
-- Verify numerus:new_expense_amount on pg
begin;
select pg_catalog.has_type_privilege('numerus.new_expense_amount', 'usage');
rollback;

View File

@ -19,9 +19,10 @@
<section id="new-expense-dialog-content" data-hx-target="main"> <section id="new-expense-dialog-content" data-hx-target="main">
<h2>{{ printf (pgettext "Edit Expense “%s”" "title") .Slug }}</h2> <h2>{{ printf (pgettext "Edit Expense “%s”" "title") .Slug }}</h2>
<form enctype="multipart/form-data" method="POST" action="{{ companyURI "/expenses/" }}{{ .Slug }}" <form enctype="multipart/form-data" method="POST" action="{{ companyURI "/expenses/" }}{{ .Slug }}"
data-hx-boost="true"> data-hx-boost="true"
data-hx-swap="innerHTML show:false"
>
{{ csrfToken }} {{ csrfToken }}
{{ putMethod }}
{{ with .Form -}} {{ with .Form -}}
<div class="expenses-data"> <div class="expenses-data">
@ -36,9 +37,36 @@
</div> </div>
{{- end }} {{- end }}
<fieldset> <table id="invoice-summary">
<button class="primary" type="submit">{{( pgettext "Save" "action" )}}</button> <tbody>
{{- range $tax := .Taxes }}
<tr>
<th scope="row">{{ index . 0 }}</th>
<td class="numeric">{{ index . 1 | formatPrice }}</td>
</tr>
{{- end }}
<tr>
<th scope="row">{{(pgettext "Total" "title")}}</th>
<td class="numeric">{{ .Total | formatPrice }}</td>
</tr>
</tbody>
</table>
<fieldset class="button-bar">
<button formnovalidate
id="recompute-button"
name="action" value="update"
type="submit">{{( pgettext "Update" "action" )}}</button>
<button class="primary"
name="_method" value="PUT"
type="submit">{{( pgettext "Save" "action" )}}</button>
</fieldset> </fieldset>
</form> </form>
</section> </section>
<script>
document.body.addEventListener('recompute', function () {
document.getElementById('recompute-button').click();
});
</script>
{{- end }} {{- end }}

View File

@ -18,21 +18,53 @@
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.expenseForm*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.expenseForm*/ -}}
<section id="new-expense-dialog-content" data-hx-target="main"> <section id="new-expense-dialog-content" data-hx-target="main">
<h2>{{(pgettext "New Expense" "title")}}</h2> <h2>{{(pgettext "New Expense" "title")}}</h2>
<form enctype="multipart/form-data" method="POST" action="{{ companyURI "/expenses" }}" data-hx-boost="true"> <form enctype="multipart/form-data" method="POST" action="{{ companyURI "/expenses" }}"
data-hx-swap="innerHTML show:false"
data-hx-boost="true">
{{ csrfToken }} {{ csrfToken }}
<div class="expenses-data"> {{ with .Form }}
{{ template "select-field" .Invoicer }} <div class="expenses-data">
{{ template "input-field" .InvoiceNumber }} {{ template "select-field" .Invoicer }}
{{ template "input-field" .InvoiceDate }} {{ template "input-field" .InvoiceNumber }}
{{ template "input-field" .Amount }} {{ template "input-field" .InvoiceDate }}
{{ template "select-field" .Tax }} {{ template "input-field" .Amount }}
{{ template "tags-field" .Tags }} {{ template "select-field" .Tax }}
{{ template "select-field" .ExpenseStatus }} {{ template "tags-field" .Tags }}
{{ template "file-field" .File }} {{ template "select-field" .ExpenseStatus }}
</div> {{ template "file-field" .File }}
<fieldset> </div>
<button class="primary" type="submit">{{( pgettext "Save" "action" )}}</button> {{ end }}
<table id="invoice-summary">
<tbody>
{{- range $tax := .Taxes }}
<tr>
<th scope="row">{{ index . 0 }}</th>
<td class="numeric">{{ index . 1 | formatPrice }}</td>
</tr>
{{- end }}
<tr>
<th scope="row">{{(pgettext "Total" "title")}}</th>
<td class="numeric">{{ .Total | formatPrice }}</td>
</tr>
</tbody>
</table>
<fieldset class="button-bar">
<button formnovalidate
id="recompute-button"
name="action" value="update"
type="submit">{{( pgettext "Update" "action" )}}</button>
<button class="primary"
name="action" value="add"
type="submit">{{( pgettext "Save" "action" )}}</button>
</fieldset> </fieldset>
</form> </form>
</section> </section>
<script>
document.body.addEventListener('recompute', function () {
document.getElementById('recompute-button').click();
});
</script>
{{- end }} {{- end }}