Compare commits
4 Commits
dfdc9fde76
...
8efae0485e
Author | SHA1 | Date |
---|---|---|
jordi fita mas | 8efae0485e | |
jordi fita mas | 1ab48d2947 | |
jordi fita mas | c685fc496b | |
jordi fita mas | c882158da3 |
|
@ -20,6 +20,7 @@ $$
|
|||
declare
|
||||
iid integer;
|
||||
products_to_keep integer[];
|
||||
products_to_delete integer[];
|
||||
company integer;
|
||||
ccode text;
|
||||
product edited_invoice_product;
|
||||
|
@ -78,7 +79,16 @@ begin
|
|||
join unnest(product.tax) as ptax(tax_id) using (tax_id);
|
||||
end loop;
|
||||
|
||||
delete from invoice_product where invoice_id = iid and not (invoice_product_id = any(products_to_keep));
|
||||
select array_agg(invoice_product_id)
|
||||
into products_to_delete
|
||||
from invoice_product
|
||||
where invoice_id = iid
|
||||
and not (invoice_product_id = any(products_to_keep));
|
||||
|
||||
if array_length(products_to_delete, 1) > 0 then
|
||||
delete from invoice_product_tax where invoice_product_id = any(products_to_delete);
|
||||
delete from invoice_product where invoice_product_id = any(products_to_delete);
|
||||
end if;
|
||||
|
||||
delete from invoice_tag where invoice_id = iid;
|
||||
|
||||
|
|
17
pkg/form.go
17
pkg/form.go
|
@ -35,7 +35,18 @@ func (field *InputField) Scan(value interface{}) error {
|
|||
field.Val = ""
|
||||
return nil
|
||||
}
|
||||
field.Val = fmt.Sprintf("%v", value)
|
||||
switch v := value.(type) {
|
||||
case time.Time:
|
||||
if field.Type == "date" {
|
||||
field.Val = v.Format("2006-01-02")
|
||||
} else if field.Type == "time" {
|
||||
field.Val = v.Format("15:04")
|
||||
} else {
|
||||
field.Val = v.Format(time.RFC3339)
|
||||
}
|
||||
default:
|
||||
field.Val = fmt.Sprintf("%v", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -63,6 +74,10 @@ func (field *InputField) Float64() float64 {
|
|||
return value
|
||||
}
|
||||
|
||||
func (field *InputField) String() string {
|
||||
return field.Val
|
||||
}
|
||||
|
||||
type SelectOption struct {
|
||||
Value string
|
||||
Label string
|
||||
|
|
209
pkg/invoices.go
209
pkg/invoices.go
|
@ -117,14 +117,15 @@ func mustCollectInvoiceStatuses(ctx context.Context, conn *Conn, locale *Locale)
|
|||
}
|
||||
|
||||
func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
locale := getLocale(r)
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
form := newInvoiceForm(r.Context(), conn, locale, company)
|
||||
slug := params[0].Value
|
||||
if slug == "new" {
|
||||
locale := getLocale(r)
|
||||
form := newInvoiceForm(r.Context(), conn, locale, company)
|
||||
if invoiceToDuplicate := r.URL.Query().Get("duplicate"); invoiceToDuplicate != "" {
|
||||
form.MustFillFromDatabase(r.Context(), conn, invoiceToDuplicate)
|
||||
form.InvoiceStatus.Selected = []string{"created"}
|
||||
form.Number.Val = ""
|
||||
}
|
||||
form.Date.Val = time.Now().Format("2006-01-02")
|
||||
|
@ -304,10 +305,11 @@ func mustRenderNewInvoiceForm(w http.ResponseWriter, r *http.Request, form *invo
|
|||
mustRenderAppTemplate(w, r, "invoices/new.gohtml", page)
|
||||
}
|
||||
|
||||
func mustRenderNewInvoiceProductsForm(w http.ResponseWriter, r *http.Request, form *invoiceForm) {
|
||||
func mustRenderNewInvoiceProductsForm(w http.ResponseWriter, r *http.Request, action string, form *invoiceForm) {
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
page := newInvoiceProductsPage{
|
||||
Action: companyURI(company, action),
|
||||
Form: form,
|
||||
Products: mustGetProductChoices(r.Context(), conn, company),
|
||||
}
|
||||
|
@ -334,6 +336,7 @@ func mustGetProductChoices(ctx context.Context, conn *Conn, company *Company) []
|
|||
}
|
||||
|
||||
type newInvoiceProductsPage struct {
|
||||
Action string
|
||||
Form *invoiceForm
|
||||
Products []*productChoice
|
||||
}
|
||||
|
@ -362,39 +365,18 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param
|
|||
mustRenderNewInvoiceForm(w, r, form)
|
||||
return
|
||||
}
|
||||
reg := regexp.MustCompile("[^a-z0-9-]+")
|
||||
tags := strings.Split(reg.ReplaceAllString(form.Tags.Val, " "), " ")
|
||||
slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7, $8)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, tags, NewInvoiceProductArray(form.Products))
|
||||
slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7, $8)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, form.SplitTags(), NewInvoiceProductArray(form.Products))
|
||||
http.Redirect(w, r, companyURI(company, "/invoices/"+slug), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func HandleNewInvoiceAction(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 "select-products":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
mustRenderNewInvoiceProductsForm(w, r, form)
|
||||
case "add-products":
|
||||
form.AddProducts(r.Context(), conn, r.Form["id"])
|
||||
w.WriteHeader(http.StatusOK)
|
||||
mustRenderNewInvoiceForm(w, r, form)
|
||||
func HandleNewInvoiceAction(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
switch params[0].Value {
|
||||
case "new":
|
||||
handleInvoiceAction(w, r, "/invoices/new", mustRenderNewInvoiceForm)
|
||||
case "batch":
|
||||
HandleBatchInvoiceAction(w, r, params)
|
||||
default:
|
||||
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -450,6 +432,7 @@ func mustWriteInvoicesPdf(r *http.Request, slugs []string) []byte {
|
|||
type invoiceForm struct {
|
||||
locale *Locale
|
||||
company *Company
|
||||
InvoiceStatus *SelectField
|
||||
Customer *SelectField
|
||||
Number *InputField
|
||||
Date *InputField
|
||||
|
@ -463,6 +446,13 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
|||
return &invoiceForm{
|
||||
locale: locale,
|
||||
company: company,
|
||||
InvoiceStatus: &SelectField{
|
||||
Name: "invoice_status",
|
||||
Required: true,
|
||||
Label: pgettext("input", "Invoice Status", locale),
|
||||
Selected: []string{"created"},
|
||||
Options: MustGetOptions(ctx, conn, "select invoice_status.invoice_status, isi18n.name from invoice_status join invoice_status_i18n isi18n using(invoice_status) where isi18n.lang_tag = $1 order by invoice_status", locale.Language.String()),
|
||||
},
|
||||
Customer: &SelectField{
|
||||
Name: "customer",
|
||||
Label: pgettext("input", "Customer", locale),
|
||||
|
@ -504,6 +494,7 @@ func (form *invoiceForm) Parse(r *http.Request) error {
|
|||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
form.InvoiceStatus.FillValue(r)
|
||||
form.Customer.FillValue(r)
|
||||
form.Number.FillValue(r)
|
||||
form.Date.FillValue(r)
|
||||
|
@ -529,7 +520,8 @@ func (form *invoiceForm) Parse(r *http.Request) error {
|
|||
func (form *invoiceForm) Validate() bool {
|
||||
validator := newFormValidator()
|
||||
|
||||
validator.CheckValidSelectOption(form.Customer, gettext("Name can not be empty.", form.locale))
|
||||
validator.CheckValidSelectOption(form.InvoiceStatus, gettext("Selected invoice status is not valid.", form.locale))
|
||||
validator.CheckValidSelectOption(form.Customer, gettext("Selected customer is not valid.", 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))
|
||||
}
|
||||
|
@ -556,7 +548,7 @@ func (form *invoiceForm) Update() {
|
|||
}
|
||||
|
||||
func (form *invoiceForm) AddProducts(ctx context.Context, conn *Conn, productsId []string) {
|
||||
form.mustAddProductsFromQuery(ctx, conn, "select product_id, name, description, to_price(price, decimal_digits), 1 as quantity, 0 as discount, array_remove(array_agg(tax_id), null) 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)
|
||||
form.mustAddProductsFromQuery(ctx, conn, "select '', product_id, name, description, to_price(price, decimal_digits), 1 as quantity, 0 as discount, array_remove(array_agg(tax_id), null) 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)
|
||||
}
|
||||
|
||||
func (form *invoiceForm) mustAddProductsFromQuery(ctx context.Context, conn *Conn, sql string, args ...interface{}) {
|
||||
|
@ -566,7 +558,7 @@ func (form *invoiceForm) mustAddProductsFromQuery(ctx context.Context, conn *Con
|
|||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
product := newInvoiceProductForm(index, form.company, form.locale, taxOptions)
|
||||
if err := rows.Scan(product.ProductId, product.Name, product.Description, product.Price, product.Quantity, product.Discount, product.Tax); err != nil {
|
||||
if err := rows.Scan(product.InvoiceProductId, product.ProductId, product.Name, product.Description, product.Price, product.Quantity, product.Discount, product.Tax); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
form.Products = append(form.Products, product)
|
||||
|
@ -577,12 +569,15 @@ func (form *invoiceForm) mustAddProductsFromQuery(ctx context.Context, conn *Con
|
|||
}
|
||||
}
|
||||
|
||||
func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) {
|
||||
func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
|
||||
var invoiceId int
|
||||
selectedInvoiceStatus := form.InvoiceStatus.Selected
|
||||
form.InvoiceStatus.Clear()
|
||||
selectedPaymentMethod := form.PaymentMethod.Selected
|
||||
form.PaymentMethod.Clear()
|
||||
if notFoundErrorOrPanic(conn.QueryRow(ctx, `
|
||||
select invoice_id
|
||||
, invoice_status
|
||||
, contact_id
|
||||
, invoice_number
|
||||
, invoice_date
|
||||
|
@ -598,34 +593,47 @@ func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
|
|||
, invoice_date
|
||||
, notes
|
||||
, payment_method_id
|
||||
`, slug).Scan(&invoiceId, form.Customer, form.Number, form.Date, form.Notes, form.PaymentMethod, form.Tags)) {
|
||||
`, slug).Scan(&invoiceId, form.InvoiceStatus, form.Customer, form.Number, form.Date, form.Notes, form.PaymentMethod, form.Tags)) {
|
||||
form.PaymentMethod.Selected = selectedPaymentMethod
|
||||
return
|
||||
form.InvoiceStatus.Selected = selectedInvoiceStatus
|
||||
return false
|
||||
}
|
||||
form.Products = []*invoiceProductForm{}
|
||||
form.mustAddProductsFromQuery(ctx, conn, "select product_id, name, description, to_price(price, $2), quantity, (discount_rate * 100)::integer, array_remove(array_agg(tax_id), null) from invoice_product left join invoice_product_tax using (invoice_product_id) where invoice_id = $1 group by product_id, name, description, discount_rate, price, quantity", invoiceId, form.company.DecimalDigits)
|
||||
form.mustAddProductsFromQuery(ctx, conn, "select invoice_product_id::text, product_id, name, description, to_price(price, $2), quantity, (discount_rate * 100)::integer, array_remove(array_agg(tax_id), null) from invoice_product left join invoice_product_tax using (invoice_product_id) where invoice_id = $1 group by invoice_product_id, product_id, name, description, discount_rate, price, quantity", invoiceId, form.company.DecimalDigits)
|
||||
return true
|
||||
}
|
||||
|
||||
func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption {
|
||||
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 (form *invoiceForm) SplitTags() []string {
|
||||
reg := regexp.MustCompile("[^a-z0-9-]+")
|
||||
return strings.Split(reg.ReplaceAllString(form.Tags.Val, " "), " ")
|
||||
}
|
||||
|
||||
type invoiceProductForm struct {
|
||||
locale *Locale
|
||||
company *Company
|
||||
ProductId *InputField
|
||||
Name *InputField
|
||||
Description *InputField
|
||||
Price *InputField
|
||||
Quantity *InputField
|
||||
Discount *InputField
|
||||
Tax *SelectField
|
||||
locale *Locale
|
||||
company *Company
|
||||
InvoiceProductId *InputField
|
||||
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 {
|
||||
form := &invoiceProductForm{
|
||||
locale: locale,
|
||||
company: company,
|
||||
InvoiceProductId: &InputField{
|
||||
Label: pgettext("input", "Id", locale),
|
||||
Type: "hidden",
|
||||
Required: true,
|
||||
},
|
||||
ProductId: &InputField{
|
||||
Label: pgettext("input", "Id", locale),
|
||||
Type: "hidden",
|
||||
|
@ -678,6 +686,7 @@ func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptio
|
|||
|
||||
func (form *invoiceProductForm) Reindex(index int) {
|
||||
suffix := "." + strconv.Itoa(index)
|
||||
form.InvoiceProductId.Name = "product.invoice_product_id" + suffix
|
||||
form.ProductId.Name = "product.id" + suffix
|
||||
form.Name.Name = "product.name" + suffix
|
||||
form.Description.Name = "product.description" + suffix
|
||||
|
@ -691,6 +700,7 @@ func (form *invoiceProductForm) Parse(r *http.Request) error {
|
|||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
form.InvoiceProductId.FillValue(r)
|
||||
form.ProductId.FillValue(r)
|
||||
form.Name.FillValue(r)
|
||||
form.Description.FillValue(r)
|
||||
|
@ -703,6 +713,7 @@ func (form *invoiceProductForm) Parse(r *http.Request) error {
|
|||
|
||||
func (form *invoiceProductForm) Validate() bool {
|
||||
validator := newFormValidator()
|
||||
validator.CheckRequiredInput(form.ProductId, gettext("Product ID can not be empty.", form.locale))
|
||||
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))
|
||||
|
@ -719,8 +730,11 @@ func (form *invoiceProductForm) Validate() bool {
|
|||
}
|
||||
|
||||
func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
locale := getLocale(r)
|
||||
conn := getConn(r)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
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
|
||||
}
|
||||
|
@ -728,10 +742,97 @@ func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprout
|
|||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
invoiceStatus := r.FormValue("status")
|
||||
slug := conn.MustGetText(r.Context(), "", "update invoice set invoice_status = $1 where slug = $2 returning slug", invoiceStatus, params[0].Value)
|
||||
if slug == "" {
|
||||
http.NotFound(w, r)
|
||||
if r.FormValue("quick") == "status" {
|
||||
slug := conn.MustGetText(r.Context(), "", "update invoice set invoice_status = $1 where slug = $2 returning slug", form.InvoiceStatus, params[0].Value)
|
||||
if slug == "" {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
http.Redirect(w, r, companyURI(mustGetCompany(r), "/invoices"), http.StatusSeeOther)
|
||||
} else {
|
||||
slug := params[0].Value
|
||||
if !form.Validate() {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
mustRenderEditInvoiceForm(w, r, slug, form)
|
||||
return
|
||||
}
|
||||
slug = conn.MustGetText(r.Context(), "", "select edit_invoice($1, $2, $3, $4, $5, $6, $7)", slug, form.InvoiceStatus, form.Customer, form.Notes, form.PaymentMethod, form.SplitTags(), EditedInvoiceProductArray(form.Products))
|
||||
if slug == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, companyURI(company, "/invoices/"+slug), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func ServeEditInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
slug := params[0].Value
|
||||
locale := getLocale(r)
|
||||
form := newInvoiceForm(r.Context(), conn, locale, company)
|
||||
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
mustRenderEditInvoiceForm(w, r, slug, form)
|
||||
}
|
||||
|
||||
type editInvoicePage struct {
|
||||
*newInvoicePage
|
||||
Slug string
|
||||
Number string
|
||||
}
|
||||
|
||||
func newEditInvoicePage(slug string, form *invoiceForm, r *http.Request) *editInvoicePage {
|
||||
return &editInvoicePage{
|
||||
newNewInvoicePage(form, r),
|
||||
slug,
|
||||
form.Number.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func mustRenderEditInvoiceForm(w http.ResponseWriter, r *http.Request, slug string, form *invoiceForm) {
|
||||
page := newEditInvoicePage(slug, form, r)
|
||||
mustRenderAppTemplate(w, r, "invoices/edit.gohtml", page)
|
||||
}
|
||||
|
||||
func HandleEditInvoiceAction(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
slug := params[0].Value
|
||||
actionUri := fmt.Sprintf("/invoices/%s/edit", slug)
|
||||
handleInvoiceAction(w, r, actionUri, func(w http.ResponseWriter, r *http.Request, form *invoiceForm) {
|
||||
mustRenderEditInvoiceForm(w, r, slug, form)
|
||||
})
|
||||
}
|
||||
|
||||
type renderFormFunc func(w http.ResponseWriter, r *http.Request, form *invoiceForm)
|
||||
|
||||
func handleInvoiceAction(w http.ResponseWriter, r *http.Request, action string, renderForm renderFormFunc) {
|
||||
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)
|
||||
renderForm(w, r, form)
|
||||
case "select-products":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
mustRenderNewInvoiceProductsForm(w, r, action, form)
|
||||
case "add-products":
|
||||
form.AddProducts(r.Context(), conn, r.Form["id"])
|
||||
w.WriteHeader(http.StatusOK)
|
||||
renderForm(w, r, form)
|
||||
default:
|
||||
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
|
||||
}
|
||||
http.Redirect(w, r, companyURI(mustGetCompany(r), "/invoices"), http.StatusSeeOther)
|
||||
}
|
||||
|
|
|
@ -34,6 +34,40 @@ func (src NewInvoiceProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte)
|
|||
return array.EncodeBinary(ci, buf)
|
||||
}
|
||||
|
||||
type EditedInvoiceProductArray []*invoiceProductForm
|
||||
|
||||
func (src EditedInvoiceProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
|
||||
typeName := "edited_invoice_product[]"
|
||||
dt, ok := ci.DataTypeForName(typeName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
||||
}
|
||||
var values [][]interface{}
|
||||
for _, form := range src {
|
||||
var invoiceProductId interface{} = nil
|
||||
if form.InvoiceProductId.Val != "" {
|
||||
if id := form.InvoiceProductId.Integer(); id > 0 {
|
||||
invoiceProductId = id
|
||||
}
|
||||
}
|
||||
values = append(values, []interface{}{
|
||||
invoiceProductId,
|
||||
form.ProductId.Val,
|
||||
form.Name.Val,
|
||||
form.Description.Val,
|
||||
form.Price.Val,
|
||||
form.Quantity.Val,
|
||||
form.Discount.Float64() / 100.0,
|
||||
form.Tax.Selected,
|
||||
})
|
||||
}
|
||||
array := pgtype.NewValue(dt.Value).(pgtype.ValueTranscoder)
|
||||
if err := array.Set(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return array.EncodeBinary(ci, buf)
|
||||
}
|
||||
|
||||
func registerPgTypes(ctx context.Context, conn *pgx.Conn) error {
|
||||
if _, err := conn.Exec(ctx, "set role to admin"); err != nil {
|
||||
return err
|
||||
|
@ -71,6 +105,36 @@ func registerPgTypes(ctx context.Context, conn *pgx.Conn) error {
|
|||
return err
|
||||
}
|
||||
|
||||
editedInvoiceProduct, err := pgtype.NewCompositeType(
|
||||
"edited_invoice_product",
|
||||
[]pgtype.CompositeTypeField{
|
||||
{"invoice_product_id", pgtype.Int4OID},
|
||||
{"product_id", pgtype.Int4OID},
|
||||
{"name", pgtype.TextOID},
|
||||
{"description", pgtype.TextOID},
|
||||
{"price", pgtype.TextOID},
|
||||
{"quantity", pgtype.Int4OID},
|
||||
{"discount_rate", discountRateOID},
|
||||
{"tax", pgtype.Int4ArrayOID},
|
||||
},
|
||||
conn.ConnInfo(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
editedInvoiceProductOID, err := registerPgType(ctx, conn, editedInvoiceProduct, editedInvoiceProduct.TypeName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
editedInvoiceProductArray := pgtype.NewArrayType("edited_invoice_product[]", editedInvoiceProductOID, func() pgtype.ValueTranscoder {
|
||||
value := editedInvoiceProduct.NewTypeValue()
|
||||
return value.(pgtype.ValueTranscoder)
|
||||
})
|
||||
_, err = registerPgType(ctx, conn, editedInvoiceProductArray, editedInvoiceProductArray.TypeName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = conn.Exec(ctx, "reset role")
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -28,8 +28,9 @@ func NewRouter(db *Db) http.Handler {
|
|||
companyRouter.POST("/invoices", HandleAddInvoice)
|
||||
companyRouter.GET("/invoices/:slug", ServeInvoice)
|
||||
companyRouter.PUT("/invoices/:slug", HandleUpdateInvoice)
|
||||
companyRouter.POST("/invoices/new", HandleNewInvoiceAction)
|
||||
companyRouter.POST("/invoices/batch", HandleBatchInvoiceAction)
|
||||
companyRouter.POST("/invoices/:slug", HandleNewInvoiceAction)
|
||||
companyRouter.GET("/invoices/:slug/edit", ServeEditInvoice)
|
||||
companyRouter.POST("/invoices/:slug/edit", HandleEditInvoiceAction)
|
||||
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
|
||||
})
|
||||
|
|
154
po/ca.po
154
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-03-10 13:59+0100\n"
|
||||
"POT-Creation-Date: 2023-03-13 14:50+0100\n"
|
||||
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||
|
@ -18,79 +18,82 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: web/template/invoices/products.gohtml:2
|
||||
#: web/template/invoices/products.gohtml:15
|
||||
#: web/template/invoices/products.gohtml:19
|
||||
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/invoices/view.gohtml:9
|
||||
#: 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
|
||||
#: web/template/invoices/edit.gohtml:9 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
|
||||
#: web/template/invoices/view.gohtml:10
|
||||
#: web/template/invoices/view.gohtml:10 web/template/invoices/edit.gohtml:10
|
||||
msgctxt "title"
|
||||
msgid "Invoices"
|
||||
msgstr "Factures"
|
||||
|
||||
#: web/template/invoices/products.gohtml:11 web/template/invoices/new.gohtml:2
|
||||
#: web/template/invoices/products.gohtml:12 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:42
|
||||
#: web/template/invoices/products.gohtml:47
|
||||
#: web/template/products/index.gohtml:21
|
||||
msgctxt "product"
|
||||
msgid "All"
|
||||
msgstr "Tots"
|
||||
|
||||
#: web/template/invoices/products.gohtml:43
|
||||
#: web/template/invoices/products.gohtml:48
|
||||
#: web/template/products/index.gohtml:22
|
||||
msgctxt "title"
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: web/template/invoices/products.gohtml:44
|
||||
#: web/template/invoices/view.gohtml:54 web/template/products/index.gohtml:23
|
||||
#: web/template/invoices/products.gohtml:49
|
||||
#: web/template/invoices/view.gohtml:56 web/template/products/index.gohtml:23
|
||||
msgctxt "title"
|
||||
msgid "Price"
|
||||
msgstr "Preu"
|
||||
|
||||
#: web/template/invoices/products.gohtml:58
|
||||
#: web/template/invoices/products.gohtml:63
|
||||
#: web/template/products/index.gohtml:37
|
||||
msgid "No products added yet."
|
||||
msgstr "No hi ha cap producte."
|
||||
|
||||
#: web/template/invoices/products.gohtml:66 web/template/invoices/new.gohtml:62
|
||||
#: web/template/invoices/products.gohtml:71 web/template/invoices/new.gohtml:63
|
||||
#: web/template/invoices/edit.gohtml:64
|
||||
msgctxt "action"
|
||||
msgid "Add products"
|
||||
msgstr "Afegeix productes"
|
||||
|
||||
#: web/template/invoices/new.gohtml:43 web/template/invoices/view.gohtml:59
|
||||
#: web/template/invoices/new.gohtml:44 web/template/invoices/view.gohtml:61
|
||||
#: web/template/invoices/edit.gohtml:45
|
||||
msgctxt "title"
|
||||
msgid "Subtotal"
|
||||
msgstr "Subtotal"
|
||||
|
||||
#: web/template/invoices/new.gohtml:53 web/template/invoices/view.gohtml:63
|
||||
#: web/template/invoices/view.gohtml:103
|
||||
#: web/template/invoices/new.gohtml:54 web/template/invoices/view.gohtml:65
|
||||
#: web/template/invoices/view.gohtml:105 web/template/invoices/edit.gohtml:55
|
||||
msgctxt "title"
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
||||
#: web/template/invoices/new.gohtml:65
|
||||
#: web/template/invoices/new.gohtml:66 web/template/invoices/edit.gohtml:67
|
||||
msgctxt "action"
|
||||
msgid "Update"
|
||||
msgstr "Actualitza"
|
||||
|
||||
#: web/template/invoices/new.gohtml:67 web/template/invoices/index.gohtml:19
|
||||
#: web/template/invoices/new.gohtml:68 web/template/invoices/index.gohtml:19
|
||||
msgctxt "action"
|
||||
msgid "New invoice"
|
||||
msgstr "Nova factura"
|
||||
|
@ -105,7 +108,7 @@ msgctxt "invoice"
|
|||
msgid "All"
|
||||
msgstr "Totes"
|
||||
|
||||
#: web/template/invoices/index.gohtml:29 web/template/invoices/view.gohtml:26
|
||||
#: web/template/invoices/index.gohtml:29 web/template/invoices/view.gohtml:28
|
||||
msgctxt "title"
|
||||
msgid "Date"
|
||||
msgstr "Data"
|
||||
|
@ -150,45 +153,60 @@ msgctxt "action"
|
|||
msgid "Select invoice %v"
|
||||
msgstr "Selecciona factura %v"
|
||||
|
||||
#: web/template/invoices/index.gohtml:91 web/template/invoices/view.gohtml:14
|
||||
#: web/template/invoices/index.gohtml:92 web/template/invoices/view.gohtml:16
|
||||
msgctxt "action"
|
||||
msgid "Edit"
|
||||
msgstr "Edita"
|
||||
|
||||
#: web/template/invoices/index.gohtml:98 web/template/invoices/view.gohtml:15
|
||||
msgctxt "action"
|
||||
msgid "Duplicate"
|
||||
msgstr "Duplica"
|
||||
|
||||
#: web/template/invoices/index.gohtml:101
|
||||
#: web/template/invoices/index.gohtml:108
|
||||
msgid "No invoices added yet."
|
||||
msgstr "No hi ha cap factura."
|
||||
|
||||
#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:25
|
||||
#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:27
|
||||
msgctxt "title"
|
||||
msgid "Invoice %s"
|
||||
msgstr "Factura %s"
|
||||
|
||||
#: web/template/invoices/view.gohtml:17
|
||||
#: web/template/invoices/view.gohtml:19
|
||||
msgctxt "action"
|
||||
msgid "Download invoice"
|
||||
msgstr "Descarrega factura"
|
||||
|
||||
#: web/template/invoices/view.gohtml:53
|
||||
#: web/template/invoices/view.gohtml:55
|
||||
msgctxt "title"
|
||||
msgid "Concept"
|
||||
msgstr "Concepte"
|
||||
|
||||
#: web/template/invoices/view.gohtml:56
|
||||
#: web/template/invoices/view.gohtml:58
|
||||
msgctxt "title"
|
||||
msgid "Discount"
|
||||
msgstr "Descompte"
|
||||
|
||||
#: web/template/invoices/view.gohtml:58
|
||||
#: web/template/invoices/view.gohtml:60
|
||||
msgctxt "title"
|
||||
msgid "Units"
|
||||
msgstr "Unitats"
|
||||
|
||||
#: web/template/invoices/view.gohtml:93
|
||||
#: web/template/invoices/view.gohtml:95
|
||||
msgctxt "title"
|
||||
msgid "Tax Base"
|
||||
msgstr "Base imposable"
|
||||
|
||||
#: web/template/invoices/edit.gohtml:2 web/template/invoices/edit.gohtml:15
|
||||
msgctxt "title"
|
||||
msgid "Edit Invoice “%s”"
|
||||
msgstr "Edició de la factura «%s»"
|
||||
|
||||
#: web/template/invoices/edit.gohtml:69
|
||||
msgctxt "action"
|
||||
msgid "Edit invoice"
|
||||
msgstr "Edita factura"
|
||||
|
||||
#: web/template/dashboard.gohtml:2
|
||||
msgctxt "title"
|
||||
msgid "Dashboard"
|
||||
|
@ -428,44 +446,43 @@ msgstr "No podeu deixar la contrasenya en blanc."
|
|||
msgid "Invalid user or password."
|
||||
msgstr "Nom d’usuari o contrasenya incorrectes."
|
||||
|
||||
#: pkg/products.go:165 pkg/invoices.go:635
|
||||
#: pkg/products.go:165 pkg/invoices.go:643
|
||||
msgctxt "input"
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: pkg/products.go:171 pkg/invoices.go:640
|
||||
#: pkg/products.go:171 pkg/invoices.go:648
|
||||
msgctxt "input"
|
||||
msgid "Description"
|
||||
msgstr "Descripció"
|
||||
|
||||
#: pkg/products.go:176 pkg/invoices.go:644
|
||||
#: pkg/products.go:176 pkg/invoices.go:652
|
||||
msgctxt "input"
|
||||
msgid "Price"
|
||||
msgstr "Preu"
|
||||
|
||||
#: pkg/products.go:186 pkg/invoices.go:670
|
||||
#: pkg/products.go:186 pkg/invoices.go:678
|
||||
msgctxt "input"
|
||||
msgid "Taxes"
|
||||
msgstr "Imposts"
|
||||
|
||||
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:532
|
||||
#: pkg/invoices.go:706
|
||||
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:717
|
||||
msgid "Name can not be empty."
|
||||
msgstr "No podeu deixar el nom en blanc."
|
||||
|
||||
#: pkg/products.go:207 pkg/invoices.go:707
|
||||
#: pkg/products.go:207 pkg/invoices.go:718
|
||||
msgid "Price can not be empty."
|
||||
msgstr "No podeu deixar el preu en blanc."
|
||||
|
||||
#: pkg/products.go:208 pkg/invoices.go:708
|
||||
#: pkg/products.go:208 pkg/invoices.go:719
|
||||
msgid "Price must be a number greater than zero."
|
||||
msgstr "El preu ha de ser un número major a zero."
|
||||
|
||||
#: pkg/products.go:210 pkg/invoices.go:716
|
||||
#: pkg/products.go:210 pkg/invoices.go:727
|
||||
msgid "Selected tax is not valid."
|
||||
msgstr "Heu seleccionat un impost que no és vàlid."
|
||||
|
||||
#: pkg/products.go:211 pkg/invoices.go:717
|
||||
#: pkg/products.go:211 pkg/invoices.go:728
|
||||
msgid "You can only select a tax of each class."
|
||||
msgstr "Només podeu seleccionar un impost de cada classe."
|
||||
|
||||
|
@ -573,88 +590,105 @@ msgstr "La confirmació no és igual a la contrasenya."
|
|||
msgid "Selected language is not valid."
|
||||
msgstr "Heu seleccionat un idioma que no és vàlid."
|
||||
|
||||
#: pkg/invoices.go:302
|
||||
#: pkg/invoices.go:303
|
||||
msgid "Select a customer to bill."
|
||||
msgstr "Escolliu un client a facturar."
|
||||
|
||||
#: pkg/invoices.go:397 pkg/invoices.go:426
|
||||
msgid "Invalid action"
|
||||
msgstr "Acció invàlida."
|
||||
|
||||
#: pkg/invoices.go:420
|
||||
#: pkg/invoices.go:402
|
||||
msgid "invoices.zip"
|
||||
msgstr "factures.zip"
|
||||
|
||||
#: pkg/invoices.go:468
|
||||
#: pkg/invoices.go:408 pkg/invoices.go:836
|
||||
msgid "Invalid action"
|
||||
msgstr "Acció invàlida."
|
||||
|
||||
#: pkg/invoices.go:452
|
||||
msgctxt "input"
|
||||
msgid "Invoice Status"
|
||||
msgstr "Estat de la factura"
|
||||
|
||||
#: pkg/invoices.go:458
|
||||
msgctxt "input"
|
||||
msgid "Customer"
|
||||
msgstr "Client"
|
||||
|
||||
#: pkg/invoices.go:474
|
||||
#: pkg/invoices.go:464
|
||||
msgctxt "input"
|
||||
msgid "Number"
|
||||
msgstr "Número"
|
||||
|
||||
#: pkg/invoices.go:479
|
||||
#: pkg/invoices.go:469
|
||||
msgctxt "input"
|
||||
msgid "Invoice Date"
|
||||
msgstr "Data de factura"
|
||||
|
||||
#: pkg/invoices.go:485
|
||||
#: pkg/invoices.go:475
|
||||
msgctxt "input"
|
||||
msgid "Notes"
|
||||
msgstr "Notes"
|
||||
|
||||
#: pkg/invoices.go:490
|
||||
#: pkg/invoices.go:480
|
||||
msgctxt "input"
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetes"
|
||||
|
||||
#: pkg/invoices.go:496
|
||||
#: pkg/invoices.go:486
|
||||
msgctxt "input"
|
||||
msgid "Payment Method"
|
||||
msgstr "Mètode de pagament"
|
||||
|
||||
#: pkg/invoices.go:533
|
||||
#: pkg/invoices.go:523
|
||||
msgid "Selected invoice status is not valid."
|
||||
msgstr "Heu seleccionat un estat de factura que no és vàlid."
|
||||
|
||||
#: pkg/invoices.go:524
|
||||
msgid "Selected customer is not valid."
|
||||
msgstr "Heu seleccionat un client que no és vàlid."
|
||||
|
||||
#: pkg/invoices.go:525
|
||||
msgid "Invoice date can not be empty."
|
||||
msgstr "No podeu deixar la data de la factura en blanc."
|
||||
|
||||
#: pkg/invoices.go:534
|
||||
#: pkg/invoices.go:526
|
||||
msgid "Invoice date must be a valid date."
|
||||
msgstr "La data de facturació ha de ser vàlida."
|
||||
|
||||
#: pkg/invoices.go:536
|
||||
#: pkg/invoices.go:528
|
||||
msgid "Selected payment method is not valid."
|
||||
msgstr "Heu seleccionat un mètode de pagament que no és vàlid."
|
||||
|
||||
#: pkg/invoices.go:630
|
||||
#: pkg/invoices.go:633 pkg/invoices.go:638
|
||||
msgctxt "input"
|
||||
msgid "Id"
|
||||
msgstr "Identificador"
|
||||
|
||||
#: pkg/invoices.go:653
|
||||
#: pkg/invoices.go:661
|
||||
msgctxt "input"
|
||||
msgid "Quantity"
|
||||
msgstr "Quantitat"
|
||||
|
||||
#: pkg/invoices.go:661
|
||||
#: pkg/invoices.go:669
|
||||
msgctxt "input"
|
||||
msgid "Discount (%)"
|
||||
msgstr "Descompte (%)"
|
||||
|
||||
#: pkg/invoices.go:710
|
||||
#: pkg/invoices.go:716
|
||||
msgid "Product ID can not be empty."
|
||||
msgstr "No podeu deixar l’identificador del producte en blanc."
|
||||
|
||||
#: pkg/invoices.go:721
|
||||
msgid "Quantity can not be empty."
|
||||
msgstr "No podeu deixar la quantitat en blanc."
|
||||
|
||||
#: pkg/invoices.go:711
|
||||
#: pkg/invoices.go:722
|
||||
msgid "Quantity must be a number greater than zero."
|
||||
msgstr "La quantitat ha de ser un número major a zero."
|
||||
|
||||
#: pkg/invoices.go:713
|
||||
#: pkg/invoices.go:724
|
||||
msgid "Discount can not be empty."
|
||||
msgstr "No podeu deixar el descompte en blanc."
|
||||
|
||||
#: pkg/invoices.go:714
|
||||
#: pkg/invoices.go:725
|
||||
msgid "Discount must be a percentage between 0 and 100."
|
||||
msgstr "El descompte ha de ser un percentatge entre 0 i 100."
|
||||
|
||||
|
|
154
po/es.po
154
po/es.po
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-03-10 13:59+0100\n"
|
||||
"POT-Creation-Date: 2023-03-13 14:50+0100\n"
|
||||
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||
|
@ -18,79 +18,82 @@ msgstr ""
|
|||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: web/template/invoices/products.gohtml:2
|
||||
#: web/template/invoices/products.gohtml:15
|
||||
#: web/template/invoices/products.gohtml:19
|
||||
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/invoices/view.gohtml:9
|
||||
#: 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
|
||||
#: web/template/invoices/edit.gohtml:9 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
|
||||
#: web/template/invoices/view.gohtml:10
|
||||
#: web/template/invoices/view.gohtml:10 web/template/invoices/edit.gohtml:10
|
||||
msgctxt "title"
|
||||
msgid "Invoices"
|
||||
msgstr "Facturas"
|
||||
|
||||
#: web/template/invoices/products.gohtml:11 web/template/invoices/new.gohtml:2
|
||||
#: web/template/invoices/products.gohtml:12 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:42
|
||||
#: web/template/invoices/products.gohtml:47
|
||||
#: web/template/products/index.gohtml:21
|
||||
msgctxt "product"
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
#: web/template/invoices/products.gohtml:43
|
||||
#: web/template/invoices/products.gohtml:48
|
||||
#: web/template/products/index.gohtml:22
|
||||
msgctxt "title"
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: web/template/invoices/products.gohtml:44
|
||||
#: web/template/invoices/view.gohtml:54 web/template/products/index.gohtml:23
|
||||
#: web/template/invoices/products.gohtml:49
|
||||
#: web/template/invoices/view.gohtml:56 web/template/products/index.gohtml:23
|
||||
msgctxt "title"
|
||||
msgid "Price"
|
||||
msgstr "Precio"
|
||||
|
||||
#: web/template/invoices/products.gohtml:58
|
||||
#: web/template/invoices/products.gohtml:63
|
||||
#: web/template/products/index.gohtml:37
|
||||
msgid "No products added yet."
|
||||
msgstr "No hay productos."
|
||||
|
||||
#: web/template/invoices/products.gohtml:66 web/template/invoices/new.gohtml:62
|
||||
#: web/template/invoices/products.gohtml:71 web/template/invoices/new.gohtml:63
|
||||
#: web/template/invoices/edit.gohtml:64
|
||||
msgctxt "action"
|
||||
msgid "Add products"
|
||||
msgstr "Añadir productos"
|
||||
|
||||
#: web/template/invoices/new.gohtml:43 web/template/invoices/view.gohtml:59
|
||||
#: web/template/invoices/new.gohtml:44 web/template/invoices/view.gohtml:61
|
||||
#: web/template/invoices/edit.gohtml:45
|
||||
msgctxt "title"
|
||||
msgid "Subtotal"
|
||||
msgstr "Subtotal"
|
||||
|
||||
#: web/template/invoices/new.gohtml:53 web/template/invoices/view.gohtml:63
|
||||
#: web/template/invoices/view.gohtml:103
|
||||
#: web/template/invoices/new.gohtml:54 web/template/invoices/view.gohtml:65
|
||||
#: web/template/invoices/view.gohtml:105 web/template/invoices/edit.gohtml:55
|
||||
msgctxt "title"
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
||||
#: web/template/invoices/new.gohtml:65
|
||||
#: web/template/invoices/new.gohtml:66 web/template/invoices/edit.gohtml:67
|
||||
msgctxt "action"
|
||||
msgid "Update"
|
||||
msgstr "Actualizar"
|
||||
|
||||
#: web/template/invoices/new.gohtml:67 web/template/invoices/index.gohtml:19
|
||||
#: web/template/invoices/new.gohtml:68 web/template/invoices/index.gohtml:19
|
||||
msgctxt "action"
|
||||
msgid "New invoice"
|
||||
msgstr "Nueva factura"
|
||||
|
@ -105,7 +108,7 @@ msgctxt "invoice"
|
|||
msgid "All"
|
||||
msgstr "Todas"
|
||||
|
||||
#: web/template/invoices/index.gohtml:29 web/template/invoices/view.gohtml:26
|
||||
#: web/template/invoices/index.gohtml:29 web/template/invoices/view.gohtml:28
|
||||
msgctxt "title"
|
||||
msgid "Date"
|
||||
msgstr "Fecha"
|
||||
|
@ -150,45 +153,60 @@ msgctxt "action"
|
|||
msgid "Select invoice %v"
|
||||
msgstr "Seleccionar factura %v"
|
||||
|
||||
#: web/template/invoices/index.gohtml:91 web/template/invoices/view.gohtml:14
|
||||
#: web/template/invoices/index.gohtml:92 web/template/invoices/view.gohtml:16
|
||||
msgctxt "action"
|
||||
msgid "Edit"
|
||||
msgstr "Editar"
|
||||
|
||||
#: web/template/invoices/index.gohtml:98 web/template/invoices/view.gohtml:15
|
||||
msgctxt "action"
|
||||
msgid "Duplicate"
|
||||
msgstr "Duplicar"
|
||||
|
||||
#: web/template/invoices/index.gohtml:101
|
||||
#: web/template/invoices/index.gohtml:108
|
||||
msgid "No invoices added yet."
|
||||
msgstr "No hay facturas."
|
||||
|
||||
#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:25
|
||||
#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:27
|
||||
msgctxt "title"
|
||||
msgid "Invoice %s"
|
||||
msgstr "Factura %s"
|
||||
|
||||
#: web/template/invoices/view.gohtml:17
|
||||
#: web/template/invoices/view.gohtml:19
|
||||
msgctxt "action"
|
||||
msgid "Download invoice"
|
||||
msgstr "Descargar factura"
|
||||
|
||||
#: web/template/invoices/view.gohtml:53
|
||||
#: web/template/invoices/view.gohtml:55
|
||||
msgctxt "title"
|
||||
msgid "Concept"
|
||||
msgstr "Concepto"
|
||||
|
||||
#: web/template/invoices/view.gohtml:56
|
||||
#: web/template/invoices/view.gohtml:58
|
||||
msgctxt "title"
|
||||
msgid "Discount"
|
||||
msgstr "Descuento"
|
||||
|
||||
#: web/template/invoices/view.gohtml:58
|
||||
#: web/template/invoices/view.gohtml:60
|
||||
msgctxt "title"
|
||||
msgid "Units"
|
||||
msgstr "Unidades"
|
||||
|
||||
#: web/template/invoices/view.gohtml:93
|
||||
#: web/template/invoices/view.gohtml:95
|
||||
msgctxt "title"
|
||||
msgid "Tax Base"
|
||||
msgstr "Base imponible"
|
||||
|
||||
#: web/template/invoices/edit.gohtml:2 web/template/invoices/edit.gohtml:15
|
||||
msgctxt "title"
|
||||
msgid "Edit Invoice “%s”"
|
||||
msgstr "Edición del la factura «%s»"
|
||||
|
||||
#: web/template/invoices/edit.gohtml:69
|
||||
msgctxt "action"
|
||||
msgid "Edit invoice"
|
||||
msgstr "Editar factura"
|
||||
|
||||
#: web/template/dashboard.gohtml:2
|
||||
msgctxt "title"
|
||||
msgid "Dashboard"
|
||||
|
@ -428,44 +446,43 @@ msgstr "No podéis dejar la contraseña en blanco."
|
|||
msgid "Invalid user or password."
|
||||
msgstr "Nombre de usuario o contraseña inválido."
|
||||
|
||||
#: pkg/products.go:165 pkg/invoices.go:635
|
||||
#: pkg/products.go:165 pkg/invoices.go:643
|
||||
msgctxt "input"
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: pkg/products.go:171 pkg/invoices.go:640
|
||||
#: pkg/products.go:171 pkg/invoices.go:648
|
||||
msgctxt "input"
|
||||
msgid "Description"
|
||||
msgstr "Descripción"
|
||||
|
||||
#: pkg/products.go:176 pkg/invoices.go:644
|
||||
#: pkg/products.go:176 pkg/invoices.go:652
|
||||
msgctxt "input"
|
||||
msgid "Price"
|
||||
msgstr "Precio"
|
||||
|
||||
#: pkg/products.go:186 pkg/invoices.go:670
|
||||
#: pkg/products.go:186 pkg/invoices.go:678
|
||||
msgctxt "input"
|
||||
msgid "Taxes"
|
||||
msgstr "Impuestos"
|
||||
|
||||
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:532
|
||||
#: pkg/invoices.go:706
|
||||
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:717
|
||||
msgid "Name can not be empty."
|
||||
msgstr "No podéis dejar el nombre en blanco."
|
||||
|
||||
#: pkg/products.go:207 pkg/invoices.go:707
|
||||
#: pkg/products.go:207 pkg/invoices.go:718
|
||||
msgid "Price can not be empty."
|
||||
msgstr "No podéis dejar el precio en blanco."
|
||||
|
||||
#: pkg/products.go:208 pkg/invoices.go:708
|
||||
#: pkg/products.go:208 pkg/invoices.go:719
|
||||
msgid "Price must be a number greater than zero."
|
||||
msgstr "El precio tiene que ser un número mayor a cero."
|
||||
|
||||
#: pkg/products.go:210 pkg/invoices.go:716
|
||||
#: pkg/products.go:210 pkg/invoices.go:727
|
||||
msgid "Selected tax is not valid."
|
||||
msgstr "Habéis escogido un impuesto que no es válido."
|
||||
|
||||
#: pkg/products.go:211 pkg/invoices.go:717
|
||||
#: pkg/products.go:211 pkg/invoices.go:728
|
||||
msgid "You can only select a tax of each class."
|
||||
msgstr "Solo podéis escoger un impuesto de cada clase."
|
||||
|
||||
|
@ -573,88 +590,105 @@ msgstr "La confirmación no corresponde con la contraseña."
|
|||
msgid "Selected language is not valid."
|
||||
msgstr "Habéis escogido un idioma que no es válido."
|
||||
|
||||
#: pkg/invoices.go:302
|
||||
#: pkg/invoices.go:303
|
||||
msgid "Select a customer to bill."
|
||||
msgstr "Escoged un cliente a facturar."
|
||||
|
||||
#: pkg/invoices.go:397 pkg/invoices.go:426
|
||||
msgid "Invalid action"
|
||||
msgstr "Acción inválida."
|
||||
|
||||
#: pkg/invoices.go:420
|
||||
#: pkg/invoices.go:402
|
||||
msgid "invoices.zip"
|
||||
msgstr "facturas.zip"
|
||||
|
||||
#: pkg/invoices.go:468
|
||||
#: pkg/invoices.go:408 pkg/invoices.go:836
|
||||
msgid "Invalid action"
|
||||
msgstr "Acción inválida."
|
||||
|
||||
#: pkg/invoices.go:452
|
||||
msgctxt "input"
|
||||
msgid "Invoice Status"
|
||||
msgstr "Estado de la factura"
|
||||
|
||||
#: pkg/invoices.go:458
|
||||
msgctxt "input"
|
||||
msgid "Customer"
|
||||
msgstr "Cliente"
|
||||
|
||||
#: pkg/invoices.go:474
|
||||
#: pkg/invoices.go:464
|
||||
msgctxt "input"
|
||||
msgid "Number"
|
||||
msgstr "Número"
|
||||
|
||||
#: pkg/invoices.go:479
|
||||
#: pkg/invoices.go:469
|
||||
msgctxt "input"
|
||||
msgid "Invoice Date"
|
||||
msgstr "Fecha de factura"
|
||||
|
||||
#: pkg/invoices.go:485
|
||||
#: pkg/invoices.go:475
|
||||
msgctxt "input"
|
||||
msgid "Notes"
|
||||
msgstr "Notas"
|
||||
|
||||
#: pkg/invoices.go:490
|
||||
#: pkg/invoices.go:480
|
||||
msgctxt "input"
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetes"
|
||||
|
||||
#: pkg/invoices.go:496
|
||||
#: pkg/invoices.go:486
|
||||
msgctxt "input"
|
||||
msgid "Payment Method"
|
||||
msgstr "Método de pago"
|
||||
|
||||
#: pkg/invoices.go:533
|
||||
#: pkg/invoices.go:523
|
||||
msgid "Selected invoice status is not valid."
|
||||
msgstr "Habéis escogido un estado de factura que no es válido."
|
||||
|
||||
#: pkg/invoices.go:524
|
||||
msgid "Selected customer is not valid."
|
||||
msgstr "Habéis escogido un cliente que no es válido."
|
||||
|
||||
#: pkg/invoices.go:525
|
||||
msgid "Invoice date can not be empty."
|
||||
msgstr "No podéis dejar la fecha de la factura en blanco."
|
||||
|
||||
#: pkg/invoices.go:534
|
||||
#: pkg/invoices.go:526
|
||||
msgid "Invoice date must be a valid date."
|
||||
msgstr "La fecha de factura debe ser válida."
|
||||
|
||||
#: pkg/invoices.go:536
|
||||
#: pkg/invoices.go:528
|
||||
msgid "Selected payment method is not valid."
|
||||
msgstr "Habéis escogido un método de pago que no es válido."
|
||||
|
||||
#: pkg/invoices.go:630
|
||||
#: pkg/invoices.go:633 pkg/invoices.go:638
|
||||
msgctxt "input"
|
||||
msgid "Id"
|
||||
msgstr "Identificador"
|
||||
|
||||
#: pkg/invoices.go:653
|
||||
#: pkg/invoices.go:661
|
||||
msgctxt "input"
|
||||
msgid "Quantity"
|
||||
msgstr "Cantidad"
|
||||
|
||||
#: pkg/invoices.go:661
|
||||
#: pkg/invoices.go:669
|
||||
msgctxt "input"
|
||||
msgid "Discount (%)"
|
||||
msgstr "Descuento (%)"
|
||||
|
||||
#: pkg/invoices.go:710
|
||||
#: pkg/invoices.go:716
|
||||
msgid "Product ID can not be empty."
|
||||
msgstr "No podéis dejar el identificador de producto en blanco."
|
||||
|
||||
#: pkg/invoices.go:721
|
||||
msgid "Quantity can not be empty."
|
||||
msgstr "No podéis dejar la cantidad en blanco."
|
||||
|
||||
#: pkg/invoices.go:711
|
||||
#: pkg/invoices.go:722
|
||||
msgid "Quantity must be a number greater than zero."
|
||||
msgstr "La cantidad tiene que ser un número mayor a cero."
|
||||
|
||||
#: pkg/invoices.go:713
|
||||
#: pkg/invoices.go:724
|
||||
msgid "Discount can not be empty."
|
||||
msgstr "No podéis dejar el descuento en blanco."
|
||||
|
||||
#: pkg/invoices.go:714
|
||||
#: pkg/invoices.go:725
|
||||
msgid "Discount must be a percentage between 0 and 100."
|
||||
msgstr "El descuento tiene que ser un porcentaje entre 0 y 100."
|
||||
|
||||
|
|
|
@ -93,7 +93,8 @@ values (19, 15, 7, 'P1.0', 1100)
|
|||
;
|
||||
|
||||
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
|
||||
values (20, 4, 0.21)
|
||||
values (19, 4, 0.21)
|
||||
, (20, 4, 0.21)
|
||||
, (21, 3, -0.07)
|
||||
, (21, 4, 0.21)
|
||||
, (22, 3, -0.15)
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
{{ define "title" -}}
|
||||
{{ printf ( pgettext "Edit Invoice “%s”" "title" ) .Number }}
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editInvoicePage*/ -}}
|
||||
<nav>
|
||||
<p>
|
||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||
<a href="{{ companyURI "/invoices"}}">{{( pgettext "Invoices" "title" )}}</a> /
|
||||
<a>{{ .Number }}</a>
|
||||
</p>
|
||||
</nav>
|
||||
<section class="dialog-content">
|
||||
<h2>{{ printf (pgettext "Edit Invoice “%s”" "title") .Number }}</h2>
|
||||
<form method="POST" action="{{ companyURI "/invoices/" }}{{ .Slug }}">
|
||||
{{ csrfToken }}
|
||||
|
||||
{{ with .Form -}}
|
||||
{{ template "select-field" .Customer }}
|
||||
{{ template "hidden-field" .Number }}
|
||||
{{ template "hidden-field" .Date }}
|
||||
{{ template "input-field" .Tags }}
|
||||
{{ template "select-field" .PaymentMethod }}
|
||||
{{ template "select-field" .InvoiceStatus }}
|
||||
{{ template "input-field" .Notes }}
|
||||
|
||||
{{- range $product := .Products }}
|
||||
<fieldset class="new-invoice-product">
|
||||
{{ template "hidden-field" .InvoiceProductId }}
|
||||
{{ template "hidden-field" .ProductId }}
|
||||
{{ template "input-field" .Name }}
|
||||
{{ template "input-field" .Price }}
|
||||
{{ template "input-field" .Quantity }}
|
||||
{{ template "input-field" .Discount }}
|
||||
{{ template "input-field" .Description }}
|
||||
{{ template "select-field" .Tax }}
|
||||
</fieldset>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{(pgettext "Subtotal" "title")}}</th>
|
||||
<td class="numeric">{{ .Subtotal | formatPrice }}</td>
|
||||
</tr>
|
||||
{{- range $tax := .Taxes }}
|
||||
<tr>
|
||||
<th scope="row">{{ index . 0 }}</th>
|
||||
<td class="numeric">{{ index . 1 | formatPrice }}</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
<tr>
|
||||
<th scope="row">{{(pgettext "Total" "title")}}</th>
|
||||
<td class="numeric">{{ .Total | formatPrice }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<fieldset>
|
||||
<button formnovalidate formaction="{{ companyURI "/invoices" }}/{{ .Slug }}/edit"
|
||||
name="action" value="select-products"
|
||||
type="submit">{{( pgettext "Add products" "action" )}}</button>
|
||||
<button formnovalidate formaction="{{ companyURI "/invoices" }}/{{ .Slug }}/edit"
|
||||
name="action" value="update"
|
||||
type="submit">{{( pgettext "Update" "action" )}}</button>
|
||||
<button class="primary" name="_method" value="PUT"
|
||||
type="submit">{{( pgettext "Edit invoice" "action" )}}</button>
|
||||
</fieldset>
|
||||
|
||||
</form>
|
||||
</section>
|
||||
{{- end }}
|
|
@ -54,12 +54,13 @@
|
|||
<form action="{{companyURI "/invoices/"}}{{ .Slug }}" method="POST">
|
||||
{{ csrfToken }}
|
||||
{{ putMethod }}
|
||||
<input type="hidden" name="quick" value="status">
|
||||
<ul role="menu">
|
||||
{{- range $status, $name := $.InvoiceStatuses }}
|
||||
{{- if ne $status $invoice.Status }}
|
||||
<li role="presentation">
|
||||
<button role="menuitem" type="submit"
|
||||
name="status" value="{{ $status }}"
|
||||
name="invoice_status" value="{{ $status }}"
|
||||
class="invoice-status-{{ $status }}"
|
||||
>{{ $name }}</button>
|
||||
</li>
|
||||
|
@ -85,6 +86,12 @@
|
|||
<details class="menu">
|
||||
<summary><i class="ri-more-line"></i></summary>
|
||||
<ul role="menu" class="action-menu">
|
||||
<li role="presentation">
|
||||
<a role="menuitem" href="{{ companyURI "/invoices"}}/{{ .Slug }}/edit">
|
||||
<i class="ri-edit-line"></i>
|
||||
{{( pgettext "Edit" "action" )}}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a role="menuitem" href="{{ companyURI "/invoices/new"}}?duplicate={{ .Slug }}">
|
||||
<i class="ri-file-copy-line"></i>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{{- end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceForm*/ -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newInvoicePage*/ -}}
|
||||
<nav>
|
||||
<p>
|
||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||
|
@ -17,6 +17,7 @@
|
|||
{{ csrfToken }}
|
||||
|
||||
{{ with .Form -}}
|
||||
{{ template "hidden-select-field" .InvoiceStatus }}
|
||||
{{ template "select-field" .Customer }}
|
||||
{{ template "input-field" .Number }}
|
||||
{{ template "input-field" .Date }}
|
||||
|
|
|
@ -8,12 +8,16 @@
|
|||
<p>
|
||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||
<a href="{{ companyURI "/invoices"}}">{{( pgettext "Invoices" "title" )}}</a> /
|
||||
<a>{{( pgettext "New Invoice" "title" )}}</a>
|
||||
{{ if eq .Form.Number.Val "" }}
|
||||
<a>{{( pgettext "New Invoice" "title" )}}</a>
|
||||
{{ else }}
|
||||
<a>{{ .Form.Number }}</a>
|
||||
{{ end }}
|
||||
</p>
|
||||
</nav>
|
||||
<section class="dialog-content">
|
||||
<h2>{{(pgettext "Add Products to Invoice" "title")}}</h2>
|
||||
<form method="POST" action="{{ companyURI "/invoices/new" }}">
|
||||
<form method="POST" action="{{ .Action }}">
|
||||
{{ csrfToken }}
|
||||
|
||||
{{- with .Form }}
|
||||
|
@ -25,6 +29,7 @@
|
|||
|
||||
{{- range $product := .Products }}
|
||||
<fieldset>
|
||||
{{ template "hidden-field" .InvoiceProductId }}
|
||||
{{ template "hidden-field" .ProductId }}
|
||||
{{ template "hidden-field" .Name }}
|
||||
{{ template "hidden-field" .Description }}
|
||||
|
|
|
@ -11,7 +11,9 @@
|
|||
<a>{{ .Number }}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="primary button" href="{{ companyURI "/invoices/new"}}?duplicate={{ .Slug }}">{{( pgettext "Duplicate" "action" )}}</a>
|
||||
<a class="button primary"
|
||||
href="{{ companyURI "/invoices/new"}}?duplicate={{ .Slug }}">{{( pgettext "Duplicate" "action" )}}</a>
|
||||
<a class="button primary" href="{{ companyURI "/invoices/"}}{{ .Slug }}/edit">{{( pgettext "Edit" "action" )}}</a>
|
||||
<a class="primary button"
|
||||
href="{{ companyURI "/invoices/" }}{{ .Slug }}.pdf"
|
||||
download="{{ .Number}}.pdf">{{( pgettext "Download invoice" "action" )}}</a>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{{ define "title" -}}
|
||||
{{printf (pgettext "Edit Product “%s”" "title") .Name.Val }}
|
||||
{{printf (pgettext "Edit Product “%s”" "title") .Name }}
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" }}
|
||||
|
@ -8,11 +8,11 @@
|
|||
<p>
|
||||
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
|
||||
<a href="{{ companyURI "/products"}}">{{( pgettext "Products" "title" )}}</a> /
|
||||
<a>{{ .Name.Val }}</a>
|
||||
<a>{{ .Name }}</a>
|
||||
</p>
|
||||
</nav>
|
||||
<section class="dialog-content">
|
||||
<h2>{{printf (pgettext "Edit Product “%s”" "title") .Name.Val }}</h2>
|
||||
<h2>{{printf (pgettext "Edit Product “%s”" "title") .Name }}</h2>
|
||||
<form method="POST">
|
||||
{{ csrfToken }}
|
||||
{{ putMethod }}
|
||||
|
|
Loading…
Reference in New Issue