Compare commits

..

2 Commits

9 changed files with 626 additions and 75 deletions

239
pkg/expenses.go Normal file
View File

@ -0,0 +1,239 @@
package pkg
import (
"context"
"fmt"
"github.com/julienschmidt/httprouter"
"html/template"
"math"
"net/http"
"time"
)
type ExpenseEntry struct {
Slug string
InvoiceDate time.Time
InvoiceNumber string
Amount string
InvoicerName string
Tags []string
}
type expensesIndexPage struct {
Expenses []*ExpenseEntry
}
func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
company := mustGetCompany(r)
page := &expensesIndexPage{
Expenses: mustCollectExpenseEntries(r.Context(), conn, company),
}
mustRenderMainTemplate(w, r, "expenses/index.gohtml", page)
}
func mustCollectExpenseEntries(ctx context.Context, conn *Conn, company *Company) []*ExpenseEntry {
rows, err := conn.Query(ctx, `
select expense.slug
, invoice_date
, invoice_number
, to_price(amount, decimal_digits)
, contact.business_name
, expense.tags
from expense
join contact using (contact_id)
join currency using (currency_code)
where expense.company_id = $1
order by invoice_date
`, company.Id)
if err != nil {
panic(err)
}
defer rows.Close()
var entries []*ExpenseEntry
for rows.Next() {
entry := &ExpenseEntry{}
err = rows.Scan(&entry.Slug, &entry.InvoiceDate, &entry.InvoiceNumber, &entry.Amount, &entry.InvoicerName, &entry.Tags)
if err != nil {
panic(err)
}
entries = append(entries, entry)
}
if rows.Err() != nil {
panic(rows.Err())
}
return entries
}
func ServeExpenseForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
form := newExpenseForm(r.Context(), conn, locale, company)
slug := params[0].Value
if slug == "new" {
w.WriteHeader(http.StatusOK)
form.InvoiceDate.Val = time.Now().Format("2006-01-02")
mustRenderNewExpenseForm(w, r, form)
return
}
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
http.NotFound(w, r)
return
}
w.WriteHeader(http.StatusOK)
mustRenderEditExpenseForm(w, r, slug, form)
}
func mustRenderNewExpenseForm(w http.ResponseWriter, r *http.Request, form *expenseForm) {
locale := getLocale(r)
form.Invoicer.EmptyLabel = gettext("Select a contact.", locale)
mustRenderMainTemplate(w, r, "expenses/new.gohtml", form)
}
func mustRenderEditExpenseForm(w http.ResponseWriter, r *http.Request, slug string, form *expenseForm) {
page := &editExpensePage{
Slug: slug,
Form: form,
}
mustRenderMainTemplate(w, r, "expenses/edit.gohtml", page)
}
type editExpensePage struct {
Slug string
Form *expenseForm
}
type expenseForm struct {
locale *Locale
company *Company
Invoicer *SelectField
InvoiceNumber *InputField
InvoiceDate *InputField
Tax *SelectField
Amount *InputField
Tags *TagsField
}
func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *expenseForm {
return &expenseForm{
locale: locale,
company: company,
Invoicer: &SelectField{
Name: "invoicer",
Label: pgettext("input", "Contact", locale),
Required: true,
Options: mustGetContactOptions(ctx, conn, company),
},
InvoiceNumber: &InputField{
Name: "invoice_number",
Label: pgettext("input", "Invoice number", locale),
Type: "text",
},
InvoiceDate: &InputField{
Name: "invoice_date",
Label: pgettext("input", "Invoice Date", locale),
Required: true,
Type: "date",
},
Tax: &SelectField{
Name: "tax",
Label: pgettext("input", "Taxes", locale),
Multiple: true,
Options: mustGetTaxOptions(ctx, conn, company),
},
Amount: &InputField{
Name: "amount",
Label: pgettext("input", "Amount", locale),
Type: "number",
Required: true,
Attributes: []template.HTMLAttr{
`min="0"`,
template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
},
},
Tags: &TagsField{
Name: "tags",
Label: pgettext("input", "Tags", locale),
},
}
}
func (form *expenseForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
form.Invoicer.FillValue(r)
form.InvoiceNumber.FillValue(r)
form.InvoiceDate.FillValue(r)
form.Tax.FillValue(r)
form.Amount.FillValue(r)
form.Tags.FillValue(r)
return nil
}
func (form *expenseForm) Validate() bool {
validator := newFormValidator()
validator.CheckValidSelectOption(form.Invoicer, gettext("Selected contact is not valid.", form.locale))
validator.CheckValidDate(form.InvoiceDate, gettext("Invoice date must be a valid date.", form.locale))
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
if validator.CheckRequiredInput(form.Amount, gettext("Amount can not be empty.", form.locale)) {
validator.CheckValidDecimal(form.Amount, form.company.MinCents(), math.MaxFloat64, gettext("Amount must be a number greater than zero.", form.locale))
}
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
return validator.AllOK()
}
func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
return !notFoundErrorOrPanic(conn.QueryRow(ctx, `
select contact_id
, invoice_number
, invoice_date
, to_price(amount, decimal_digits)
, array_agg(tax_id)
, array_to_string(tags, ',')
from expense
left join expense_tax using (expense_id)
join currency using (currency_code)
where expense.slug = $1
group by contact_id
, invoice_number
, invoice_date
, amount
, decimal_digits
, tags
`, slug).Scan(
form.Invoicer,
form.InvoiceNumber,
form.InvoiceDate,
form.Amount,
form.Tax,
form.Tags))
}
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)
conn.MustExec(r.Context(), "select add_expense($1, $2, $3, $4, $5, $6, $7)", company.Id, form.InvoiceDate, form.Invoicer, form.InvoiceNumber, form.Amount, taxes, form.Tags)
htmxRedirect(w, r, companyURI(company, "/expenses"))
}

