Add the form to add products to an invoice and create invoices too
Still missing: the invoice number, that requires more tables and possibly a PL/pgSQL function to do it properly.
This commit is contained in:
parent
72fbed68ac
commit
4903c8a3b9
|
@ -96,6 +96,14 @@ func (c *Conn) MustExec(ctx context.Context, sql string, args ...interface{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Conn) MustQuery(ctx context.Context, sql string, args ...interface{}) pgx.Rows {
|
||||||
|
rows, err := c.Conn.Query(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
type Tx struct {
|
type Tx struct {
|
||||||
pgx.Tx
|
pgx.Tx
|
||||||
}
|
}
|
||||||
|
|
15
pkg/form.go
15
pkg/form.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/jackc/pgtype"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
@ -12,6 +13,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Attribute struct {
|
type Attribute struct {
|
||||||
|
@ -72,6 +74,14 @@ func (field *SelectField) Scan(value interface{}) error {
|
||||||
field.Selected = append(field.Selected, "")
|
field.Selected = append(field.Selected, "")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if str, ok := value.(string); ok {
|
||||||
|
if array, err := pgtype.ParseUntypedTextArray(str); err == nil {
|
||||||
|
for _, element := range array.Elements {
|
||||||
|
field.Selected = append(field.Selected, element)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
field.Selected = append(field.Selected, fmt.Sprintf("%v", value))
|
field.Selected = append(field.Selected, fmt.Sprintf("%v", value))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -185,6 +195,11 @@ func (v *FormValidator) CheckValidURL(field *InputField, message string) bool {
|
||||||
return v.checkInput(field, err == nil, message)
|
return v.checkInput(field, err == nil, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *FormValidator) CheckValidDate(field *InputField, message string) bool {
|
||||||
|
_, err := time.Parse("2006-02-01", field.Val)
|
||||||
|
return v.checkInput(field, err == nil, message)
|
||||||
|
}
|
||||||
|
|
||||||
func (v *FormValidator) CheckValidPostalCode(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool {
|
func (v *FormValidator) CheckValidPostalCode(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool {
|
||||||
pattern := "^" + conn.MustGetText(ctx, ".{1,255}", "select postal_code_regex from country where country_code = $1", country) + "$"
|
pattern := "^" + conn.MustGetText(ctx, ".{1,255}", "select postal_code_regex from country where country_code = $1", country) + "$"
|
||||||
match, err := regexp.MatchString(pattern, field.Val)
|
match, err := regexp.MatchString(pattern, field.Val)
|
||||||
|
|
290
pkg/invoices.go
290
pkg/invoices.go
|
@ -3,20 +3,59 @@ package pkg
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type InvoiceEntry struct {
|
||||||
|
Slug string
|
||||||
|
Date time.Time
|
||||||
|
Number string
|
||||||
|
CustomerName string
|
||||||
|
CustomerSlug string
|
||||||
|
Status string
|
||||||
|
StatusLabel string
|
||||||
|
}
|
||||||
|
|
||||||
type InvoicesIndexPage struct {
|
type InvoicesIndexPage struct {
|
||||||
|
Invoices []*InvoiceEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
page := &InvoicesIndexPage{}
|
page := &InvoicesIndexPage{
|
||||||
|
Invoices: mustGetInvoiceEntries(r.Context(), getConn(r), mustGetCompany(r), getLocale(r)),
|
||||||
|
}
|
||||||
mustRenderAppTemplate(w, r, "invoices/index.gohtml", page)
|
mustRenderAppTemplate(w, r, "invoices/index.gohtml", page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustGetInvoiceEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale) []*InvoiceEntry {
|
||||||
|
rows, err := conn.Query(ctx, "select invoice.slug, invoice_date, invoice_number, contact.business_name, contact.slug, invoice.invoice_status, isi18n.name from invoice join contact using (contact_id) join invoice_status_i18n isi18n on invoice.invoice_status = isi18n.invoice_status and isi18n.lang_tag = $2 where invoice.company_id = $1 order by invoice_date, invoice_number", company.Id, locale.Language.String())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var entries []*InvoiceEntry
|
||||||
|
for rows.Next() {
|
||||||
|
entry := &InvoiceEntry{}
|
||||||
|
err = rows.Scan(&entry.Slug, &entry.Date, &entry.Number, &entry.CustomerName, &entry.CustomerSlug, &entry.Status, &entry.StatusLabel)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
panic(rows.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
func GetInvoiceForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func GetInvoiceForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
locale := getLocale(r)
|
locale := getLocale(r)
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
|
@ -36,19 +75,123 @@ func mustRenderNewInvoiceForm(w http.ResponseWriter, r *http.Request, form *invo
|
||||||
mustRenderAppTemplate(w, r, "invoices/new.gohtml", form)
|
mustRenderAppTemplate(w, r, "invoices/new.gohtml", form)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
func mustRenderNewInvoiceProductsForm(w http.ResponseWriter, r *http.Request, form *invoiceForm) {
|
||||||
r.ParseForm()
|
conn := getConn(r)
|
||||||
w.Write([]byte("OK"))
|
company := mustGetCompany(r)
|
||||||
|
page := newInvoiceProductsPage{
|
||||||
|
Form: form,
|
||||||
|
Products: mustGetProductChoices(r.Context(), conn, company),
|
||||||
|
}
|
||||||
|
mustRenderAppTemplate(w, r, "invoices/products.gohtml", page)
|
||||||
}
|
}
|
||||||
|
|
||||||
type invoiceProductForm struct {
|
func mustGetProductChoices(ctx context.Context, conn *Conn, company *Company) []*productChoice {
|
||||||
ProductId *InputField
|
rows := conn.MustQuery(ctx, "select product.product_id, product.name, to_price(price, decimal_digits) from product join company using (company_id) join currency using (currency_code) where company_id = $1 order by name", company.Id)
|
||||||
Name *InputField
|
defer rows.Close()
|
||||||
Description *InputField
|
|
||||||
Price *InputField
|
var choices []*productChoice
|
||||||
Quantity *InputField
|
for rows.Next() {
|
||||||
Discount *InputField
|
entry := &productChoice{}
|
||||||
Tax *SelectField
|
if err := rows.Scan(&entry.Id, &entry.Name, &entry.Price); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
choices = append(choices, entry)
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
panic(rows.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
return choices
|
||||||
|
}
|
||||||
|
|
||||||
|
type newInvoiceProductsPage struct {
|
||||||
|
Form *invoiceForm
|
||||||
|
Products []*productChoice
|
||||||
|
}
|
||||||
|
|
||||||
|
type productChoice struct {
|
||||||
|
Id int
|
||||||
|
Name string
|
||||||
|
Price string
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
locale := getLocale(r)
|
||||||
|
conn := getConn(r)
|
||||||
|
company := mustGetCompany(r)
|
||||||
|
form := newInvoiceForm(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
|
||||||
|
}
|
||||||
|
switch r.Form.Get("action") {
|
||||||
|
case "update":
|
||||||
|
form.Update()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
mustRenderNewInvoiceForm(w, r, form)
|
||||||
|
case "products":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
mustRenderNewInvoiceProductsForm(w, r, form)
|
||||||
|
case "add":
|
||||||
|
if !form.Validate() {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
mustRenderNewInvoiceForm(w, r, form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tx := conn.MustBegin(r.Context())
|
||||||
|
invoiceId := tx.MustGetInteger(r.Context(), "insert into invoice (company_id, invoice_number, invoice_date, contact_id, notes, currency_code) select company_id, $2, $3, $4, $5, currency_code from company join currency using (currency_code) where company_id = $1 returning invoice_id", company.Id, form.Number, form.Date, form.Customer, form.Notes)
|
||||||
|
batch := &pgx.Batch{}
|
||||||
|
for _, product := range form.Products {
|
||||||
|
batch.Queue("insert into invoice_product(invoice_id, product_id, name, description, price, quantity, discount_rate) select $2, $3, $4, $5, parse_price($6, decimal_digits), $7, $8 / 100::decimal from company join currency using (currency_code) where company_id = $1", company.Id, invoiceId, product.ProductId, product.Name, product.Description, product.Price, product.Quantity.Integer(), product.Discount.Integer())
|
||||||
|
}
|
||||||
|
br := tx.SendBatch(r.Context(), batch)
|
||||||
|
for range form.Products {
|
||||||
|
if _, err := br.Exec(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := br.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
tx.MustCommit(r.Context())
|
||||||
|
http.Redirect(w, r, companyURI(company, "/invoices"), http.StatusSeeOther)
|
||||||
|
default:
|
||||||
|
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleAddProductsToInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
locale := getLocale(r)
|
||||||
|
conn := getConn(r)
|
||||||
|
company := mustGetCompany(r)
|
||||||
|
form := newInvoiceForm(r.Context(), conn, locale, company)
|
||||||
|
if err := form.Parse(r); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
index := len(form.Products)
|
||||||
|
productsId := r.Form["id"]
|
||||||
|
rows := conn.MustQuery(r.Context(), "select product_id, name, description, to_price(price, decimal_digits), 1 as quantity, 0 as discount, array_agg(tax_id) from product join company using (company_id) join currency using (currency_code) left join product_tax using (product_id) where product_id = any ($1) group by product_id, name, description, price, decimal_digits", productsId)
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
product := newInvoiceProductForm(index, company, locale, form.Tax.Options)
|
||||||
|
if err := rows.Scan(product.ProductId, product.Name, product.Description, product.Price, product.Quantity, product.Discount, product.Tax); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
form.Products = append(form.Products, product)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
panic(rows.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
mustRenderNewInvoiceForm(w, r, form)
|
||||||
}
|
}
|
||||||
|
|
||||||
type invoiceForm struct {
|
type invoiceForm struct {
|
||||||
|
@ -58,6 +201,7 @@ type invoiceForm struct {
|
||||||
Number *InputField
|
Number *InputField
|
||||||
Date *InputField
|
Date *InputField
|
||||||
Notes *InputField
|
Notes *InputField
|
||||||
|
Tax *SelectField
|
||||||
Products []*invoiceProductForm
|
Products []*invoiceProductForm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,33 +232,103 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
||||||
Label: pgettext("input", "Notes", locale),
|
Label: pgettext("input", "Notes", locale),
|
||||||
Type: "textarea",
|
Type: "textarea",
|
||||||
},
|
},
|
||||||
Products: []*invoiceProductForm{
|
Tax: &SelectField{
|
||||||
newInvoiceProductForm(ctx, conn, company, locale),
|
Name: "text",
|
||||||
|
Label: pgettext("input", "Taxes", locale),
|
||||||
|
Multiple: true,
|
||||||
|
Options: mustGetTaxOptions(ctx, conn, company),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newInvoiceProductForm(ctx context.Context, conn *Conn, company *Company, locale *Locale) *invoiceProductForm {
|
func (form *invoiceForm) Parse(r *http.Request) error {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
form.Customer.FillValue(r)
|
||||||
|
form.Number.FillValue(r)
|
||||||
|
form.Date.FillValue(r)
|
||||||
|
form.Notes.FillValue(r)
|
||||||
|
if r.Form.Has("product.id.0") {
|
||||||
|
for index := 0; r.Form.Has("product.id." + strconv.Itoa(index)); index++ {
|
||||||
|
productForm := newInvoiceProductForm(index, form.company, form.locale, form.Tax.Options)
|
||||||
|
if err := productForm.Parse(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
form.Products = append(form.Products, productForm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (form *invoiceForm) Validate() bool {
|
||||||
|
validator := newFormValidator()
|
||||||
|
|
||||||
|
validator.CheckValidSelectOption(form.Customer, gettext("Name can not be empty.", form.locale))
|
||||||
|
if validator.CheckRequiredInput(form.Date, gettext("Invoice date can not be empty.", form.locale)) {
|
||||||
|
validator.CheckValidDate(form.Date, gettext("Invoice date must be a valid date.", form.locale))
|
||||||
|
}
|
||||||
|
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
|
||||||
|
|
||||||
|
allOK := validator.AllOK()
|
||||||
|
for _, product := range form.Products {
|
||||||
|
allOK = product.Validate() && allOK
|
||||||
|
}
|
||||||
|
return allOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (form *invoiceForm) Update() {
|
||||||
|
products := form.Products
|
||||||
|
form.Products = nil
|
||||||
|
index := 0
|
||||||
|
for _, product := range products {
|
||||||
|
if product.Quantity.Val != "0" {
|
||||||
|
form.Products = append(form.Products, product)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption {
|
||||||
|
return MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
type invoiceProductForm struct {
|
||||||
|
locale *Locale
|
||||||
|
company *Company
|
||||||
|
ProductId *InputField
|
||||||
|
Name *InputField
|
||||||
|
Description *InputField
|
||||||
|
Price *InputField
|
||||||
|
Quantity *InputField
|
||||||
|
Discount *InputField
|
||||||
|
Tax *SelectField
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptions []*SelectOption) *invoiceProductForm {
|
||||||
|
suffix := "." + strconv.Itoa(index)
|
||||||
return &invoiceProductForm{
|
return &invoiceProductForm{
|
||||||
|
locale: locale,
|
||||||
|
company: company,
|
||||||
ProductId: &InputField{
|
ProductId: &InputField{
|
||||||
Name: "product[][id]",
|
Name: "product.id" + suffix,
|
||||||
Label: pgettext("input", "Id", locale),
|
Label: pgettext("input", "Id", locale),
|
||||||
Type: "hidden",
|
Type: "hidden",
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
Name: &InputField{
|
Name: &InputField{
|
||||||
Name: "product[][name]",
|
Name: "product.name" + suffix,
|
||||||
Label: pgettext("input", "Name", locale),
|
Label: pgettext("input", "Name", locale),
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
Description: &InputField{
|
Description: &InputField{
|
||||||
Name: "product[][description]",
|
Name: "product.description" + suffix,
|
||||||
Label: pgettext("input", "Description", locale),
|
Label: pgettext("input", "Description", locale),
|
||||||
Type: "textarea",
|
Type: "textarea",
|
||||||
},
|
},
|
||||||
Price: &InputField{
|
Price: &InputField{
|
||||||
Name: "product[][price]",
|
Name: "product.price" + suffix,
|
||||||
Label: pgettext("input", "Price", locale),
|
Label: pgettext("input", "Price", locale),
|
||||||
Type: "number",
|
Type: "number",
|
||||||
Required: true,
|
Required: true,
|
||||||
|
@ -124,7 +338,7 @@ func newInvoiceProductForm(ctx context.Context, conn *Conn, company *Company, lo
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Quantity: &InputField{
|
Quantity: &InputField{
|
||||||
Name: "product[][quantity]",
|
Name: "product.quantity" + suffix,
|
||||||
Label: pgettext("input", "Quantity", locale),
|
Label: pgettext("input", "Quantity", locale),
|
||||||
Type: "number",
|
Type: "number",
|
||||||
Required: true,
|
Required: true,
|
||||||
|
@ -133,7 +347,7 @@ func newInvoiceProductForm(ctx context.Context, conn *Conn, company *Company, lo
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Discount: &InputField{
|
Discount: &InputField{
|
||||||
Name: "product[][discount]",
|
Name: "product.discount" + suffix,
|
||||||
Label: pgettext("input", "Discount (%)", locale),
|
Label: pgettext("input", "Discount (%)", locale),
|
||||||
Type: "number",
|
Type: "number",
|
||||||
Required: true,
|
Required: true,
|
||||||
|
@ -143,10 +357,40 @@ func newInvoiceProductForm(ctx context.Context, conn *Conn, company *Company, lo
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Tax: &SelectField{
|
Tax: &SelectField{
|
||||||
Name: "product[][tax]",
|
Name: "product.tax" + suffix,
|
||||||
Label: pgettext("input", "Taxes", locale),
|
Label: pgettext("input", "Taxes", locale),
|
||||||
Multiple: true,
|
Multiple: true,
|
||||||
Options: MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id),
|
Options: taxOptions,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (form *invoiceProductForm) Parse(r *http.Request) error {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
form.ProductId.FillValue(r)
|
||||||
|
form.Name.FillValue(r)
|
||||||
|
form.Description.FillValue(r)
|
||||||
|
form.Price.FillValue(r)
|
||||||
|
form.Quantity.FillValue(r)
|
||||||
|
form.Discount.FillValue(r)
|
||||||
|
form.Tax.FillValue(r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (form *invoiceProductForm) Validate() bool {
|
||||||
|
validator := newFormValidator()
|
||||||
|
validator.CheckRequiredInput(form.Name, gettext("Name can not be empty.", form.locale))
|
||||||
|
if validator.CheckRequiredInput(form.Price, gettext("Price can not be empty.", form.locale)) {
|
||||||
|
validator.CheckValidDecimal(form.Price, form.company.MinCents(), math.MaxFloat64, gettext("Price must be a number greater than zero.", form.locale))
|
||||||
|
}
|
||||||
|
if validator.CheckRequiredInput(form.Quantity, gettext("Quantity can not be empty.", form.locale)) {
|
||||||
|
validator.CheckValidInteger(form.Quantity, 1, math.MaxInt, gettext("Quantity must be a number greater than zero.", form.locale))
|
||||||
|
}
|
||||||
|
if validator.CheckRequiredInput(form.Discount, gettext("Discount can not be empty.", form.locale)) {
|
||||||
|
validator.CheckValidInteger(form.Discount, 0, 100, gettext("Discount must be a percentage between 0 and 100.", form.locale))
|
||||||
|
}
|
||||||
|
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
|
||||||
|
return validator.AllOK()
|
||||||
|
}
|
||||||
|
|
|
@ -214,7 +214,7 @@ func newProductForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
||||||
Name: "tax",
|
Name: "tax",
|
||||||
Label: pgettext("input", "Taxes", locale),
|
Label: pgettext("input", "Taxes", locale),
|
||||||
Multiple: true,
|
Multiple: true,
|
||||||
Options: MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id),
|
Options: mustGetTaxOptions(ctx, conn, company),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ func NewRouter(db *Db) http.Handler {
|
||||||
companyRouter.GET("/invoices", IndexInvoices)
|
companyRouter.GET("/invoices", IndexInvoices)
|
||||||
companyRouter.POST("/invoices", HandleAddInvoice)
|
companyRouter.POST("/invoices", HandleAddInvoice)
|
||||||
companyRouter.GET("/invoices/:slug", GetInvoiceForm)
|
companyRouter.GET("/invoices/:slug", GetInvoiceForm)
|
||||||
|
companyRouter.POST("/invoices/new/products", HandleAddProductsToInvoice)
|
||||||
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
|
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const overrideMethodName = "_method"
|
const overrideMethodName = "_method"
|
||||||
|
@ -39,6 +40,9 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
|
||||||
}
|
}
|
||||||
return p.Sprintf("%.*f", company.DecimalDigits, number.Decimal(f))
|
return p.Sprintf("%.*f", company.DecimalDigits, number.Decimal(f))
|
||||||
},
|
},
|
||||||
|
"formatDate": func(time time.Time) string {
|
||||||
|
return time.Format("02/01/2006")
|
||||||
|
},
|
||||||
"csrfToken": func() template.HTML {
|
"csrfToken": func() template.HTML {
|
||||||
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, csrfTokenField, user.CsrfToken))
|
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, csrfTokenField, user.CsrfToken))
|
||||||
},
|
},
|
||||||
|
|
230
po/ca.po
230
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-02-08 13:43+0100\n"
|
"POT-Creation-Date: 2023-02-12 20:51+0100\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"
|
||||||
|
@ -17,6 +17,111 @@ msgstr ""
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:2
|
||||||
|
#: web/template/invoices/products.gohtml:15
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Add Products to Invoice"
|
||||||
|
msgstr "Afegeix productes a la factura"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:9 web/template/invoices/new.gohtml:9
|
||||||
|
#: web/template/invoices/index.gohtml:8 web/template/contacts/new.gohtml:9
|
||||||
|
#: web/template/contacts/index.gohtml:8 web/template/contacts/edit.gohtml:9
|
||||||
|
#: web/template/profile.gohtml:9 web/template/tax-details.gohtml:8
|
||||||
|
#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:8
|
||||||
|
#: web/template/products/edit.gohtml:9
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Home"
|
||||||
|
msgstr "Inici"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:10 web/template/invoices/new.gohtml:10
|
||||||
|
#: web/template/invoices/index.gohtml:2 web/template/invoices/index.gohtml:9
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Invoices"
|
||||||
|
msgstr "Factures"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:11 web/template/invoices/new.gohtml:2
|
||||||
|
#: web/template/invoices/new.gohtml:11 web/template/invoices/new.gohtml:15
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "New Invoice"
|
||||||
|
msgstr "Nova factura"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:41
|
||||||
|
#: web/template/products/index.gohtml:21
|
||||||
|
msgctxt "product"
|
||||||
|
msgid "All"
|
||||||
|
msgstr "Tots"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:42
|
||||||
|
#: web/template/products/index.gohtml:22
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Nom"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:43
|
||||||
|
#: web/template/products/index.gohtml:23
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Price"
|
||||||
|
msgstr "Preu"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:57
|
||||||
|
#: web/template/products/index.gohtml:37
|
||||||
|
msgid "No products added yet."
|
||||||
|
msgstr "No hi ha cap producte."
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:37
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Add products"
|
||||||
|
msgstr "Afegeix productes"
|
||||||
|
|
||||||
|
#: web/template/invoices/new.gohtml:38
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Update"
|
||||||
|
msgstr "Actualitza"
|
||||||
|
|
||||||
|
#: web/template/invoices/new.gohtml:40 web/template/invoices/index.gohtml:13
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "New invoice"
|
||||||
|
msgstr "Nova factura"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:21
|
||||||
|
msgctxt "invoice"
|
||||||
|
msgid "All"
|
||||||
|
msgstr "Totes"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:22
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Date"
|
||||||
|
msgstr "Data"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:23
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Invoice Num."
|
||||||
|
msgstr "Núm. factura"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:24 web/template/contacts/index.gohtml:22
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Customer"
|
||||||
|
msgstr "Client"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:25
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "Estat"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:26
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Label"
|
||||||
|
msgstr "Etiqueta"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:27
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "Descàrrega"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:45
|
||||||
|
msgid "No invoices added yet."
|
||||||
|
msgstr "No hi ha cap factura."
|
||||||
|
|
||||||
#: web/template/dashboard.gohtml:2
|
#: web/template/dashboard.gohtml:2
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Dashboard"
|
msgid "Dashboard"
|
||||||
|
@ -44,10 +149,15 @@ msgstr "Tauler"
|
||||||
|
|
||||||
#: web/template/app.gohtml:44
|
#: web/template/app.gohtml:44
|
||||||
msgctxt "nav"
|
msgctxt "nav"
|
||||||
|
msgid "Invoices"
|
||||||
|
msgstr "Factures"
|
||||||
|
|
||||||
|
#: web/template/app.gohtml:45
|
||||||
|
msgctxt "nav"
|
||||||
msgid "Products"
|
msgid "Products"
|
||||||
msgstr "Productes"
|
msgstr "Productes"
|
||||||
|
|
||||||
#: web/template/app.gohtml:45
|
#: web/template/app.gohtml:46
|
||||||
msgctxt "nav"
|
msgctxt "nav"
|
||||||
msgid "Contacts"
|
msgid "Contacts"
|
||||||
msgstr "Contactes"
|
msgstr "Contactes"
|
||||||
|
@ -58,14 +168,6 @@ msgctxt "title"
|
||||||
msgid "New Contact"
|
msgid "New Contact"
|
||||||
msgstr "Nou contacte"
|
msgstr "Nou contacte"
|
||||||
|
|
||||||
#: web/template/contacts/new.gohtml:9 web/template/contacts/index.gohtml:8
|
|
||||||
#: web/template/contacts/edit.gohtml:9 web/template/profile.gohtml:9
|
|
||||||
#: web/template/tax-details.gohtml:8 web/template/products/new.gohtml:9
|
|
||||||
#: web/template/products/index.gohtml:8 web/template/products/edit.gohtml:9
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Home"
|
|
||||||
msgstr "Inici"
|
|
||||||
|
|
||||||
#: web/template/contacts/new.gohtml:10 web/template/contacts/index.gohtml:2
|
#: web/template/contacts/new.gohtml:10 web/template/contacts/index.gohtml:2
|
||||||
#: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10
|
#: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
|
@ -82,11 +184,6 @@ msgctxt "contact"
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Tots"
|
msgstr "Tots"
|
||||||
|
|
||||||
#: web/template/contacts/index.gohtml:22
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Customer"
|
|
||||||
msgstr "Client"
|
|
||||||
|
|
||||||
#: web/template/contacts/index.gohtml:23
|
#: web/template/contacts/index.gohtml:23
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
|
@ -199,25 +296,6 @@ msgctxt "action"
|
||||||
msgid "New product"
|
msgid "New product"
|
||||||
msgstr "Nou producte"
|
msgstr "Nou producte"
|
||||||
|
|
||||||
#: web/template/products/index.gohtml:21
|
|
||||||
msgctxt "product"
|
|
||||||
msgid "All"
|
|
||||||
msgstr "Tots"
|
|
||||||
|
|
||||||
#: web/template/products/index.gohtml:22
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Name"
|
|
||||||
msgstr "Nom"
|
|
||||||
|
|
||||||
#: web/template/products/index.gohtml:23
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Price"
|
|
||||||
msgstr "Preu"
|
|
||||||
|
|
||||||
#: web/template/products/index.gohtml:37
|
|
||||||
msgid "No products added yet."
|
|
||||||
msgstr "No hi ha cap producte."
|
|
||||||
|
|
||||||
#: web/template/products/edit.gohtml:2 web/template/products/edit.gohtml:15
|
#: web/template/products/edit.gohtml:2 web/template/products/edit.gohtml:15
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Edit Product “%s”"
|
msgid "Edit Product “%s”"
|
||||||
|
@ -254,39 +332,40 @@ msgstr "No podeu deixar la contrasenya en blanc."
|
||||||
msgid "Invalid user or password."
|
msgid "Invalid user or password."
|
||||||
msgstr "Nom d’usuari o contrasenya incorrectes."
|
msgstr "Nom d’usuari o contrasenya incorrectes."
|
||||||
|
|
||||||
#: pkg/products.go:193
|
#: pkg/products.go:194 pkg/invoices.go:321
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "Nom"
|
msgstr "Nom"
|
||||||
|
|
||||||
#: pkg/products.go:199
|
#: pkg/products.go:200 pkg/invoices.go:327
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Descripció"
|
msgstr "Descripció"
|
||||||
|
|
||||||
#: pkg/products.go:204
|
#: pkg/products.go:205 pkg/invoices.go:332
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Price"
|
msgid "Price"
|
||||||
msgstr "Preu"
|
msgstr "Preu"
|
||||||
|
|
||||||
#: pkg/products.go:214
|
#: pkg/products.go:215 pkg/invoices.go:237 pkg/invoices.go:361
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Taxes"
|
msgid "Taxes"
|
||||||
msgstr "Imposts"
|
msgstr "Imposts"
|
||||||
|
|
||||||
#: pkg/products.go:234 pkg/profile.go:92
|
#: pkg/products.go:235 pkg/profile.go:92 pkg/invoices.go:267
|
||||||
|
#: pkg/invoices.go:384
|
||||||
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:235
|
#: pkg/products.go:236 pkg/invoices.go:385
|
||||||
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:236
|
#: pkg/products.go:237 pkg/invoices.go:386
|
||||||
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:238
|
#: pkg/products.go:239 pkg/invoices.go:271 pkg/invoices.go:394
|
||||||
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."
|
||||||
|
|
||||||
|
@ -349,6 +428,73 @@ 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/invoices.go:66
|
||||||
|
msgid "Select a customer to bill."
|
||||||
|
msgstr "Escolliu un client a facturar."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:163
|
||||||
|
msgid "Invalid action"
|
||||||
|
msgstr "Acció invàlida."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:214
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Customer"
|
||||||
|
msgstr "Client"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:220
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Number"
|
||||||
|
msgstr "Número"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:226
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Invoice Date"
|
||||||
|
msgstr "Data de factura"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:232
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Notes"
|
||||||
|
msgstr "Notes"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:268
|
||||||
|
msgid "Invoice date can not be empty."
|
||||||
|
msgstr "No podeu deixar la data de la factura en blanc."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:269
|
||||||
|
msgid "Invoice date must be a valid date."
|
||||||
|
msgstr "La data de facturació ha de ser vàlida."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:315
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Id"
|
||||||
|
msgstr "Identificador"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:342
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Quantity"
|
||||||
|
msgstr "Quantitat"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:351
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Discount (%)"
|
||||||
|
msgstr "Descompte (%)"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:388
|
||||||
|
msgid "Quantity can not be empty."
|
||||||
|
msgstr "No podeu deixar la quantitat en blanc."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:389
|
||||||
|
msgid "Quantity must be a number greater than zero."
|
||||||
|
msgstr "La quantitat ha de ser un número major a zero."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:391
|
||||||
|
msgid "Discount can not be empty."
|
||||||
|
msgstr "No podeu deixar el descompte en blanc."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:392
|
||||||
|
msgid "Discount must be a percentage between 0 and 100."
|
||||||
|
msgstr "El descompte ha de ser un percentatge entre 0 i 100."
|
||||||
|
|
||||||
#: pkg/contacts.go:149
|
#: pkg/contacts.go:149
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Business name"
|
msgid "Business name"
|
||||||
|
|
230
po/es.po
230
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-02-08 13:43+0100\n"
|
"POT-Creation-Date: 2023-02-12 20:51+0100\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"
|
||||||
|
@ -17,6 +17,111 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:2
|
||||||
|
#: web/template/invoices/products.gohtml:15
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Add Products to Invoice"
|
||||||
|
msgstr "Añadir productos a la factura"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:9 web/template/invoices/new.gohtml:9
|
||||||
|
#: web/template/invoices/index.gohtml:8 web/template/contacts/new.gohtml:9
|
||||||
|
#: web/template/contacts/index.gohtml:8 web/template/contacts/edit.gohtml:9
|
||||||
|
#: web/template/profile.gohtml:9 web/template/tax-details.gohtml:8
|
||||||
|
#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:8
|
||||||
|
#: web/template/products/edit.gohtml:9
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Home"
|
||||||
|
msgstr "Inicio"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:10 web/template/invoices/new.gohtml:10
|
||||||
|
#: web/template/invoices/index.gohtml:2 web/template/invoices/index.gohtml:9
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Invoices"
|
||||||
|
msgstr "Facturas"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:11 web/template/invoices/new.gohtml:2
|
||||||
|
#: web/template/invoices/new.gohtml:11 web/template/invoices/new.gohtml:15
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "New Invoice"
|
||||||
|
msgstr "Nueva factura"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:41
|
||||||
|
#: web/template/products/index.gohtml:21
|
||||||
|
msgctxt "product"
|
||||||
|
msgid "All"
|
||||||
|
msgstr "Todos"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:42
|
||||||
|
#: web/template/products/index.gohtml:22
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Nombre"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:43
|
||||||
|
#: web/template/products/index.gohtml:23
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Price"
|
||||||
|
msgstr "Precio"
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:57
|
||||||
|
#: web/template/products/index.gohtml:37
|
||||||
|
msgid "No products added yet."
|
||||||
|
msgstr "No hay productos."
|
||||||
|
|
||||||
|
#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:37
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Add products"
|
||||||
|
msgstr "Añadir productos"
|
||||||
|
|
||||||
|
#: web/template/invoices/new.gohtml:38
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Update"
|
||||||
|
msgstr "Actualizar"
|
||||||
|
|
||||||
|
#: web/template/invoices/new.gohtml:40 web/template/invoices/index.gohtml:13
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "New invoice"
|
||||||
|
msgstr "Nueva factura"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:21
|
||||||
|
msgctxt "invoice"
|
||||||
|
msgid "All"
|
||||||
|
msgstr "Todas"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:22
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Date"
|
||||||
|
msgstr "Fecha"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:23
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Invoice Num."
|
||||||
|
msgstr "Nº factura"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:24 web/template/contacts/index.gohtml:22
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Customer"
|
||||||
|
msgstr "Cliente"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:25
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "Estado"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:26
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Label"
|
||||||
|
msgstr "Etiqueta"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:27
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "Descargar"
|
||||||
|
|
||||||
|
#: web/template/invoices/index.gohtml:45
|
||||||
|
msgid "No invoices added yet."
|
||||||
|
msgstr "No hay facturas."
|
||||||
|
|
||||||
#: web/template/dashboard.gohtml:2
|
#: web/template/dashboard.gohtml:2
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Dashboard"
|
msgid "Dashboard"
|
||||||
|
@ -44,10 +149,15 @@ msgstr "Panel"
|
||||||
|
|
||||||
#: web/template/app.gohtml:44
|
#: web/template/app.gohtml:44
|
||||||
msgctxt "nav"
|
msgctxt "nav"
|
||||||
|
msgid "Invoices"
|
||||||
|
msgstr "Facturas"
|
||||||
|
|
||||||
|
#: web/template/app.gohtml:45
|
||||||
|
msgctxt "nav"
|
||||||
msgid "Products"
|
msgid "Products"
|
||||||
msgstr "Productos"
|
msgstr "Productos"
|
||||||
|
|
||||||
#: web/template/app.gohtml:45
|
#: web/template/app.gohtml:46
|
||||||
msgctxt "nav"
|
msgctxt "nav"
|
||||||
msgid "Contacts"
|
msgid "Contacts"
|
||||||
msgstr "Contactos"
|
msgstr "Contactos"
|
||||||
|
@ -58,14 +168,6 @@ msgctxt "title"
|
||||||
msgid "New Contact"
|
msgid "New Contact"
|
||||||
msgstr "Nuevo contacto"
|
msgstr "Nuevo contacto"
|
||||||
|
|
||||||
#: web/template/contacts/new.gohtml:9 web/template/contacts/index.gohtml:8
|
|
||||||
#: web/template/contacts/edit.gohtml:9 web/template/profile.gohtml:9
|
|
||||||
#: web/template/tax-details.gohtml:8 web/template/products/new.gohtml:9
|
|
||||||
#: web/template/products/index.gohtml:8 web/template/products/edit.gohtml:9
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Home"
|
|
||||||
msgstr "Inicio"
|
|
||||||
|
|
||||||
#: web/template/contacts/new.gohtml:10 web/template/contacts/index.gohtml:2
|
#: web/template/contacts/new.gohtml:10 web/template/contacts/index.gohtml:2
|
||||||
#: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10
|
#: web/template/contacts/index.gohtml:9 web/template/contacts/edit.gohtml:10
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
|
@ -82,11 +184,6 @@ msgctxt "contact"
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Todos"
|
msgstr "Todos"
|
||||||
|
|
||||||
#: web/template/contacts/index.gohtml:22
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Customer"
|
|
||||||
msgstr "Cliente"
|
|
||||||
|
|
||||||
#: web/template/contacts/index.gohtml:23
|
#: web/template/contacts/index.gohtml:23
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
|
@ -199,25 +296,6 @@ msgctxt "action"
|
||||||
msgid "New product"
|
msgid "New product"
|
||||||
msgstr "Nuevo producto"
|
msgstr "Nuevo producto"
|
||||||
|
|
||||||
#: web/template/products/index.gohtml:21
|
|
||||||
msgctxt "product"
|
|
||||||
msgid "All"
|
|
||||||
msgstr "Todos"
|
|
||||||
|
|
||||||
#: web/template/products/index.gohtml:22
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Name"
|
|
||||||
msgstr "Nombre"
|
|
||||||
|
|
||||||
#: web/template/products/index.gohtml:23
|
|
||||||
msgctxt "title"
|
|
||||||
msgid "Price"
|
|
||||||
msgstr "Precio"
|
|
||||||
|
|
||||||
#: web/template/products/index.gohtml:37
|
|
||||||
msgid "No products added yet."
|
|
||||||
msgstr "No hay productos."
|
|
||||||
|
|
||||||
#: web/template/products/edit.gohtml:2 web/template/products/edit.gohtml:15
|
#: web/template/products/edit.gohtml:2 web/template/products/edit.gohtml:15
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Edit Product “%s”"
|
msgid "Edit Product “%s”"
|
||||||
|
@ -254,39 +332,40 @@ 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:193
|
#: pkg/products.go:194 pkg/invoices.go:321
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "Nombre"
|
msgstr "Nombre"
|
||||||
|
|
||||||
#: pkg/products.go:199
|
#: pkg/products.go:200 pkg/invoices.go:327
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Descripción"
|
msgstr "Descripción"
|
||||||
|
|
||||||
#: pkg/products.go:204
|
#: pkg/products.go:205 pkg/invoices.go:332
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Price"
|
msgid "Price"
|
||||||
msgstr "Precio"
|
msgstr "Precio"
|
||||||
|
|
||||||
#: pkg/products.go:214
|
#: pkg/products.go:215 pkg/invoices.go:237 pkg/invoices.go:361
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Taxes"
|
msgid "Taxes"
|
||||||
msgstr "Impuestos"
|
msgstr "Impuestos"
|
||||||
|
|
||||||
#: pkg/products.go:234 pkg/profile.go:92
|
#: pkg/products.go:235 pkg/profile.go:92 pkg/invoices.go:267
|
||||||
|
#: pkg/invoices.go:384
|
||||||
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:235
|
#: pkg/products.go:236 pkg/invoices.go:385
|
||||||
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:236
|
#: pkg/products.go:237 pkg/invoices.go:386
|
||||||
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:238
|
#: pkg/products.go:239 pkg/invoices.go:271 pkg/invoices.go:394
|
||||||
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."
|
||||||
|
|
||||||
|
@ -349,6 +428,73 @@ 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/invoices.go:66
|
||||||
|
msgid "Select a customer to bill."
|
||||||
|
msgstr "Escoged un cliente a facturar."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:163
|
||||||
|
msgid "Invalid action"
|
||||||
|
msgstr "Acción inválida."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:214
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Customer"
|
||||||
|
msgstr "Cliente"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:220
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Number"
|
||||||
|
msgstr "Número"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:226
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Invoice Date"
|
||||||
|
msgstr "Fecha de factura"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:232
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Notes"
|
||||||
|
msgstr "Notas"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:268
|
||||||
|
msgid "Invoice date can not be empty."
|
||||||
|
msgstr "No podéis dejar la fecha de la factura en blanco."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:269
|
||||||
|
msgid "Invoice date must be a valid date."
|
||||||
|
msgstr "La fecha de factura debe ser válida."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:315
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Id"
|
||||||
|
msgstr "Identificador"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:342
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Quantity"
|
||||||
|
msgstr "Cantidad"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:351
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Discount (%)"
|
||||||
|
msgstr "Descuento (%)"
|
||||||
|
|
||||||
|
#: pkg/invoices.go:388
|
||||||
|
msgid "Quantity can not be empty."
|
||||||
|
msgstr "No podéis dejar la cantidad en blanco."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:389
|
||||||
|
msgid "Quantity must be a number greater than zero."
|
||||||
|
msgstr "La cantidad tiene que ser un número mayor a cero."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:391
|
||||||
|
msgid "Discount can not be empty."
|
||||||
|
msgstr "No podéis dejar el descuento en blanco."
|
||||||
|
|
||||||
|
#: pkg/invoices.go:392
|
||||||
|
msgid "Discount must be a percentage between 0 and 100."
|
||||||
|
msgstr "El descuento tiene que ser un percentage entre 0 y 100."
|
||||||
|
|
||||||
#: pkg/contacts.go:149
|
#: pkg/contacts.go:149
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Business name"
|
msgid "Business name"
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
{{ define "hidden-field" -}}
|
{{ define "hidden-field" -}}
|
||||||
<input type="{{ .Type }}" name="{{ .Name }}" id="{{ .Name }}-field"
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InputField*/ -}}
|
||||||
|
<input type="hidden" name="{{ .Name }}"
|
||||||
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
|
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
|
||||||
{{ if .Required }}required="required"{{ end }} value="{{ .Val }}">
|
value="{{ .Val }}">
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{ define "input-field" -}}
|
{{ define "input-field" -}}
|
||||||
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InputField*/ -}}
|
||||||
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
||||||
{{ if eq .Type "textarea" }}
|
{{ if eq .Type "textarea" }}
|
||||||
<textarea name="{{ .Name }}" id="{{ .Name }}-field"
|
<textarea name="{{ .Name }}" id="{{ .Name }}-field"
|
||||||
|
@ -27,7 +29,17 @@
|
||||||
</div>
|
</div>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "hidden-select-field" -}}
|
||||||
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.SelectField*/ -}}
|
||||||
|
{{- range $selected := .Selected }}
|
||||||
|
<input type="hidden" name="{{ $.Name }}"
|
||||||
|
{{- range $attribute := $.Attributes }} {{$attribute}} {{ end }}
|
||||||
|
value="{{ . }}">
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
{{ define "select-field" -}}
|
{{ define "select-field" -}}
|
||||||
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.SelectField*/ -}}
|
||||||
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
||||||
<select id="{{ .Name }}-field" name="{{ .Name }}"
|
<select id="{{ .Name }}-field" name="{{ .Name }}"
|
||||||
{{- range $attribute := .Attributes }} {{$attribute}} {{ end -}}
|
{{- range $attribute := .Attributes }} {{$attribute}} {{ end -}}
|
||||||
|
|
|
@ -28,9 +28,23 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{{ with .Invoices }}
|
||||||
|
{{- range $invoice := . }}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>{{ .Date|formatDate }}</td>
|
||||||
|
<td><a href="{{ companyURI "/invoices/"}}{{ .Slug }}">{{ .Number }}</a></td>
|
||||||
|
<td><a href="{{ companyURI "/contacts/"}}{{ .CustomerSlug }}">{{ .CustomerName }}</a></td>
|
||||||
|
<td class="invoice-status-{{ .Status }}">{{ .StatusLabel }}</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
{{ else }}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7">{{( gettext "No invoices added yet." )}}</td>
|
<td colspan="7">{{( gettext "No invoices added yet." )}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{{ end }}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -34,7 +34,10 @@
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<button class="primary" name="action" value="add" type="submit">{{( pgettext "New invoice" "action" )}}</button>
|
<button name="action" value="products" type="submit">{{( pgettext "Add products" "action" )}}</button>
|
||||||
|
<button name="action" value="update" type="submit">{{( pgettext "Update" "action" )}}</button>
|
||||||
|
<button class="primary" name="action" value="add"
|
||||||
|
type="submit">{{( pgettext "New invoice" "action" )}}</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
{{ define "title" -}}
|
||||||
|
{{( pgettext "Add Products to Invoice" "title" )}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newInvoiceProductsPage*/ -}}
|
||||||
|
<nav>
|
||||||
|
<p>
|
||||||
|
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||||
|
<a href="{{ companyURI "/invoices"}}">{{( pgettext "Invoices" "title" )}}</a> /
|
||||||
|
<a>{{( pgettext "New Invoice" "title" )}}</a>
|
||||||
|
</p>
|
||||||
|
</nav>
|
||||||
|
<section class="dialog-content">
|
||||||
|
<h2>{{(pgettext "Add Products to Invoice" "title")}}</h2>
|
||||||
|
<form method="POST" action="{{ companyURI "/invoices/new/products" }}">
|
||||||
|
{{ csrfToken }}
|
||||||
|
|
||||||
|
{{- with .Form }}
|
||||||
|
{{ template "hidden-select-field" .Customer }}
|
||||||
|
{{ template "hidden-field" .Number }}
|
||||||
|
{{ template "hidden-field" .Date }}
|
||||||
|
{{ template "hidden-field" .Notes }}
|
||||||
|
|
||||||
|
{{- range $product := .Products }}
|
||||||
|
<fieldset>
|
||||||
|
{{ template "hidden-field" .ProductId }}
|
||||||
|
{{ template "hidden-field" .Name }}
|
||||||
|
{{ template "hidden-field" .Description }}
|
||||||
|
{{ template "hidden-field" .Price }}
|
||||||
|
{{ template "hidden-field" .Quantity }}
|
||||||
|
{{ template "hidden-field" .Discount }}
|
||||||
|
{{ template "hidden-select-field" .Tax }}
|
||||||
|
</fieldset>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{( pgettext "All" "product" )}}</th>
|
||||||
|
<th>{{( pgettext "Name" "title" )}}</th>
|
||||||
|
<th>{{( pgettext "Price" "title" )}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ with .Products }}
|
||||||
|
{{- range $product, $key := . }}
|
||||||
|
<tr>
|
||||||
|
<td><input type="checkbox" name="id" id="new-product-id-{{$key}}" value="{{.Id}}"></td>
|
||||||
|
<td><label for="new-product-id-{{$key}}">{{ .Name }}</label></td>
|
||||||
|
<td>{{ .Price | formatPrice }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
{{ else }}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">{{( gettext "No products added yet." )}}</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<button class="primary" type="submit">{{( pgettext "Add products" "action" )}}</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{{- end }}
|
Loading…
Reference in New Issue