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:
parent
7d55e949fc
commit
a7c1df20f0
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||||
|
|
140
pkg/expenses.go
140
pkg/expenses.go
|
@ -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 (
|
||||||
|
select expense_id
|
||||||
|
, expense.amount as subtotal
|
||||||
|
, coalesce(sum(tax.amount)::integer, 0) as taxes
|
||||||
|
, currency_code
|
||||||
from expense
|
from expense
|
||||||
join currency using (currency_code)
|
left join expense_tax_amount as tax using (expense_id)
|
||||||
where (%s)
|
where (%s)
|
||||||
|
group by expense_id
|
||||||
|
, expense.amount
|
||||||
|
, currency_code
|
||||||
|
) as expense
|
||||||
|
join currency using (currency_code)
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
62
po/ca.po
62
po/ca.po
|
@ -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 l’import en blanc."
|
msgstr "No podeu deixar l’import 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 "L’import ha de ser un número major a zero."
|
msgstr "L’import 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"
|
||||||
|
|
62
po/es.po
62
po/es.po
|
@ -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"
|
||||||
|
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert numerus:new_expense_amount from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop type if exists numerus.new_expense_amount;
|
||||||
|
|
||||||
|
commit;
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Verify numerus:new_expense_amount on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select pg_catalog.has_type_privilege('numerus.new_expense_amount', 'usage');
|
||||||
|
|
||||||
|
rollback;
|
|
@ -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 }}
|
||||||
|
|
|
@ -18,8 +18,11 @@
|
||||||
{{- /*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 }}
|
||||||
|
{{ with .Form }}
|
||||||
<div class="expenses-data">
|
<div class="expenses-data">
|
||||||
{{ template "select-field" .Invoicer }}
|
{{ template "select-field" .Invoicer }}
|
||||||
{{ template "input-field" .InvoiceNumber }}
|
{{ template "input-field" .InvoiceNumber }}
|
||||||
|
@ -30,9 +33,38 @@
|
||||||
{{ template "select-field" .ExpenseStatus }}
|
{{ template "select-field" .ExpenseStatus }}
|
||||||
{{ template "file-field" .File }}
|
{{ template "file-field" .File }}
|
||||||
</div>
|
</div>
|
||||||
<fieldset>
|
{{ end }}
|
||||||
<button class="primary" type="submit">{{( pgettext "Save" "action" )}}</button>
|
|
||||||
|
<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 }}
|
||||||
|
|
Loading…
Reference in New Issue