View File

@ -159,7 +159,7 @@ func newInvoiceFilterForm(ctx context.Context, conn *Conn, locale *Locale, compa
Name: "customer", Name: "customer",
Label: pgettext("input", "Customer", locale), Label: pgettext("input", "Customer", locale),
EmptyLabel: gettext("All customers", locale), EmptyLabel: gettext("All customers", locale),
Options: MustGetOptions(ctx, conn, "select contact_id::text, business_name from contact where company_id = $1 order by business_name", company.Id), Options: mustGetContactOptions(ctx, conn, company),
}, },
InvoiceStatus: &SelectField{ InvoiceStatus: &SelectField{
Name: "invoice_status", Name: "invoice_status",
@ -579,7 +579,7 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Name: "customer", Name: "customer",
Label: pgettext("input", "Customer", locale), Label: pgettext("input", "Customer", locale),
Required: true, Required: true,
Options: MustGetOptions(ctx, conn, "select contact_id::text, business_name from contact where company_id = $1 order by business_name", company.Id), Options: mustGetContactOptions(ctx, conn, company),
}, },
Date: &InputField{ Date: &InputField{
Name: "date", Name: "date",
@ -738,6 +738,10 @@ func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*Sel
return MustGetGroupedOptions(ctx, conn, "select tax_id::text, tax.name, tax_class.name from tax join tax_class using (tax_class_id) where tax.company_id = $1 order by tax_class.name, tax.name", company.Id) return MustGetGroupedOptions(ctx, conn, "select tax_id::text, tax.name, tax_class.name from tax join tax_class using (tax_class_id) where tax.company_id = $1 order by tax_class.name, tax.name", company.Id)
} }
func mustGetContactOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption {
return MustGetOptions(ctx, conn, "select contact_id::text, business_name from contact where company_id = $1 order by business_name", company.Id)
}
type invoiceProductForm struct { type invoiceProductForm struct {
locale *Locale locale *Locale
company *Company company *Company

View File

@ -34,6 +34,9 @@ func NewRouter(db *Db) http.Handler {
companyRouter.PUT("/invoices/:slug/tags", HandleUpdateInvoiceTags) companyRouter.PUT("/invoices/:slug/tags", HandleUpdateInvoiceTags)
companyRouter.GET("/invoices/:slug/tags/edit", ServeEditInvoiceTags) companyRouter.GET("/invoices/:slug/tags/edit", ServeEditInvoiceTags)
companyRouter.GET("/search/products", HandleProductSearch) companyRouter.GET("/search/products", HandleProductSearch)
companyRouter.GET("/expenses", IndexExpenses)
companyRouter.POST("/expenses", HandleAddExpense)
companyRouter.GET("/expenses/:slug", ServeExpenseForm)
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
mustRenderMainTemplate(w, r, "dashboard.gohtml", nil) mustRenderMainTemplate(w, r, "dashboard.gohtml", nil)
}) })

158
po/ca.po
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-04-28 00:05+0200\n" "POT-Creation-Date: 2023-05-03 12:40+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"
@ -27,9 +27,10 @@ msgstr "Afegeix productes a la factura"
#: web/template/invoices/index.gohtml:9 web/template/invoices/view.gohtml:9 #: web/template/invoices/index.gohtml:9 web/template/invoices/view.gohtml:9
#: web/template/invoices/edit.gohtml:9 web/template/contacts/new.gohtml:9 #: web/template/invoices/edit.gohtml:9 web/template/contacts/new.gohtml:9
#: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10 #: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10
#: web/template/profile.gohtml:9 web/template/tax-details.gohtml:9 #: web/template/profile.gohtml:9 web/template/expenses/new.gohtml:10
#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:9 #: web/template/expenses/index.gohtml:10 web/template/expenses/edit.gohtml:10
#: web/template/products/edit.gohtml:10 #: web/template/tax-details.gohtml:9 web/template/products/new.gohtml:9
#: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10
msgctxt "title" msgctxt "title"
msgid "Home" msgid "Home"
msgstr "Inici" msgstr "Inici"
@ -48,7 +49,7 @@ msgid "New Invoice"
msgstr "Nova factura" msgstr "Nova factura"
#: web/template/invoices/products.gohtml:48 #: web/template/invoices/products.gohtml:48
#: web/template/products/index.gohtml:38 #: web/template/expenses/index.gohtml:25 web/template/products/index.gohtml:38
msgctxt "product" msgctxt "product"
msgid "All" msgid "All"
msgstr "Tots" msgstr "Tots"
@ -139,7 +140,7 @@ msgctxt "title"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
#: web/template/invoices/index.gohtml:52 #: web/template/invoices/index.gohtml:52 web/template/expenses/index.gohtml:29
msgctxt "title" msgctxt "title"
msgid "Amount" msgid "Amount"
msgstr "Import" msgstr "Import"
@ -245,10 +246,15 @@ msgstr "Factures"
#: web/template/app.gohtml:48 #: web/template/app.gohtml:48
msgctxt "nav" msgctxt "nav"
msgid "Expenses"
msgstr "Despeses"
#: web/template/app.gohtml:49
msgctxt "nav"
msgid "Products" msgid "Products"
msgstr "Productes" msgstr "Productes"
#: web/template/app.gohtml:49 #: web/template/app.gohtml:50
msgctxt "nav" msgctxt "nav"
msgid "Contacts" msgid "Contacts"
msgstr "Contactes" msgstr "Contactes"
@ -335,6 +341,52 @@ msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Desa canvis" msgstr "Desa canvis"
#: web/template/expenses/new.gohtml:3 web/template/expenses/new.gohtml:12
#: web/template/expenses/new.gohtml:20
msgctxt "title"
msgid "New Expense"
msgstr "Nova despesa"
#: web/template/expenses/new.gohtml:11 web/template/expenses/index.gohtml:3
#: web/template/expenses/index.gohtml:11 web/template/expenses/edit.gohtml:11
msgctxt "title"
msgid "Expenses"
msgstr "Despeses"
#: web/template/expenses/new.gohtml:32 web/template/expenses/index.gohtml:15
msgctxt "action"
msgid "New expense"
msgstr "Nova despesa"
#: web/template/expenses/index.gohtml:26
msgctxt "title"
msgid "Contact"
msgstr "Contacte"
#: web/template/expenses/index.gohtml:27
msgctxt "title"
msgid "Invoice Date"
msgstr "Data de factura"
#: web/template/expenses/index.gohtml:28
msgctxt "title"
msgid "Invoice Number"
msgstr "Número de factura"
#: web/template/expenses/index.gohtml:52
msgid "No expenses added yet."
msgstr "No hi ha cap despesa."
#: web/template/expenses/edit.gohtml:3 web/template/expenses/edit.gohtml:20
msgctxt "title"
msgid "Edit Expense “%s”"
msgstr "Edició de la despesa «%s»"
#: web/template/expenses/edit.gohtml:35
msgctxt "action"
msgid "Update expense"
msgstr "Actualitza despesa"
#: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:10 #: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:10
#: web/template/tax-details.gohtml:18 #: web/template/tax-details.gohtml:18
msgctxt "title" msgctxt "title"
@ -460,13 +512,14 @@ msgstr "No podeu deixar la contrasenya en blanc."
msgid "Invalid user or password." msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes." msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/products.go:161 pkg/products.go:260 pkg/invoices.go:771 #: pkg/products.go:161 pkg/products.go:260 pkg/invoices.go:776
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: pkg/products.go:166 pkg/products.go:287 pkg/invoices.go:187 #: pkg/products.go:166 pkg/products.go:287 pkg/expenses.go:158
#: pkg/invoices.go:597 pkg/invoices.go:1031 pkg/contacts.go:256 #: pkg/invoices.go:187 pkg/invoices.go:597 pkg/invoices.go:1042
#: pkg/contacts.go:256
msgctxt "input" msgctxt "input"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
@ -494,38 +547,40 @@ msgstr "Qualsevol"
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."
#: pkg/products.go:266 pkg/invoices.go:785 #: pkg/products.go:266 pkg/invoices.go:790
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: pkg/products.go:271 pkg/invoices.go:789 #: pkg/products.go:271 pkg/invoices.go:794
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Preu" msgstr "Preu"
#: pkg/products.go:281 pkg/invoices.go:815 #: pkg/products.go:281 pkg/expenses.go:142 pkg/invoices.go:823
msgctxt "input" msgctxt "input"
msgid "Taxes" msgid "Taxes"
msgstr "Imposts" msgstr "Imposts"
#: pkg/products.go:306 pkg/profile.go:92 pkg/invoices.go:856 #: pkg/products.go:306 pkg/profile.go:92 pkg/invoices.go:867
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc." msgstr "No podeu deixar el nom en blanc."
#: pkg/products.go:307 pkg/invoices.go:857 #: pkg/products.go:307 pkg/invoices.go:868
msgid "Price can not be empty." msgid "Price can not be empty."
msgstr "No podeu deixar el preu en blanc." msgstr "No podeu deixar el preu en blanc."
#: pkg/products.go:308 pkg/invoices.go:858 #: pkg/products.go:308 pkg/invoices.go:869
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:310 pkg/invoices.go:866 #: pkg/products.go:310 pkg/expenses.go:180 pkg/expenses.go:185
#: pkg/invoices.go:877
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:311 pkg/invoices.go:867 #: pkg/products.go:311 pkg/expenses.go:181 pkg/expenses.go:186
#: pkg/invoices.go:878
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."
@ -633,6 +688,46 @@ msgstr "La confirmació no és igual a la contrasenya."
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid." msgstr "Heu seleccionat un idioma que no és vàlid."
#: pkg/expenses.go:91
msgid "Select a contact."
msgstr "Escolliu un contacte."
#: pkg/expenses.go:125
msgctxt "input"
msgid "Contact"
msgstr "Contacte"
#: pkg/expenses.go:131
msgctxt "input"
msgid "Invoice number"
msgstr "Número de factura"
#: pkg/expenses.go:136 pkg/invoices.go:586
msgctxt "input"
msgid "Invoice Date"
msgstr "Data de factura"
#: pkg/expenses.go:148
msgctxt "input"
msgid "Amount"
msgstr "Import"
#: pkg/expenses.go:178
msgid "Selected contact is not valid."
msgstr "Heu seleccionat un contacte que no és vàlid."
#: pkg/expenses.go:179 pkg/invoices.go:641
msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida."
#: pkg/expenses.go:182
msgid "Amount can not be empty."
msgstr "No podeu deixar limport en blanc."
#: pkg/expenses.go:183
msgid "Amount must be a number greater than zero."
msgstr "Limport ha de ser un número major a zero."
#: pkg/invoices.go:160 pkg/invoices.go:580 #: pkg/invoices.go:160 pkg/invoices.go:580
msgctxt "input" msgctxt "input"
msgid "Customer" msgid "Customer"
@ -674,15 +769,10 @@ msgstr "Escolliu un client a facturar."
msgid "invoices.zip" msgid "invoices.zip"
msgstr "factures.zip" msgstr "factures.zip"
#: pkg/invoices.go:530 pkg/invoices.go:1017 #: pkg/invoices.go:530 pkg/invoices.go:1028
msgid "Invalid action" msgid "Invalid action"
msgstr "Acció invàlida." msgstr "Acció invàlida."
#: pkg/invoices.go:586
msgctxt "input"
msgid "Invoice Date"
msgstr "Data de factura"
#: pkg/invoices.go:592 #: pkg/invoices.go:592
msgctxt "input" msgctxt "input"
msgid "Notes" msgid "Notes"
@ -705,46 +795,42 @@ msgstr "Heu seleccionat un client que no és vàlid."
msgid "Invoice date can not be empty." msgid "Invoice date can not be empty."
msgstr "No podeu deixar la data de la factura en blanc." msgstr "No podeu deixar la data de la factura en blanc."
#: pkg/invoices.go:641
msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida."
#: pkg/invoices.go:643 #: pkg/invoices.go:643
msgid "Selected payment method is not valid." msgid "Selected payment method is not valid."
msgstr "Heu seleccionat un mètode de pagament que no és vàlid." msgstr "Heu seleccionat un mètode de pagament que no és vàlid."
#: pkg/invoices.go:761 pkg/invoices.go:766 #: pkg/invoices.go:766 pkg/invoices.go:771
msgctxt "input" msgctxt "input"
msgid "Id" msgid "Id"
msgstr "Identificador" msgstr "Identificador"
#: pkg/invoices.go:798 #: pkg/invoices.go:804
msgctxt "input" msgctxt "input"
msgid "Quantity" msgid "Quantity"
msgstr "Quantitat" msgstr "Quantitat"
#: pkg/invoices.go:806 #: pkg/invoices.go:813
msgctxt "input" msgctxt "input"
msgid "Discount (%)" msgid "Discount (%)"
msgstr "Descompte (%)" msgstr "Descompte (%)"
#: pkg/invoices.go:854 #: pkg/invoices.go:865
msgid "Product ID must be a number greater than zero." msgid "Product ID must be a number greater than zero."
msgstr "LID del producte ha de ser un número major a zero." msgstr "LID del producte ha de ser un número major a zero."
#: pkg/invoices.go:860 #: pkg/invoices.go:871
msgid "Quantity can not be empty." msgid "Quantity can not be empty."
msgstr "No podeu deixar la quantitat en blanc." msgstr "No podeu deixar la quantitat en blanc."
#: pkg/invoices.go:861 #: pkg/invoices.go:872
msgid "Quantity must be a number greater than zero." msgid "Quantity must be a number greater than zero."
msgstr "La quantitat ha de ser un número major a zero." msgstr "La quantitat ha de ser un número major a zero."
#: pkg/invoices.go:863 #: pkg/invoices.go:874
msgid "Discount can not be empty." msgid "Discount can not be empty."
msgstr "No podeu deixar el descompte en blanc." msgstr "No podeu deixar el descompte en blanc."
#: pkg/invoices.go:864 #: pkg/invoices.go:875
msgid "Discount must be a percentage between 0 and 100." msgid "Discount must be a percentage between 0 and 100."
msgstr "El descompte ha de ser un percentatge entre 0 i 100." msgstr "El descompte ha de ser un percentatge entre 0 i 100."

160
po/es.po
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-04-28 00:05+0200\n" "POT-Creation-Date: 2023-05-03 12:40+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"
@ -27,9 +27,10 @@ msgstr "Añadir productos a la factura"
#: web/template/invoices/index.gohtml:9 web/template/invoices/view.gohtml:9 #: web/template/invoices/index.gohtml:9 web/template/invoices/view.gohtml:9
#: web/template/invoices/edit.gohtml:9 web/template/contacts/new.gohtml:9 #: web/template/invoices/edit.gohtml:9 web/template/contacts/new.gohtml:9
#: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10 #: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10
#: web/template/profile.gohtml:9 web/template/tax-details.gohtml:9 #: web/template/profile.gohtml:9 web/template/expenses/new.gohtml:10
#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:9 #: web/template/expenses/index.gohtml:10 web/template/expenses/edit.gohtml:10
#: web/template/products/edit.gohtml:10 #: web/template/tax-details.gohtml:9 web/template/products/new.gohtml:9
#: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10
msgctxt "title" msgctxt "title"
msgid "Home" msgid "Home"
msgstr "Inicio" msgstr "Inicio"
@ -48,7 +49,7 @@ msgid "New Invoice"
msgstr "Nueva factura" msgstr "Nueva factura"
#: web/template/invoices/products.gohtml:48 #: web/template/invoices/products.gohtml:48
#: web/template/products/index.gohtml:38 #: web/template/expenses/index.gohtml:25 web/template/products/index.gohtml:38
msgctxt "product" msgctxt "product"
msgid "All" msgid "All"
msgstr "Todos" msgstr "Todos"
@ -139,7 +140,7 @@ msgctxt "title"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
#: web/template/invoices/index.gohtml:52 #: web/template/invoices/index.gohtml:52 web/template/expenses/index.gohtml:29
msgctxt "title" msgctxt "title"
msgid "Amount" msgid "Amount"
msgstr "Importe" msgstr "Importe"
@ -206,7 +207,7 @@ msgstr "Base imponible"
#: web/template/invoices/edit.gohtml:2 web/template/invoices/edit.gohtml:19 #: web/template/invoices/edit.gohtml:2 web/template/invoices/edit.gohtml:19
msgctxt "title" msgctxt "title"
msgid "Edit Invoice “%s”" msgid "Edit Invoice “%s”"
msgstr "Edición del la factura «%s»" msgstr "Edición de la factura «%s»"
#: web/template/invoices/edit.gohtml:68 #: web/template/invoices/edit.gohtml:68
msgctxt "action" msgctxt "action"
@ -245,10 +246,15 @@ msgstr "Facturas"
#: web/template/app.gohtml:48 #: web/template/app.gohtml:48
msgctxt "nav" msgctxt "nav"
msgid "Expenses"
msgstr "Gastos"
#: web/template/app.gohtml:49
msgctxt "nav"
msgid "Products" msgid "Products"
msgstr "Productos" msgstr "Productos"
#: web/template/app.gohtml:49 #: web/template/app.gohtml:50
msgctxt "nav" msgctxt "nav"
msgid "Contacts" msgid "Contacts"
msgstr "Contactos" msgstr "Contactos"
@ -335,6 +341,52 @@ msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Guardar cambios" msgstr "Guardar cambios"
#: web/template/expenses/new.gohtml:3 web/template/expenses/new.gohtml:12
#: web/template/expenses/new.gohtml:20
msgctxt "title"
msgid "New Expense"
msgstr "Nuevo gasto"
#: web/template/expenses/new.gohtml:11 web/template/expenses/index.gohtml:3
#: web/template/expenses/index.gohtml:11 web/template/expenses/edit.gohtml:11
msgctxt "title"
msgid "Expenses"
msgstr "Gastos"
#: web/template/expenses/new.gohtml:32 web/template/expenses/index.gohtml:15
msgctxt "action"
msgid "New expense"
msgstr "Nuevo gasto"
#: web/template/expenses/index.gohtml:26
msgctxt "title"
msgid "Contact"
msgstr "Contacto"
#: web/template/expenses/index.gohtml:27
msgctxt "title"
msgid "Invoice Date"
msgstr "Fecha de factura"
#: web/template/expenses/index.gohtml:28
msgctxt "title"
msgid "Invoice Number"
msgstr "Número de factura"
#: web/template/expenses/index.gohtml:52
msgid "No expenses added yet."
msgstr "No hay gastos."
#: web/template/expenses/edit.gohtml:3 web/template/expenses/edit.gohtml:20
msgctxt "title"
msgid "Edit Expense “%s”"
msgstr "Edición del gasto «%s»"
#: web/template/expenses/edit.gohtml:35
msgctxt "action"
msgid "Update expense"
msgstr "Actualizar gasto"
#: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:10 #: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:10
#: web/template/tax-details.gohtml:18 #: web/template/tax-details.gohtml:18
msgctxt "title" msgctxt "title"
@ -460,13 +512,14 @@ msgstr "No podéis dejar la contraseña en blanco."
msgid "Invalid user or password." msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido." msgstr "Nombre de usuario o contraseña inválido."
#: pkg/products.go:161 pkg/products.go:260 pkg/invoices.go:771 #: pkg/products.go:161 pkg/products.go:260 pkg/invoices.go:776
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: pkg/products.go:166 pkg/products.go:287 pkg/invoices.go:187 #: pkg/products.go:166 pkg/products.go:287 pkg/expenses.go:158
#: pkg/invoices.go:597 pkg/invoices.go:1031 pkg/contacts.go:256 #: pkg/invoices.go:187 pkg/invoices.go:597 pkg/invoices.go:1042
#: pkg/contacts.go:256
msgctxt "input" msgctxt "input"
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
@ -494,38 +547,40 @@ msgstr "Cualquiera"
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 debent tener como mínimo una de las etiquetas." msgstr "Las facturas debent tener como mínimo una de las etiquetas."
#: pkg/products.go:266 pkg/invoices.go:785 #: pkg/products.go:266 pkg/invoices.go:790
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripción" msgstr "Descripción"
#: pkg/products.go:271 pkg/invoices.go:789 #: pkg/products.go:271 pkg/invoices.go:794
msgctxt "input" msgctxt "input"
msgid "Price" msgid "Price"
msgstr "Precio" msgstr "Precio"
#: pkg/products.go:281 pkg/invoices.go:815 #: pkg/products.go:281 pkg/expenses.go:142 pkg/invoices.go:823
msgctxt "input" msgctxt "input"
msgid "Taxes" msgid "Taxes"
msgstr "Impuestos" msgstr "Impuestos"
#: pkg/products.go:306 pkg/profile.go:92 pkg/invoices.go:856 #: pkg/products.go:306 pkg/profile.go:92 pkg/invoices.go:867
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco." msgstr "No podéis dejar el nombre en blanco."
#: pkg/products.go:307 pkg/invoices.go:857 #: pkg/products.go:307 pkg/invoices.go:868
msgid "Price can not be empty." msgid "Price can not be empty."
msgstr "No podéis dejar el precio en blanco." msgstr "No podéis dejar el precio en blanco."
#: pkg/products.go:308 pkg/invoices.go:858 #: pkg/products.go:308 pkg/invoices.go:869
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:310 pkg/invoices.go:866 #: pkg/products.go:310 pkg/expenses.go:180 pkg/expenses.go:185
#: pkg/invoices.go:877
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:311 pkg/invoices.go:867 #: pkg/products.go:311 pkg/expenses.go:181 pkg/expenses.go:186
#: pkg/invoices.go:878
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."
@ -633,6 +688,46 @@ msgstr "La confirmación no corresponde con la contraseña."
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido." msgstr "Habéis escogido un idioma que no es válido."
#: pkg/expenses.go:91
msgid "Select a contact."
msgstr "Escoged un contacto"
#: pkg/expenses.go:125
msgctxt "input"
msgid "Contact"
msgstr "Contacto"
#: pkg/expenses.go:131
msgctxt "input"
msgid "Invoice number"
msgstr "Número de factura"
#: pkg/expenses.go:136 pkg/invoices.go:586
msgctxt "input"
msgid "Invoice Date"
msgstr "Fecha de factura"
#: pkg/expenses.go:148
msgctxt "input"
msgid "Amount"
msgstr "Importe"
#: pkg/expenses.go:178
msgid "Selected contact is not valid."
msgstr "Habéis escogido un contacto que no es válido."
#: pkg/expenses.go:179 pkg/invoices.go:641
msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida."
#: pkg/expenses.go:182
msgid "Amount can not be empty."
msgstr "No podéis dejar el importe en blanco."
#: pkg/expenses.go:183
msgid "Amount must be a number greater than zero."
msgstr "El importe tiene que ser un número mayor a cero."
#: pkg/invoices.go:160 pkg/invoices.go:580 #: pkg/invoices.go:160 pkg/invoices.go:580
msgctxt "input" msgctxt "input"
msgid "Customer" msgid "Customer"
@ -674,15 +769,10 @@ msgstr "Escoged un cliente a facturar."
msgid "invoices.zip" msgid "invoices.zip"
msgstr "facturas.zip" msgstr "facturas.zip"
#: pkg/invoices.go:530 pkg/invoices.go:1017 #: pkg/invoices.go:530 pkg/invoices.go:1028
msgid "Invalid action" msgid "Invalid action"
msgstr "Acción inválida." msgstr "Acción inválida."
#: pkg/invoices.go:586
msgctxt "input"
msgid "Invoice Date"
msgstr "Fecha de factura"
#: pkg/invoices.go:592 #: pkg/invoices.go:592
msgctxt "input" msgctxt "input"
msgid "Notes" msgid "Notes"
@ -705,46 +795,42 @@ msgstr "Habéis escogido un cliente que no es válido."
msgid "Invoice date can not be empty." msgid "Invoice date can not be empty."
msgstr "No podéis dejar la fecha de la factura en blanco." msgstr "No podéis dejar la fecha de la factura en blanco."
#: pkg/invoices.go:641
msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida."
#: pkg/invoices.go:643 #: pkg/invoices.go:643
msgid "Selected payment method is not valid." msgid "Selected payment method is not valid."
msgstr "Habéis escogido un método de pago que no es válido." msgstr "Habéis escogido un método de pago que no es válido."
#: pkg/invoices.go:761 pkg/invoices.go:766 #: pkg/invoices.go:766 pkg/invoices.go:771
msgctxt "input" msgctxt "input"
msgid "Id" msgid "Id"
msgstr "Identificador" msgstr "Identificador"
#: pkg/invoices.go:798 #: pkg/invoices.go:804
msgctxt "input" msgctxt "input"
msgid "Quantity" msgid "Quantity"
msgstr "Cantidad" msgstr "Cantidad"
#: pkg/invoices.go:806 #: pkg/invoices.go:813
msgctxt "input" msgctxt "input"
msgid "Discount (%)" msgid "Discount (%)"
msgstr "Descuento (%)" msgstr "Descuento (%)"
#: pkg/invoices.go:854 #: pkg/invoices.go:865
msgid "Product ID must be a number greater than zero." msgid "Product ID must be a number greater than zero."
msgstr "El ID de producto tiene que ser un número mayor a cero." msgstr "El ID de producto tiene que ser un número mayor a cero."
#: pkg/invoices.go:860 #: pkg/invoices.go:871
msgid "Quantity can not be empty." msgid "Quantity can not be empty."
msgstr "No podéis dejar la cantidad en blanco." msgstr "No podéis dejar la cantidad en blanco."
#: pkg/invoices.go:861 #: pkg/invoices.go:872
msgid "Quantity must be a number greater than zero." msgid "Quantity must be a number greater than zero."
msgstr "La cantidad tiene que ser un número mayor a cero." msgstr "La cantidad tiene que ser un número mayor a cero."
#: pkg/invoices.go:863 #: pkg/invoices.go:874
msgid "Discount can not be empty." msgid "Discount can not be empty."
msgstr "No podéis dejar el descuento en blanco." msgstr "No podéis dejar el descuento en blanco."
#: pkg/invoices.go:864 #: pkg/invoices.go:875
msgid "Discount must be a percentage between 0 and 100." msgid "Discount must be a percentage between 0 and 100."
msgstr "El descuento tiene que ser un porcentaje entre 0 y 100." msgstr "El descuento tiene que ser un porcentaje entre 0 y 100."

View File

@ -45,6 +45,7 @@
<ul> <ul>
<li><a href="{{ companyURI "/" }}">{{( pgettext "Dashboard" "nav" )}}</a></li> <li><a href="{{ companyURI "/" }}">{{( pgettext "Dashboard" "nav" )}}</a></li>
<li><a href="{{ companyURI "/invoices" }}">{{( pgettext "Invoices" "nav" )}}</a></li> <li><a href="{{ companyURI "/invoices" }}">{{( pgettext "Invoices" "nav" )}}</a></li>
<li><a href="{{ companyURI "/expenses" }}">{{( pgettext "Expenses" "nav" )}}</a></li>
<li><a href="{{ companyURI "/products" }}">{{( pgettext "Products" "nav" )}}</a></li> <li><a href="{{ companyURI "/products" }}">{{( pgettext "Products" "nav" )}}</a></li>
<li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Contacts" "nav" )}}</a></li> <li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Contacts" "nav" )}}</a></li>
</ul> </ul>

View File

@ -0,0 +1,39 @@
{{ define "title" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editExpensePage*/ -}}
{{ printf ( pgettext "Edit Expense “%s”" "title" ) .Slug }}
{{- end }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editExpensePage*/ -}}
<nav data-hx-target="main" data-hx-boost="true">
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a href="{{ companyURI "/expenses"}}">{{( pgettext "Expenses" "title" )}}</a> /
<a>{{ .Slug }}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editExpensePage*/ -}}
<section class="dialog-content" id="new-expense-dialog-content" data-hx-target="main">
<h2>{{ printf (pgettext "Edit Expense “%s”" "title") .Slug }}</h2>
<form method="POST" action="{{ companyURI "/expenses/" }}{{ .Slug }}" data-hx-boost="true">
{{ csrfToken }}
{{ putMethod }}
{{ with .Form -}}
{{ template "select-field" .Invoicer }}
{{ template "input-field" .InvoiceNumber }}
{{ template "input-field" .InvoiceDate }}
{{ template "input-field" .Amount }}
{{ template "select-field" .Tax }}
{{ template "tags-field" .Tags }}
{{- end }}
<fieldset>
<button class="primary" type="submit">{{( pgettext "Update expense" "action" )}}</button>
</fieldset>
</form>
</section>
{{- end }}

View File

@ -0,0 +1,57 @@
{{ define "title" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.expensesIndexPage*/ -}}
{{( pgettext "Expenses" "title" )}}
{{- end }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.expensesIndexPage*/ -}}
<nav data-hx-boost="true" data-hx-target="main">
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a>{{( pgettext "Expenses" "title" )}}</a>
</p>
<p>
<a class="primary button"
href="{{ companyURI "/expenses/new" }}">{{( pgettext "New expense" "action" )}}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.expensesIndexPage*/ -}}
<table>
<thead>
<tr>
<th>{{( pgettext "All" "product" )}}</th>
<th>{{( pgettext "Contact" "title" )}}</th>
<th>{{( pgettext "Invoice Date" "title" )}}</th>
<th>{{( pgettext "Invoice Number" "title" )}}</th>
<th>{{( pgettext "Amount" "title" )}}</th>
</tr>
</thead>
<tbody>
{{ with .Expenses }}
{{- range . }}
<tr>
<td></td>
<td><a href="{{ companyURI "/expenses/"}}{{ .Slug }}"
data-hx-target="main" data-hx-boost="true">{{ .InvoicerName }}</a></td>
<td>{{ .InvoiceDate|formatDate }}</td>
<td>{{ .InvoiceNumber }}</td>
<td>
{{- range $index, $tag := .Tags }}
{{- if gt $index 0 }}, {{ end -}}
{{ . }}
{{- end }}
</td>
<td class="numeric">{{ .Amount | formatPrice }}</td>
</tr>
{{- end }}
{{ else }}
<tr>
<td colspan="5">{{( gettext "No expenses added yet." )}}</td>
</tr>
{{ end }}
</tbody>
</table>
{{- end }}

View File

@ -0,0 +1,36 @@
{{ define "title" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.expenseForm*/ -}}
{{( pgettext "New Expense" "title" )}}
{{- end }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.expenseForm*/ -}}
<nav data-hx-target="main" data-hx-boost="true">
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a href="{{ companyURI "/expenses"}}">{{( pgettext "Expenses" "title" )}}</a> /
<a>{{( pgettext "New Expense" "title" )}}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.expenseForm*/ -}}
<section class="dialog-content" id="new-expense-dialog-content" data-hx-target="main">
<h2>{{(pgettext "New Expense" "title")}}</h2>
<form method="POST" action="{{ companyURI "/expenses" }}" data-hx-boost="true">
{{ csrfToken }}
{{ template "select-field" .Invoicer }}
{{ template "input-field" .InvoiceNumber }}
{{ template "input-field" .InvoiceDate }}
{{ template "input-field" .Amount }}
{{ template "select-field" .Tax }}
{{ template "tags-field" .Tags }}
<fieldset>
<button class="primary" type="submit">{{( pgettext "New expense" "action" )}}</button>
</fieldset>
</form>
</section>
{{- end }}