diff --git a/deploy/attach_to_invoice.sql b/deploy/attach_to_invoice.sql new file mode 100644 index 0000000..7c6108b --- /dev/null +++ b/deploy/attach_to_invoice.sql @@ -0,0 +1,30 @@ +-- Deploy numerus:attach_to_invoice to pg +-- requires: schema_numerus +-- requires: roles +-- requires: invoice +-- requires: invoice_attachment + +begin; + +set search_path to numerus, public; + +create or replace function attach_to_invoice(invoice_slug uuid, original_filename text, mime_type text, content bytea) returns void as +$$ + insert into invoice_attachment (invoice_id, original_filename, mime_type, content) + select invoice_id, original_filename, mime_type, content + from invoice + where slug = invoice_slug + on conflict (invoice_id) do update + set original_filename = excluded.original_filename + , mime_type = excluded.mime_type + , content = excluded.content + ; +$$ + language sql +; + +revoke execute on function attach_to_invoice(uuid, text, text, bytea) from public; +grant execute on function attach_to_invoice(uuid, text, text, bytea) to invoicer; +grant execute on function attach_to_invoice(uuid, text, text, bytea) to admin; + +commit; diff --git a/deploy/invoice_attachment.sql b/deploy/invoice_attachment.sql new file mode 100644 index 0000000..fdcf9d4 --- /dev/null +++ b/deploy/invoice_attachment.sql @@ -0,0 +1,32 @@ +-- Deploy numerus:invoice_attachment to pg +-- requires: schema_numerus +-- requires: roles +-- requires: invoice + +begin; + +set search_path to numerus, public; + +create table invoice_attachment ( + invoice_id integer primary key references invoice, + original_filename text not null, + mime_type text not null, + content bytea not null +); + +grant select, insert, update, delete on table invoice_attachment to invoicer; +grant select, insert, update, delete on table invoice_attachment to admin; + +alter table invoice_attachment enable row level security; + +create policy company_policy +on invoice_attachment +using ( + exists( + select 1 + from invoice + where invoice.invoice_id = invoice_attachment.invoice_id + ) +); + +commit; diff --git a/pkg/invoices.go b/pkg/invoices.go index 1f3679d..4aaac07 100644 --- a/pkg/invoices.go +++ b/pkg/invoices.go @@ -349,6 +349,7 @@ type invoice struct { HasDiscounts bool Total string LegalDisclaimer string + OriginalFileName string } type taxDetails struct { @@ -394,11 +395,13 @@ func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug stri , postal_code , to_price(subtotal, decimal_digits) , to_price(total, decimal_digits) + , coalesce(attachment.original_filename, '') from invoice join payment_method using (payment_method_id) join contact_tax_details using (contact_id) join invoice_amount using (invoice_id) join currency using (currency_code) + left join invoice_attachment as attachment using (invoice_id) where invoice.slug = $1`, slug).Scan( &invoiceId, &decimalDigits, @@ -413,7 +416,8 @@ func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug stri &inv.Invoicee.Province, &inv.Invoicee.PostalCode, &inv.Subtotal, - &inv.Total)) { + &inv.Total, + &inv.OriginalFileName)) { return nil } if err := conn.QueryRow(ctx, ` @@ -605,6 +609,9 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param return } slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7)", company.Id, form.Date, form.Customer, form.Notes, form.PaymentMethod, form.Tags, NewInvoiceProductArray(form.Products)) + if len(form.File.Content) > 0 { + conn.MustQuery(r.Context(), "select attach_to_invoice($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content) + } htmxRedirect(w, r, companyURI(company, "/invoices/"+slug)) } @@ -680,6 +687,7 @@ type invoiceForm struct { Tags *TagsField Products []*invoiceProductForm RemovedProduct *invoiceProductForm + File *FileField } func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *invoiceForm { @@ -721,6 +729,11 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co Selected: []string{mustGetDefaultPaymentMethod(ctx, conn, company)}, Options: mustGetPaymentMethodOptions(ctx, conn, company), }, + File: &FileField{ + Name: "file", + Label: pgettext("input", "File", locale), + MaxSize: 1 << 20, + }, } } @@ -735,7 +748,7 @@ func mustGetInvoiceStatusOptions(ctx context.Context, conn *Conn, locale *Locale } func (form *invoiceForm) Parse(r *http.Request) error { - if err := r.ParseForm(); err != nil { + if err := r.ParseMultipartForm(form.File.MaxSize); err != nil { return err } form.InvoiceStatus.FillValue(r) @@ -744,6 +757,9 @@ func (form *invoiceForm) Parse(r *http.Request) error { form.Notes.FillValue(r) form.Tags.FillValue(r) form.PaymentMethod.FillValue(r) + if err := form.File.FillValue(r); err != nil { + return err + } if _, ok := r.Form["product.id.0"]; ok { taxOptions := mustGetTaxOptions(r.Context(), getConn(r), form.company) for index := 0; true; index++ { @@ -1146,6 +1162,9 @@ func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprout http.NotFound(w, r) return } + if len(form.File.Content) > 0 { + conn.MustQuery(r.Context(), "select attach_to_invoice($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content) + } htmxRedirect(w, r, companyURI(company, "/invoices/"+slug)) } } @@ -1318,3 +1337,24 @@ func HandleUpdateInvoiceTags(w http.ResponseWriter, r *http.Request, params http } mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form) } + +func ServeInvoiceAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + slug := params[0].Value + conn := getConn(r) + var contentType string + var content []byte + if notFoundErrorOrPanic(conn.QueryRow(r.Context(), ` + select mime_type + , content + from invoice + join invoice_attachment using (invoice_id) + where slug = $1 +`, slug).Scan(&contentType, &content)) { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Length", strconv.FormatInt(int64(len(content)), 10)) + w.WriteHeader(http.StatusOK) + w.Write(content) +} diff --git a/pkg/router.go b/pkg/router.go index 2241e41..75e9f75 100644 --- a/pkg/router.go +++ b/pkg/router.go @@ -38,6 +38,7 @@ func NewRouter(db *Db) http.Handler { companyRouter.POST("/invoices/:slug/edit", HandleEditInvoiceAction) companyRouter.PUT("/invoices/:slug/tags", HandleUpdateInvoiceTags) companyRouter.GET("/invoices/:slug/tags/edit", ServeEditInvoiceTags) + companyRouter.GET("/invoices/:slug/download/:filename", ServeInvoiceAttachment) companyRouter.GET("/quotes", IndexQuotes) companyRouter.POST("/quotes", HandleAddQuote) companyRouter.GET("/quotes/:slug", ServeQuote) diff --git a/po/ca.po b/po/ca.po index ff738d4..edbe772 100644 --- a/po/ca.po +++ b/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-07-06 11:45+0200\n" +"POT-Creation-Date: 2023-07-12 20:01+0200\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -65,7 +65,7 @@ msgid "Name" msgstr "Nom" #: web/template/invoices/products.gohtml:50 -#: web/template/invoices/view.gohtml:62 web/template/quotes/products.gohtml:50 +#: web/template/invoices/view.gohtml:66 web/template/quotes/products.gohtml:50 #: web/template/quotes/view.gohtml:73 web/template/products/index.gohtml:42 msgctxt "title" msgid "Price" @@ -76,8 +76,8 @@ msgstr "Preu" msgid "No products added yet." msgstr "No hi ha cap producte." -#: web/template/invoices/products.gohtml:72 web/template/invoices/new.gohtml:87 -#: web/template/invoices/edit.gohtml:88 web/template/quotes/products.gohtml:72 +#: web/template/invoices/products.gohtml:72 web/template/invoices/new.gohtml:88 +#: web/template/invoices/edit.gohtml:89 web/template/quotes/products.gohtml:72 #: web/template/quotes/new.gohtml:88 web/template/quotes/edit.gohtml:89 msgctxt "action" msgid "Add products" @@ -94,31 +94,31 @@ msgctxt "action" msgid "Undo" msgstr "Desfes" -#: web/template/invoices/new.gohtml:63 web/template/invoices/view.gohtml:67 -#: web/template/invoices/edit.gohtml:64 web/template/quotes/new.gohtml:64 +#: web/template/invoices/new.gohtml:64 web/template/invoices/view.gohtml:71 +#: web/template/invoices/edit.gohtml:65 web/template/quotes/new.gohtml:64 #: web/template/quotes/view.gohtml:78 web/template/quotes/edit.gohtml:65 msgctxt "title" msgid "Subtotal" msgstr "Subtotal" -#: web/template/invoices/new.gohtml:73 web/template/invoices/view.gohtml:71 -#: web/template/invoices/view.gohtml:111 web/template/invoices/edit.gohtml:74 +#: web/template/invoices/new.gohtml:74 web/template/invoices/view.gohtml:75 +#: web/template/invoices/view.gohtml:115 web/template/invoices/edit.gohtml:75 #: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82 #: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75 msgctxt "title" msgid "Total" msgstr "Total" -#: web/template/invoices/new.gohtml:91 web/template/invoices/edit.gohtml:92 +#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 msgctxt "action" msgid "Update" msgstr "Actualitza" -#: web/template/invoices/new.gohtml:94 web/template/invoices/edit.gohtml:95 +#: web/template/invoices/new.gohtml:95 web/template/invoices/edit.gohtml:96 #: web/template/quotes/new.gohtml:95 web/template/quotes/edit.gohtml:96 #: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 -#: web/template/expenses/new.gohtml:33 web/template/expenses/edit.gohtml:38 +#: web/template/expenses/new.gohtml:34 web/template/expenses/edit.gohtml:40 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 msgctxt "action" msgid "Save" @@ -136,7 +136,7 @@ msgstr "Nova factura" #: web/template/invoices/index.gohtml:43 web/template/dashboard.gohtml:23 #: web/template/quotes/index.gohtml:43 web/template/contacts/index.gohtml:36 -#: web/template/expenses/index.gohtml:36 web/template/products/index.gohtml:34 +#: web/template/expenses/index.gohtml:37 web/template/products/index.gohtml:34 msgctxt "action" msgid "Filter" msgstr "Filtra" @@ -146,7 +146,7 @@ msgctxt "invoice" msgid "All" msgstr "Totes" -#: web/template/invoices/index.gohtml:50 web/template/invoices/view.gohtml:34 +#: web/template/invoices/index.gohtml:50 web/template/invoices/view.gohtml:38 #: web/template/quotes/index.gohtml:50 web/template/quotes/view.gohtml:37 msgctxt "title" msgid "Date" @@ -163,31 +163,32 @@ msgid "Customer" msgstr "Client" #: web/template/invoices/index.gohtml:53 web/template/quotes/index.gohtml:53 +#: web/template/expenses/index.gohtml:46 msgctxt "title" msgid "Status" msgstr "Estat" #: web/template/invoices/index.gohtml:54 web/template/quotes/index.gohtml:54 -#: web/template/contacts/index.gohtml:45 web/template/expenses/index.gohtml:45 +#: web/template/contacts/index.gohtml:45 web/template/expenses/index.gohtml:47 #: web/template/products/index.gohtml:41 msgctxt "title" msgid "Tags" msgstr "Etiquetes" #: web/template/invoices/index.gohtml:55 web/template/quotes/index.gohtml:55 -#: web/template/expenses/index.gohtml:46 +#: web/template/expenses/index.gohtml:48 msgctxt "title" msgid "Amount" msgstr "Import" #: web/template/invoices/index.gohtml:56 web/template/quotes/index.gohtml:56 -#: web/template/expenses/index.gohtml:47 +#: web/template/expenses/index.gohtml:49 msgctxt "title" msgid "Download" msgstr "Descàrrega" #: web/template/invoices/index.gohtml:57 web/template/quotes/index.gohtml:57 -#: web/template/contacts/index.gohtml:46 web/template/expenses/index.gohtml:48 +#: web/template/contacts/index.gohtml:46 web/template/expenses/index.gohtml:50 #: web/template/products/index.gohtml:43 msgctxt "title" msgid "Actions" @@ -209,7 +210,7 @@ msgstr "Accions per la factura %s" #: web/template/invoices/index.gohtml:121 web/template/invoices/view.gohtml:19 #: web/template/quotes/index.gohtml:120 web/template/quotes/view.gohtml:22 -#: web/template/contacts/index.gohtml:77 web/template/expenses/index.gohtml:87 +#: web/template/contacts/index.gohtml:77 web/template/expenses/index.gohtml:111 #: web/template/products/index.gohtml:73 msgctxt "action" msgid "Edit" @@ -226,11 +227,11 @@ msgid "No invoices added yet." msgstr "No hi ha cap factura." #: web/template/invoices/index.gohtml:146 web/template/quotes/index.gohtml:153 -#: web/template/expenses/index.gohtml:104 +#: web/template/expenses/index.gohtml:128 msgid "Total" msgstr "Total" -#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:33 +#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:37 msgctxt "title" msgid "Invoice %s" msgstr "Factura %s" @@ -240,22 +241,27 @@ msgctxt "action" msgid "Download invoice" msgstr "Descarrega factura" -#: web/template/invoices/view.gohtml:61 web/template/quotes/view.gohtml:72 +#: web/template/invoices/view.gohtml:25 +msgctxt "action" +msgid "Download invoice attachment" +msgstr "Descarrega l’adjunt de la factura" + +#: web/template/invoices/view.gohtml:65 web/template/quotes/view.gohtml:72 msgctxt "title" msgid "Concept" msgstr "Concepte" -#: web/template/invoices/view.gohtml:64 web/template/quotes/view.gohtml:75 +#: web/template/invoices/view.gohtml:68 web/template/quotes/view.gohtml:75 msgctxt "title" msgid "Discount" msgstr "Descompte" -#: web/template/invoices/view.gohtml:66 web/template/quotes/view.gohtml:77 +#: web/template/invoices/view.gohtml:70 web/template/quotes/view.gohtml:77 msgctxt "title" msgid "Units" msgstr "Unitats" -#: web/template/invoices/view.gohtml:101 web/template/quotes/view.gohtml:112 +#: web/template/invoices/view.gohtml:105 web/template/quotes/view.gohtml:112 msgctxt "title" msgid "Tax Base" msgstr "Base imposable" @@ -473,7 +479,7 @@ msgctxt "action" msgid "New contact" msgstr "Nou contacte" -#: web/template/contacts/index.gohtml:42 web/template/expenses/index.gohtml:42 +#: web/template/contacts/index.gohtml:42 web/template/expenses/index.gohtml:43 msgctxt "title" msgid "Contact" msgstr "Contacte" @@ -559,21 +565,21 @@ msgctxt "action" msgid "New expense" msgstr "Nova despesa" -#: web/template/expenses/index.gohtml:43 +#: web/template/expenses/index.gohtml:44 msgctxt "title" msgid "Invoice Date" msgstr "Data de factura" -#: web/template/expenses/index.gohtml:44 +#: web/template/expenses/index.gohtml:45 msgctxt "title" msgid "Invoice Number" msgstr "Número de factura" -#: web/template/expenses/index.gohtml:79 +#: web/template/expenses/index.gohtml:103 msgid "Actions for expense %s" msgstr "Accions per la despesa %s" -#: web/template/expenses/index.gohtml:97 +#: web/template/expenses/index.gohtml:121 msgid "No expenses added yet." msgstr "No hi ha cap despesa." @@ -706,83 +712,84 @@ msgstr "No podeu deixar la contrasenya en blanc." msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." -#: pkg/products.go:164 pkg/products.go:263 pkg/quote.go:823 pkg/invoices.go:909 +#: pkg/products.go:164 pkg/products.go:263 pkg/quote.go:878 pkg/invoices.go:993 #: pkg/contacts.go:140 pkg/contacts.go:248 msgctxt "input" msgid "Name" msgstr "Nom" -#: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:630 -#: pkg/expenses.go:188 pkg/expenses.go:347 pkg/invoices.go:174 -#: pkg/invoices.go:657 pkg/invoices.go:1208 pkg/contacts.go:145 +#: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:685 +#: pkg/expenses.go:228 pkg/expenses.go:417 pkg/invoices.go:174 +#: pkg/invoices.go:723 pkg/invoices.go:1295 pkg/contacts.go:145 #: pkg/contacts.go:348 msgctxt "input" msgid "Tags" msgstr "Etiquetes" -#: pkg/products.go:173 pkg/quote.go:178 pkg/expenses.go:351 pkg/invoices.go:178 +#: pkg/products.go:173 pkg/quote.go:178 pkg/expenses.go:427 pkg/invoices.go:178 #: pkg/contacts.go:149 msgctxt "input" msgid "Tags Condition" msgstr "Condició de les etiquetes" -#: pkg/products.go:177 pkg/quote.go:182 pkg/expenses.go:355 pkg/invoices.go:182 +#: pkg/products.go:177 pkg/quote.go:182 pkg/expenses.go:431 pkg/invoices.go:182 #: pkg/contacts.go:153 msgctxt "tag condition" msgid "All" msgstr "Totes" -#: pkg/products.go:178 pkg/expenses.go:356 pkg/invoices.go:183 +#: pkg/products.go:178 pkg/expenses.go:432 pkg/invoices.go:183 #: pkg/contacts.go:154 msgid "Invoices must have all the specified labels." msgstr "Les factures han de tenir totes les etiquetes." -#: pkg/products.go:182 pkg/quote.go:187 pkg/expenses.go:360 pkg/invoices.go:187 +#: pkg/products.go:182 pkg/quote.go:187 pkg/expenses.go:436 pkg/invoices.go:187 #: pkg/contacts.go:158 msgctxt "tag condition" msgid "Any" msgstr "Qualsevol" -#: pkg/products.go:183 pkg/expenses.go:361 pkg/invoices.go:188 +#: pkg/products.go:183 pkg/expenses.go:437 pkg/invoices.go:188 #: pkg/contacts.go:159 msgid "Invoices must have at least one of the specified labels." msgstr "Les factures han de tenir com a mínim una de les etiquetes." -#: pkg/products.go:269 pkg/quote.go:837 pkg/invoices.go:923 +#: pkg/products.go:269 pkg/quote.go:892 pkg/invoices.go:1007 msgctxt "input" msgid "Description" msgstr "Descripció" -#: pkg/products.go:274 pkg/quote.go:841 pkg/invoices.go:927 +#: pkg/products.go:274 pkg/quote.go:896 pkg/invoices.go:1011 msgctxt "input" msgid "Price" msgstr "Preu" -#: pkg/products.go:284 pkg/quote.go:870 pkg/expenses.go:167 pkg/invoices.go:956 +#: pkg/products.go:284 pkg/quote.go:925 pkg/expenses.go:200 +#: pkg/invoices.go:1040 msgctxt "input" msgid "Taxes" msgstr "Imposts" -#: pkg/products.go:309 pkg/quote.go:919 pkg/profile.go:92 pkg/invoices.go:1005 +#: pkg/products.go:309 pkg/quote.go:974 pkg/profile.go:92 pkg/invoices.go:1089 #: pkg/contacts.go:398 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." -#: pkg/products.go:310 pkg/quote.go:920 pkg/invoices.go:1006 +#: pkg/products.go:310 pkg/quote.go:975 pkg/invoices.go:1090 msgid "Price can not be empty." msgstr "No podeu deixar el preu en blanc." -#: pkg/products.go:311 pkg/quote.go:921 pkg/invoices.go:1007 +#: pkg/products.go:311 pkg/quote.go:976 pkg/invoices.go:1091 msgid "Price must be a number greater than zero." msgstr "El preu ha de ser un número major a zero." -#: pkg/products.go:313 pkg/quote.go:929 pkg/expenses.go:213 pkg/expenses.go:218 -#: pkg/invoices.go:1015 +#: pkg/products.go:313 pkg/quote.go:984 pkg/expenses.go:264 pkg/expenses.go:269 +#: pkg/invoices.go:1099 msgid "Selected tax is not valid." msgstr "Heu seleccionat un impost que no és vàlid." -#: pkg/products.go:314 pkg/quote.go:930 pkg/expenses.go:214 pkg/expenses.go:219 -#: pkg/invoices.go:1016 +#: pkg/products.go:314 pkg/quote.go:985 pkg/expenses.go:265 pkg/expenses.go:270 +#: pkg/invoices.go:1100 msgid "You can only select a tax of each class." msgstr "Només podeu seleccionar un impost de cada classe." @@ -991,7 +998,7 @@ msgstr "No podeu deixar el nom del mètode de pagament en blanc." msgid "Payment instructions can not be empty." msgstr "No podeu deixar les instruccions de pagament en blanc." -#: pkg/quote.go:147 pkg/quote.go:608 pkg/invoices.go:147 pkg/invoices.go:640 +#: pkg/quote.go:147 pkg/quote.go:663 pkg/invoices.go:147 pkg/invoices.go:706 msgctxt "input" msgid "Customer" msgstr "Client" @@ -1000,12 +1007,12 @@ msgstr "Client" msgid "All customers" msgstr "Tots els clients" -#: pkg/quote.go:153 pkg/quote.go:602 +#: pkg/quote.go:153 pkg/quote.go:657 msgctxt "input" msgid "Quotation Status" msgstr "Estat del pressupost" -#: pkg/quote.go:154 pkg/invoices.go:154 +#: pkg/quote.go:154 pkg/expenses.go:422 pkg/invoices.go:154 msgid "All status" msgstr "Tots els estats" @@ -1014,12 +1021,12 @@ msgctxt "input" msgid "Quotation Number" msgstr "Número de pressupost" -#: pkg/quote.go:164 pkg/expenses.go:337 pkg/invoices.go:164 +#: pkg/quote.go:164 pkg/expenses.go:407 pkg/invoices.go:164 msgctxt "input" msgid "From Date" msgstr "A partir de la data" -#: pkg/quote.go:169 pkg/expenses.go:342 pkg/invoices.go:169 +#: pkg/quote.go:169 pkg/expenses.go:412 pkg/invoices.go:169 msgctxt "input" msgid "To Date" msgstr "Fins la data" @@ -1032,99 +1039,99 @@ msgstr "Els pressuposts han de tenir totes les etiquetes." msgid "Quotations must have at least one of the specified labels." msgstr "Els pressuposts han de tenir com a mínim una de les etiquetes." -#: pkg/quote.go:550 +#: pkg/quote.go:605 msgid "quotations.zip" msgstr "pressuposts.zip" -#: pkg/quote.go:556 pkg/quote.go:1085 pkg/quote.go:1093 pkg/invoices.go:589 -#: pkg/invoices.go:1183 pkg/invoices.go:1191 +#: pkg/quote.go:611 pkg/quote.go:1140 pkg/quote.go:1148 pkg/invoices.go:654 +#: pkg/invoices.go:1270 pkg/invoices.go:1278 msgid "Invalid action" msgstr "Acció invàlida." -#: pkg/quote.go:609 +#: pkg/quote.go:664 msgid "Select a customer to quote." msgstr "Escolliu un client a pressupostar." -#: pkg/quote.go:614 +#: pkg/quote.go:669 msgctxt "input" msgid "Quotation Date" msgstr "Data del pressupost" -#: pkg/quote.go:620 +#: pkg/quote.go:675 msgctxt "input" msgid "Terms and conditions" msgstr "Condicions d’acceptació" -#: pkg/quote.go:625 pkg/invoices.go:652 +#: pkg/quote.go:680 pkg/invoices.go:718 msgctxt "input" msgid "Notes" msgstr "Notes" -#: pkg/quote.go:634 pkg/invoices.go:662 +#: pkg/quote.go:689 pkg/invoices.go:728 msgctxt "input" msgid "Payment Method" msgstr "Mètode de pagament" -#: pkg/quote.go:635 +#: pkg/quote.go:690 msgid "Select a payment method." msgstr "Escolliu un mètode de pagament." -#: pkg/quote.go:671 +#: pkg/quote.go:726 msgid "Selected quotation status is not valid." msgstr "Heu seleccionat un estat de pressupost que no és vàlid." -#: pkg/quote.go:673 pkg/invoices.go:699 +#: pkg/quote.go:728 pkg/invoices.go:783 msgid "Selected customer is not valid." msgstr "Heu seleccionat un client que no és vàlid." -#: pkg/quote.go:675 +#: pkg/quote.go:730 msgid "Quotation date can not be empty." msgstr "No podeu deixar la data del pressupost en blanc." -#: pkg/quote.go:676 +#: pkg/quote.go:731 msgid "Quotation date must be a valid date." msgstr "La data del pressupost ha de ser vàlida." -#: pkg/quote.go:679 pkg/invoices.go:703 +#: pkg/quote.go:734 pkg/invoices.go:787 msgid "Selected payment method is not valid." msgstr "Heu seleccionat un mètode de pagament que no és vàlid." -#: pkg/quote.go:813 pkg/quote.go:818 pkg/invoices.go:899 pkg/invoices.go:904 +#: pkg/quote.go:868 pkg/quote.go:873 pkg/invoices.go:983 pkg/invoices.go:988 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/quote.go:851 pkg/invoices.go:937 +#: pkg/quote.go:906 pkg/invoices.go:1021 msgctxt "input" msgid "Quantity" msgstr "Quantitat" -#: pkg/quote.go:860 pkg/invoices.go:946 +#: pkg/quote.go:915 pkg/invoices.go:1030 msgctxt "input" msgid "Discount (%)" msgstr "Descompte (%)" -#: pkg/quote.go:914 +#: pkg/quote.go:969 msgid "Quotation product ID must be a number greater than zero." msgstr "L’ID del producte de pressupost ha de ser un número major a zero." -#: pkg/quote.go:917 pkg/invoices.go:1003 +#: pkg/quote.go:972 pkg/invoices.go:1087 msgid "Product ID must be a positive number or zero." msgstr "L’ID del producte ha de ser un número positiu o zero." -#: pkg/quote.go:923 pkg/invoices.go:1009 +#: pkg/quote.go:978 pkg/invoices.go:1093 msgid "Quantity can not be empty." msgstr "No podeu deixar la quantitat en blanc." -#: pkg/quote.go:924 pkg/invoices.go:1010 +#: pkg/quote.go:979 pkg/invoices.go:1094 msgid "Quantity must be a number greater than zero." msgstr "La quantitat ha de ser un número major a zero." -#: pkg/quote.go:926 pkg/invoices.go:1012 +#: pkg/quote.go:981 pkg/invoices.go:1096 msgid "Discount can not be empty." msgstr "No podeu deixar el descompte en blanc." -#: pkg/quote.go:927 pkg/invoices.go:1013 +#: pkg/quote.go:982 pkg/invoices.go:1097 msgid "Discount must be a percentage between 0 and 100." msgstr "El descompte ha de ser un percentatge entre 0 i 100." @@ -1191,92 +1198,101 @@ msgctxt "period option" msgid "Previous year" msgstr "Any anterior" -#: pkg/expenses.go:115 +#: pkg/expenses.go:147 msgid "Select a contact." msgstr "Escolliu un contacte." -#: pkg/expenses.go:150 pkg/expenses.go:326 +#: pkg/expenses.go:183 pkg/expenses.go:396 msgctxt "input" msgid "Contact" msgstr "Contacte" -#: pkg/expenses.go:156 +#: pkg/expenses.go:189 msgctxt "input" msgid "Invoice number" msgstr "Número de factura" -#: pkg/expenses.go:161 pkg/invoices.go:646 +#: pkg/expenses.go:194 pkg/invoices.go:712 msgctxt "input" msgid "Invoice Date" msgstr "Data de factura" -#: pkg/expenses.go:173 +#: pkg/expenses.go:206 msgctxt "input" msgid "Amount" msgstr "Import" -#: pkg/expenses.go:183 +#: pkg/expenses.go:216 pkg/invoices.go:734 msgctxt "input" msgid "File" msgstr "Fitxer" -#: pkg/expenses.go:211 +#: pkg/expenses.go:222 pkg/expenses.go:421 +msgctxt "input" +msgid "Expense Status" +msgstr "Estat de la despesa" + +#: pkg/expenses.go:262 msgid "Selected contact is not valid." msgstr "Heu seleccionat un contacte que no és vàlid." -#: pkg/expenses.go:212 pkg/invoices.go:701 +#: pkg/expenses.go:263 pkg/invoices.go:785 msgid "Invoice date must be a valid date." msgstr "La data de facturació ha de ser vàlida." -#: pkg/expenses.go:215 +#: pkg/expenses.go:266 msgid "Amount can not be empty." msgstr "No podeu deixar l’import en blanc." -#: pkg/expenses.go:216 +#: pkg/expenses.go:267 msgid "Amount must be a number greater than zero." msgstr "L’import ha de ser un número major a zero." -#: pkg/expenses.go:327 +#: pkg/expenses.go:271 +msgid "Selected expense status is not valid." +msgstr "Heu seleccionat un estat de despesa que no és vàlid." + +#: pkg/expenses.go:397 msgid "All contacts" msgstr "Tots els contactes" -#: pkg/expenses.go:332 pkg/invoices.go:159 +#: pkg/expenses.go:402 pkg/invoices.go:159 msgctxt "input" msgid "Invoice Number" msgstr "Número de factura" -#: pkg/invoices.go:153 pkg/invoices.go:634 +#: pkg/invoices.go:153 pkg/invoices.go:700 msgctxt "input" msgid "Invoice Status" msgstr "Estat de la factura" -#: pkg/invoices.go:482 +#: pkg/invoices.go:544 msgid "Select a customer to bill." msgstr "Escolliu un client a facturar." -#: pkg/invoices.go:583 +#: pkg/invoices.go:648 msgid "invoices.zip" msgstr "factures.zip" -#: pkg/invoices.go:698 +#: pkg/invoices.go:782 msgid "Selected invoice status is not valid." msgstr "Heu seleccionat un estat de factura que no és vàlid." -#: pkg/invoices.go:700 +#: pkg/invoices.go:784 msgid "Invoice date can not be empty." msgstr "No podeu deixar la data de la factura en blanc." -#: pkg/invoices.go:836 +#: pkg/invoices.go:920 #, c-format msgid "Re: quotation #%s of %s" msgstr "Ref: pressupost núm. %s del %s" -#: pkg/invoices.go:837 +#: pkg/invoices.go:921 msgctxt "to_char" msgid "MM/DD/YYYY" msgstr "DD/MM/YYYY" -#: pkg/invoices.go:1000 +#: pkg/invoices.go:1084 msgid "Invoice product ID must be a number greater than zero." msgstr "L’ID del producte de factura ha de ser un número major a zero." diff --git a/po/es.po b/po/es.po index c659137..6759b72 100644 --- a/po/es.po +++ b/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-07-06 11:45+0200\n" +"POT-Creation-Date: 2023-07-12 20:01+0200\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -65,7 +65,7 @@ msgid "Name" msgstr "Nombre" #: web/template/invoices/products.gohtml:50 -#: web/template/invoices/view.gohtml:62 web/template/quotes/products.gohtml:50 +#: web/template/invoices/view.gohtml:66 web/template/quotes/products.gohtml:50 #: web/template/quotes/view.gohtml:73 web/template/products/index.gohtml:42 msgctxt "title" msgid "Price" @@ -76,8 +76,8 @@ msgstr "Precio" msgid "No products added yet." msgstr "No hay productos." -#: web/template/invoices/products.gohtml:72 web/template/invoices/new.gohtml:87 -#: web/template/invoices/edit.gohtml:88 web/template/quotes/products.gohtml:72 +#: web/template/invoices/products.gohtml:72 web/template/invoices/new.gohtml:88 +#: web/template/invoices/edit.gohtml:89 web/template/quotes/products.gohtml:72 #: web/template/quotes/new.gohtml:88 web/template/quotes/edit.gohtml:89 msgctxt "action" msgid "Add products" @@ -94,31 +94,31 @@ msgctxt "action" msgid "Undo" msgstr "Deshacer" -#: web/template/invoices/new.gohtml:63 web/template/invoices/view.gohtml:67 -#: web/template/invoices/edit.gohtml:64 web/template/quotes/new.gohtml:64 +#: web/template/invoices/new.gohtml:64 web/template/invoices/view.gohtml:71 +#: web/template/invoices/edit.gohtml:65 web/template/quotes/new.gohtml:64 #: web/template/quotes/view.gohtml:78 web/template/quotes/edit.gohtml:65 msgctxt "title" msgid "Subtotal" msgstr "Subtotal" -#: web/template/invoices/new.gohtml:73 web/template/invoices/view.gohtml:71 -#: web/template/invoices/view.gohtml:111 web/template/invoices/edit.gohtml:74 +#: web/template/invoices/new.gohtml:74 web/template/invoices/view.gohtml:75 +#: web/template/invoices/view.gohtml:115 web/template/invoices/edit.gohtml:75 #: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82 #: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75 msgctxt "title" msgid "Total" msgstr "Total" -#: web/template/invoices/new.gohtml:91 web/template/invoices/edit.gohtml:92 +#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 msgctxt "action" msgid "Update" msgstr "Actualizar" -#: web/template/invoices/new.gohtml:94 web/template/invoices/edit.gohtml:95 +#: web/template/invoices/new.gohtml:95 web/template/invoices/edit.gohtml:96 #: web/template/quotes/new.gohtml:95 web/template/quotes/edit.gohtml:96 #: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 -#: web/template/expenses/new.gohtml:33 web/template/expenses/edit.gohtml:38 +#: web/template/expenses/new.gohtml:34 web/template/expenses/edit.gohtml:40 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 msgctxt "action" msgid "Save" @@ -136,7 +136,7 @@ msgstr "Nueva factura" #: web/template/invoices/index.gohtml:43 web/template/dashboard.gohtml:23 #: web/template/quotes/index.gohtml:43 web/template/contacts/index.gohtml:36 -#: web/template/expenses/index.gohtml:36 web/template/products/index.gohtml:34 +#: web/template/expenses/index.gohtml:37 web/template/products/index.gohtml:34 msgctxt "action" msgid "Filter" msgstr "Filtrar" @@ -146,7 +146,7 @@ msgctxt "invoice" msgid "All" msgstr "Todas" -#: web/template/invoices/index.gohtml:50 web/template/invoices/view.gohtml:34 +#: web/template/invoices/index.gohtml:50 web/template/invoices/view.gohtml:38 #: web/template/quotes/index.gohtml:50 web/template/quotes/view.gohtml:37 msgctxt "title" msgid "Date" @@ -163,31 +163,32 @@ msgid "Customer" msgstr "Cliente" #: web/template/invoices/index.gohtml:53 web/template/quotes/index.gohtml:53 +#: web/template/expenses/index.gohtml:46 msgctxt "title" msgid "Status" msgstr "Estado" #: web/template/invoices/index.gohtml:54 web/template/quotes/index.gohtml:54 -#: web/template/contacts/index.gohtml:45 web/template/expenses/index.gohtml:45 +#: web/template/contacts/index.gohtml:45 web/template/expenses/index.gohtml:47 #: web/template/products/index.gohtml:41 msgctxt "title" msgid "Tags" msgstr "Etiquetes" #: web/template/invoices/index.gohtml:55 web/template/quotes/index.gohtml:55 -#: web/template/expenses/index.gohtml:46 +#: web/template/expenses/index.gohtml:48 msgctxt "title" msgid "Amount" msgstr "Importe" #: web/template/invoices/index.gohtml:56 web/template/quotes/index.gohtml:56 -#: web/template/expenses/index.gohtml:47 +#: web/template/expenses/index.gohtml:49 msgctxt "title" msgid "Download" msgstr "Descargar" #: web/template/invoices/index.gohtml:57 web/template/quotes/index.gohtml:57 -#: web/template/contacts/index.gohtml:46 web/template/expenses/index.gohtml:48 +#: web/template/contacts/index.gohtml:46 web/template/expenses/index.gohtml:50 #: web/template/products/index.gohtml:43 msgctxt "title" msgid "Actions" @@ -209,7 +210,7 @@ msgstr "Acciones para la factura %s" #: web/template/invoices/index.gohtml:121 web/template/invoices/view.gohtml:19 #: web/template/quotes/index.gohtml:120 web/template/quotes/view.gohtml:22 -#: web/template/contacts/index.gohtml:77 web/template/expenses/index.gohtml:87 +#: web/template/contacts/index.gohtml:77 web/template/expenses/index.gohtml:111 #: web/template/products/index.gohtml:73 msgctxt "action" msgid "Edit" @@ -226,11 +227,11 @@ msgid "No invoices added yet." msgstr "No hay facturas." #: web/template/invoices/index.gohtml:146 web/template/quotes/index.gohtml:153 -#: web/template/expenses/index.gohtml:104 +#: web/template/expenses/index.gohtml:128 msgid "Total" msgstr "Total" -#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:33 +#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:37 msgctxt "title" msgid "Invoice %s" msgstr "Factura %s" @@ -240,22 +241,27 @@ msgctxt "action" msgid "Download invoice" msgstr "Descargar factura" -#: web/template/invoices/view.gohtml:61 web/template/quotes/view.gohtml:72 +#: web/template/invoices/view.gohtml:25 +msgctxt "action" +msgid "Download invoice attachment" +msgstr "Descargar adjunto de factura" + +#: web/template/invoices/view.gohtml:65 web/template/quotes/view.gohtml:72 msgctxt "title" msgid "Concept" msgstr "Concepto" -#: web/template/invoices/view.gohtml:64 web/template/quotes/view.gohtml:75 +#: web/template/invoices/view.gohtml:68 web/template/quotes/view.gohtml:75 msgctxt "title" msgid "Discount" msgstr "Descuento" -#: web/template/invoices/view.gohtml:66 web/template/quotes/view.gohtml:77 +#: web/template/invoices/view.gohtml:70 web/template/quotes/view.gohtml:77 msgctxt "title" msgid "Units" msgstr "Unidades" -#: web/template/invoices/view.gohtml:101 web/template/quotes/view.gohtml:112 +#: web/template/invoices/view.gohtml:105 web/template/quotes/view.gohtml:112 msgctxt "title" msgid "Tax Base" msgstr "Base imponible" @@ -473,7 +479,7 @@ msgctxt "action" msgid "New contact" msgstr "Nuevo contacto" -#: web/template/contacts/index.gohtml:42 web/template/expenses/index.gohtml:42 +#: web/template/contacts/index.gohtml:42 web/template/expenses/index.gohtml:43 msgctxt "title" msgid "Contact" msgstr "Contacto" @@ -559,21 +565,21 @@ msgctxt "action" msgid "New expense" msgstr "Nuevo gasto" -#: web/template/expenses/index.gohtml:43 +#: web/template/expenses/index.gohtml:44 msgctxt "title" msgid "Invoice Date" msgstr "Fecha de factura" -#: web/template/expenses/index.gohtml:44 +#: web/template/expenses/index.gohtml:45 msgctxt "title" msgid "Invoice Number" msgstr "Número de factura" -#: web/template/expenses/index.gohtml:79 +#: web/template/expenses/index.gohtml:103 msgid "Actions for expense %s" msgstr "Acciones para el gasto %s" -#: web/template/expenses/index.gohtml:97 +#: web/template/expenses/index.gohtml:121 msgid "No expenses added yet." msgstr "No hay gastos." @@ -706,83 +712,84 @@ 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:164 pkg/products.go:263 pkg/quote.go:823 pkg/invoices.go:909 +#: pkg/products.go:164 pkg/products.go:263 pkg/quote.go:878 pkg/invoices.go:993 #: pkg/contacts.go:140 pkg/contacts.go:248 msgctxt "input" msgid "Name" msgstr "Nombre" -#: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:630 -#: pkg/expenses.go:188 pkg/expenses.go:347 pkg/invoices.go:174 -#: pkg/invoices.go:657 pkg/invoices.go:1208 pkg/contacts.go:145 +#: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:685 +#: pkg/expenses.go:228 pkg/expenses.go:417 pkg/invoices.go:174 +#: pkg/invoices.go:723 pkg/invoices.go:1295 pkg/contacts.go:145 #: pkg/contacts.go:348 msgctxt "input" msgid "Tags" msgstr "Etiquetes" -#: pkg/products.go:173 pkg/quote.go:178 pkg/expenses.go:351 pkg/invoices.go:178 +#: pkg/products.go:173 pkg/quote.go:178 pkg/expenses.go:427 pkg/invoices.go:178 #: pkg/contacts.go:149 msgctxt "input" msgid "Tags Condition" msgstr "Condición de las etiquetas" -#: pkg/products.go:177 pkg/quote.go:182 pkg/expenses.go:355 pkg/invoices.go:182 +#: pkg/products.go:177 pkg/quote.go:182 pkg/expenses.go:431 pkg/invoices.go:182 #: pkg/contacts.go:153 msgctxt "tag condition" msgid "All" msgstr "Todas" -#: pkg/products.go:178 pkg/expenses.go:356 pkg/invoices.go:183 +#: pkg/products.go:178 pkg/expenses.go:432 pkg/invoices.go:183 #: pkg/contacts.go:154 msgid "Invoices must have all the specified labels." msgstr "Las facturas deben tener todas las etiquetas." -#: pkg/products.go:182 pkg/quote.go:187 pkg/expenses.go:360 pkg/invoices.go:187 +#: pkg/products.go:182 pkg/quote.go:187 pkg/expenses.go:436 pkg/invoices.go:187 #: pkg/contacts.go:158 msgctxt "tag condition" msgid "Any" msgstr "Cualquiera" -#: pkg/products.go:183 pkg/expenses.go:361 pkg/invoices.go:188 +#: pkg/products.go:183 pkg/expenses.go:437 pkg/invoices.go:188 #: pkg/contacts.go:159 msgid "Invoices must have at least one of the specified labels." msgstr "Las facturas deben tener como mínimo una de las etiquetas." -#: pkg/products.go:269 pkg/quote.go:837 pkg/invoices.go:923 +#: pkg/products.go:269 pkg/quote.go:892 pkg/invoices.go:1007 msgctxt "input" msgid "Description" msgstr "Descripción" -#: pkg/products.go:274 pkg/quote.go:841 pkg/invoices.go:927 +#: pkg/products.go:274 pkg/quote.go:896 pkg/invoices.go:1011 msgctxt "input" msgid "Price" msgstr "Precio" -#: pkg/products.go:284 pkg/quote.go:870 pkg/expenses.go:167 pkg/invoices.go:956 +#: pkg/products.go:284 pkg/quote.go:925 pkg/expenses.go:200 +#: pkg/invoices.go:1040 msgctxt "input" msgid "Taxes" msgstr "Impuestos" -#: pkg/products.go:309 pkg/quote.go:919 pkg/profile.go:92 pkg/invoices.go:1005 +#: pkg/products.go:309 pkg/quote.go:974 pkg/profile.go:92 pkg/invoices.go:1089 #: pkg/contacts.go:398 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." -#: pkg/products.go:310 pkg/quote.go:920 pkg/invoices.go:1006 +#: pkg/products.go:310 pkg/quote.go:975 pkg/invoices.go:1090 msgid "Price can not be empty." msgstr "No podéis dejar el precio en blanco." -#: pkg/products.go:311 pkg/quote.go:921 pkg/invoices.go:1007 +#: pkg/products.go:311 pkg/quote.go:976 pkg/invoices.go:1091 msgid "Price must be a number greater than zero." msgstr "El precio tiene que ser un número mayor a cero." -#: pkg/products.go:313 pkg/quote.go:929 pkg/expenses.go:213 pkg/expenses.go:218 -#: pkg/invoices.go:1015 +#: pkg/products.go:313 pkg/quote.go:984 pkg/expenses.go:264 pkg/expenses.go:269 +#: pkg/invoices.go:1099 msgid "Selected tax is not valid." msgstr "Habéis escogido un impuesto que no es válido." -#: pkg/products.go:314 pkg/quote.go:930 pkg/expenses.go:214 pkg/expenses.go:219 -#: pkg/invoices.go:1016 +#: pkg/products.go:314 pkg/quote.go:985 pkg/expenses.go:265 pkg/expenses.go:270 +#: pkg/invoices.go:1100 msgid "You can only select a tax of each class." msgstr "Solo podéis escoger un impuesto de cada clase." @@ -991,7 +998,7 @@ msgstr "No podéis dejar el nombre del método de pago en blanco." msgid "Payment instructions can not be empty." msgstr "No podéis dejar las instrucciones de pago en blanco." -#: pkg/quote.go:147 pkg/quote.go:608 pkg/invoices.go:147 pkg/invoices.go:640 +#: pkg/quote.go:147 pkg/quote.go:663 pkg/invoices.go:147 pkg/invoices.go:706 msgctxt "input" msgid "Customer" msgstr "Cliente" @@ -1000,12 +1007,12 @@ msgstr "Cliente" msgid "All customers" msgstr "Todos los clientes" -#: pkg/quote.go:153 pkg/quote.go:602 +#: pkg/quote.go:153 pkg/quote.go:657 msgctxt "input" msgid "Quotation Status" msgstr "Estado del presupuesto" -#: pkg/quote.go:154 pkg/invoices.go:154 +#: pkg/quote.go:154 pkg/expenses.go:422 pkg/invoices.go:154 msgid "All status" msgstr "Todos los estados" @@ -1014,12 +1021,12 @@ msgctxt "input" msgid "Quotation Number" msgstr "Número de presupuesto" -#: pkg/quote.go:164 pkg/expenses.go:337 pkg/invoices.go:164 +#: pkg/quote.go:164 pkg/expenses.go:407 pkg/invoices.go:164 msgctxt "input" msgid "From Date" msgstr "A partir de la fecha" -#: pkg/quote.go:169 pkg/expenses.go:342 pkg/invoices.go:169 +#: pkg/quote.go:169 pkg/expenses.go:412 pkg/invoices.go:169 msgctxt "input" msgid "To Date" msgstr "Hasta la fecha" @@ -1032,99 +1039,99 @@ msgstr "Los presupuestos deben tener todas las etiquetas." msgid "Quotations must have at least one of the specified labels." msgstr "Los presupuestos deben tener como mínimo una de las etiquetas." -#: pkg/quote.go:550 +#: pkg/quote.go:605 msgid "quotations.zip" msgstr "presupuestos.zip" -#: pkg/quote.go:556 pkg/quote.go:1085 pkg/quote.go:1093 pkg/invoices.go:589 -#: pkg/invoices.go:1183 pkg/invoices.go:1191 +#: pkg/quote.go:611 pkg/quote.go:1140 pkg/quote.go:1148 pkg/invoices.go:654 +#: pkg/invoices.go:1270 pkg/invoices.go:1278 msgid "Invalid action" msgstr "Acción inválida." -#: pkg/quote.go:609 +#: pkg/quote.go:664 msgid "Select a customer to quote." msgstr "Escoged un cliente a presupuestar." -#: pkg/quote.go:614 +#: pkg/quote.go:669 msgctxt "input" msgid "Quotation Date" msgstr "Fecha del presupuesto" -#: pkg/quote.go:620 +#: pkg/quote.go:675 msgctxt "input" msgid "Terms and conditions" msgstr "Condiciones de aceptación" -#: pkg/quote.go:625 pkg/invoices.go:652 +#: pkg/quote.go:680 pkg/invoices.go:718 msgctxt "input" msgid "Notes" msgstr "Notas" -#: pkg/quote.go:634 pkg/invoices.go:662 +#: pkg/quote.go:689 pkg/invoices.go:728 msgctxt "input" msgid "Payment Method" msgstr "Método de pago" -#: pkg/quote.go:635 +#: pkg/quote.go:690 msgid "Select a payment method." msgstr "Escoged un método e pago." -#: pkg/quote.go:671 +#: pkg/quote.go:726 msgid "Selected quotation status is not valid." msgstr "Habéis escogido un estado de presupuesto que no es válido." -#: pkg/quote.go:673 pkg/invoices.go:699 +#: pkg/quote.go:728 pkg/invoices.go:783 msgid "Selected customer is not valid." msgstr "Habéis escogido un cliente que no es válido." -#: pkg/quote.go:675 +#: pkg/quote.go:730 msgid "Quotation date can not be empty." msgstr "No podéis dejar la fecha del presupuesto en blanco." -#: pkg/quote.go:676 +#: pkg/quote.go:731 msgid "Quotation date must be a valid date." msgstr "La fecha de presupuesto debe ser válida." -#: pkg/quote.go:679 pkg/invoices.go:703 +#: pkg/quote.go:734 pkg/invoices.go:787 msgid "Selected payment method is not valid." msgstr "Habéis escogido un método de pago que no es válido." -#: pkg/quote.go:813 pkg/quote.go:818 pkg/invoices.go:899 pkg/invoices.go:904 +#: pkg/quote.go:868 pkg/quote.go:873 pkg/invoices.go:983 pkg/invoices.go:988 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/quote.go:851 pkg/invoices.go:937 +#: pkg/quote.go:906 pkg/invoices.go:1021 msgctxt "input" msgid "Quantity" msgstr "Cantidad" -#: pkg/quote.go:860 pkg/invoices.go:946 +#: pkg/quote.go:915 pkg/invoices.go:1030 msgctxt "input" msgid "Discount (%)" msgstr "Descuento (%)" -#: pkg/quote.go:914 +#: pkg/quote.go:969 msgid "Quotation product ID must be a number greater than zero." msgstr "El ID de producto de presupuesto tiene que ser un número mayor a cero." -#: pkg/quote.go:917 pkg/invoices.go:1003 +#: pkg/quote.go:972 pkg/invoices.go:1087 msgid "Product ID must be a positive number or zero." msgstr "El ID de producto tiene que ser un número positivo o cero." -#: pkg/quote.go:923 pkg/invoices.go:1009 +#: pkg/quote.go:978 pkg/invoices.go:1093 msgid "Quantity can not be empty." msgstr "No podéis dejar la cantidad en blanco." -#: pkg/quote.go:924 pkg/invoices.go:1010 +#: pkg/quote.go:979 pkg/invoices.go:1094 msgid "Quantity must be a number greater than zero." msgstr "La cantidad tiene que ser un número mayor a cero." -#: pkg/quote.go:926 pkg/invoices.go:1012 +#: pkg/quote.go:981 pkg/invoices.go:1096 msgid "Discount can not be empty." msgstr "No podéis dejar el descuento en blanco." -#: pkg/quote.go:927 pkg/invoices.go:1013 +#: pkg/quote.go:982 pkg/invoices.go:1097 msgid "Discount must be a percentage between 0 and 100." msgstr "El descuento tiene que ser un porcentaje entre 0 y 100." @@ -1191,92 +1198,101 @@ msgctxt "period option" msgid "Previous year" msgstr "Año anterior" -#: pkg/expenses.go:115 +#: pkg/expenses.go:147 msgid "Select a contact." msgstr "Escoged un contacto" -#: pkg/expenses.go:150 pkg/expenses.go:326 +#: pkg/expenses.go:183 pkg/expenses.go:396 msgctxt "input" msgid "Contact" msgstr "Contacto" -#: pkg/expenses.go:156 +#: pkg/expenses.go:189 msgctxt "input" msgid "Invoice number" msgstr "Número de factura" -#: pkg/expenses.go:161 pkg/invoices.go:646 +#: pkg/expenses.go:194 pkg/invoices.go:712 msgctxt "input" msgid "Invoice Date" msgstr "Fecha de factura" -#: pkg/expenses.go:173 +#: pkg/expenses.go:206 msgctxt "input" msgid "Amount" msgstr "Importe" -#: pkg/expenses.go:183 +#: pkg/expenses.go:216 pkg/invoices.go:734 msgctxt "input" msgid "File" msgstr "Archivo" -#: pkg/expenses.go:211 +#: pkg/expenses.go:222 pkg/expenses.go:421 +msgctxt "input" +msgid "Expense Status" +msgstr "Estado del gasto" + +#: pkg/expenses.go:262 msgid "Selected contact is not valid." msgstr "Habéis escogido un contacto que no es válido." -#: pkg/expenses.go:212 pkg/invoices.go:701 +#: pkg/expenses.go:263 pkg/invoices.go:785 msgid "Invoice date must be a valid date." msgstr "La fecha de factura debe ser válida." -#: pkg/expenses.go:215 +#: pkg/expenses.go:266 msgid "Amount can not be empty." msgstr "No podéis dejar el importe en blanco." -#: pkg/expenses.go:216 +#: pkg/expenses.go:267 msgid "Amount must be a number greater than zero." msgstr "El importe tiene que ser un número mayor a cero." -#: pkg/expenses.go:327 +#: pkg/expenses.go:271 +msgid "Selected expense status is not valid." +msgstr "Habéis escogido un estado de gasto que no es válido." + +#: pkg/expenses.go:397 msgid "All contacts" msgstr "Todos los contactos" -#: pkg/expenses.go:332 pkg/invoices.go:159 +#: pkg/expenses.go:402 pkg/invoices.go:159 msgctxt "input" msgid "Invoice Number" msgstr "Número de factura" -#: pkg/invoices.go:153 pkg/invoices.go:634 +#: pkg/invoices.go:153 pkg/invoices.go:700 msgctxt "input" msgid "Invoice Status" msgstr "Estado de la factura" -#: pkg/invoices.go:482 +#: pkg/invoices.go:544 msgid "Select a customer to bill." msgstr "Escoged un cliente a facturar." -#: pkg/invoices.go:583 +#: pkg/invoices.go:648 msgid "invoices.zip" msgstr "facturas.zip" -#: pkg/invoices.go:698 +#: pkg/invoices.go:782 msgid "Selected invoice status is not valid." msgstr "Habéis escogido un estado de factura que no es válido." -#: pkg/invoices.go:700 +#: pkg/invoices.go:784 msgid "Invoice date can not be empty." msgstr "No podéis dejar la fecha de la factura en blanco." -#: pkg/invoices.go:836 +#: pkg/invoices.go:920 #, c-format msgid "Re: quotation #%s of %s" msgstr "Ref: presupuesto n.º %s del %s" -#: pkg/invoices.go:837 +#: pkg/invoices.go:921 msgctxt "to_char" msgid "MM/DD/YYYY" msgstr "DD/MM/YYYY" -#: pkg/invoices.go:1000 +#: pkg/invoices.go:1084 msgid "Invoice product ID must be a number greater than zero." msgstr "El ID de producto de factura tiene que ser un número mayor a cero." diff --git a/revert/attach_to_invoice.sql b/revert/attach_to_invoice.sql new file mode 100644 index 0000000..40390d1 --- /dev/null +++ b/revert/attach_to_invoice.sql @@ -0,0 +1,7 @@ +-- Revert numerus:attach_to_invoice from pg + +begin; + +drop function if exists numerus.attach_to_invoice(uuid, text, text, bytea); + +commit; diff --git a/revert/invoice_attachment.sql b/revert/invoice_attachment.sql new file mode 100644 index 0000000..3336f7a --- /dev/null +++ b/revert/invoice_attachment.sql @@ -0,0 +1,7 @@ +-- Revert numerus:invoice_attachment from pg + +begin; + +drop table if exists numerus.invoice_attachment; + +commit; diff --git a/sqitch.plan b/sqitch.plan index 2d0575c..2345800 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -122,3 +122,5 @@ available_expense_status [schema_numerus expense_status expense_status_i18n] 202 expense_expense_status [expense] 2023-07-11T12:28:58Z jordi fita mas # Add expense_status to expense relation add_expense [add_expense@v1 expense_status expense_expense_status] 2023-07-11T13:16:16Z jordi fita mas # Add expense_status parameter to add_expense edit_expense [edit_expense@v1 expense_status expense_expense_status] 2023-07-11T13:21:17Z jordi fita mas # Add expense_status parameter to edit_expense +invoice_attachment [schema_numerus roles invoice] 2023-07-12T17:10:58Z jordi fita mas # Add relation for invoice attachment +attach_to_invoice [schema_numerus roles invoice invoice_attachment] 2023-07-12T17:21:19Z jordi fita mas # Add function to attachment a document to invoices diff --git a/test/attach_to_invoice.sql b/test/attach_to_invoice.sql new file mode 100644 index 0000000..df9fc05 --- /dev/null +++ b/test/attach_to_invoice.sql @@ -0,0 +1,97 @@ +-- Test attach_to_invoice +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to auth, numerus, public; + +select plan(12); + +select has_function('numerus', 'attach_to_invoice', array ['uuid', 'text', 'text', 'bytea']); +select function_lang_is('numerus', 'attach_to_invoice', array ['uuid', 'text', 'text', 'bytea'], 'sql'); +select function_returns('numerus', 'attach_to_invoice', array ['uuid', 'text', 'text', 'bytea'], 'void'); +select isnt_definer('numerus', 'attach_to_invoice', array ['uuid', 'text', 'text', 'bytea']); +select volatility_is('numerus', 'attach_to_invoice', array ['uuid', 'text', 'text', 'bytea'], 'volatile'); +select function_privs_are('numerus', 'attach_to_invoice', array ['uuid', 'text', 'text', 'bytea'], 'guest', array []::text[]); +select function_privs_are('numerus', 'attach_to_invoice', array ['uuid', 'text', 'text', 'bytea'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'attach_to_invoice', array ['uuid', 'text', 'text', 'bytea'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'attach_to_invoice', array ['uuid', 'text', 'text', 'bytea'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate invoice_attachment cascade; +truncate invoice cascade; +truncate contact_tax_details cascade; +truncate contact cascade; +truncate tax cascade; +truncate tax_class cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + + +set constraints "company_default_payment_method_id_fkey" deferred; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') + , (112, 1, 'bank', 'send money to my bank account') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into tax_class (tax_class_id, company_id, name) +values (11, 1, 'tax') +; + +insert into tax (tax_id, company_id, tax_class_id, name, rate) +values (3, 1, 11, 'IRPF -15 %', -0.15) + , (4, 1, 11, 'IVA 21 %', 0.21) +; + +insert into contact (contact_id, company_id, name) +values (12, 1, 'Contact 2.1') + , (13, 1, 'Contact 2.2') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values (12, 'Customer 2.1 Ltd', 'XXX123', 'Fake St.', 'City', 'Province', '17480', 'ES') + , (13, 'Customer 2.2 Ltd', 'XXX234', 'Fake St.', 'City', 'Province', '17480', 'ES') +; + +insert into invoice (invoice_id, company_id, slug, invoice_number, invoice_date, contact_id, payment_method_id, currency_code, tags) +values (15, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'INV1', '2023-05-04', 12, 111, 'EUR', '{tag1}') + , (16, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'INV2', '2023-05-05', 13, 112, 'EUR', '{tag2}') +; + +insert into invoice_attachment (invoice_id, original_filename, mime_type, content) +values (16, 'something.txt', 'text/plain', convert_to('Once upon a time…', 'UTF-8')) +; + +select lives_ok( + $$ select attach_to_invoice('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'invoice.txt', 'text/plain', convert_to('Total 42€', 'UTF-8')) $$, + 'Should be able to attach a document to the first invoice' +); + +select lives_ok( + $$ select attach_to_invoice('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'invoice.html', 'text/html', convert_to('

Total 42€

', 'UTF-8')) $$, + 'Should be able to replace the second invoice’s attachment with a new document' +); + +select bag_eq( + $$ select invoice_id, original_filename, mime_type, content from invoice_attachment $$, + $$ values (15, 'invoice.txt', 'text/plain', convert_to('Total 42€', 'UTF-8')) + , (16, 'invoice.html', 'text/html', convert_to('

Total 42€

', 'UTF-8')) + $$, + 'Should have attached all documents' +); + +select * +from finish(); + +rollback; diff --git a/test/invoice_attachment.sql b/test/invoice_attachment.sql new file mode 100644 index 0000000..4ca96ff --- /dev/null +++ b/test/invoice_attachment.sql @@ -0,0 +1,150 @@ +-- Test invoice_attachment +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(29); + +set search_path to numerus, auth, public; + +select has_table('invoice_attachment'); +select has_pk('invoice_attachment' ); +select table_privs_are('invoice_attachment', 'guest', array []::text[]); +select table_privs_are('invoice_attachment', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('invoice_attachment', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('invoice_attachment', 'authenticator', array []::text[]); + +select has_column('invoice_attachment', 'invoice_id'); +select col_is_pk('invoice_attachment', 'invoice_id'); +select col_is_fk('invoice_attachment', 'invoice_id'); +select fk_ok('invoice_attachment', 'invoice_id', 'invoice', 'invoice_id'); +select col_type_is('invoice_attachment', 'invoice_id', 'integer'); +select col_not_null('invoice_attachment', 'invoice_id'); +select col_hasnt_default('invoice_attachment', 'invoice_id'); + +select has_column('invoice_attachment', 'original_filename'); +select col_type_is('invoice_attachment', 'original_filename', 'text'); +select col_not_null('invoice_attachment', 'original_filename'); +select col_hasnt_default('invoice_attachment', 'original_filename'); + +select has_column('invoice_attachment', 'mime_type'); +select col_type_is('invoice_attachment', 'mime_type', 'text'); +select col_not_null('invoice_attachment', 'mime_type'); +select col_hasnt_default('invoice_attachment', 'mime_type'); + +select has_column('invoice_attachment', 'content'); +select col_type_is('invoice_attachment', 'content', 'bytea'); +select col_not_null('invoice_attachment', 'content'); +select col_hasnt_default('invoice_attachment', 'content'); + + +set client_min_messages to warning; +truncate invoice_attachment cascade; +truncate invoice cascade; +truncate tax cascade; +truncate tax_class cascade; +truncate contact_tax_details cascade; +truncate contact cascade; +truncate company_user cascade; +truncate payment_method cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +set constraints "company_default_payment_method_id_fkey" deferred; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222) + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (444, 4, 'cash', 'cash') + , (222, 2, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into company_user (company_id, user_id) +values (2, 1) + , (4, 5) +; + +insert into tax_class (tax_class_id, company_id, name) +values (22, 2, 'vat') + , (44, 4, 'vat') +; + +insert into tax (tax_id, company_id, tax_class_id, name, rate) +values (3, 2, 22, 'IVA 21 %', 0.21) + , (6, 4, 44, 'IVA 10 %', 0.10) +; + +insert into contact (contact_id, company_id, name) +values ( 9, 2, 'Customer 1') + , (10, 4, 'Customer 2') +; + +insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code) +values ( 9, 'Customer 1 Ltd', 'XXX123', 'Fake St.', 'City', 'Province', '17480', 'ES') + , (10, 'Customer 2 Ltd', 'XXX234', 'Fake St.', 'City', 'Province', '17480', 'ES') +; + +insert into invoice (invoice_id, company_id, invoice_number, contact_id, invoice_date, payment_method_id, currency_code) +values (13, 2, 'INV001', 9, '2011-01-11', 222, 'EUR') + , (14, 4, 'INV002', 10, '2022-02-22', 444, 'EUR') +; + +insert into invoice_attachment (invoice_id, original_filename, mime_type, content) +values (13, 'invoice.txt', 'text/plain', convert_to('IOU 42', 'UTF8')) + , (14, 'invoice.html', 'text/html', convert_to('IOU 42', 'UTF8')) +; + +prepare invoice_attachment_data as +select invoice_id, original_filename +from invoice_attachment +order by invoice_id, original_filename; + +set role invoicer; +select is_empty('invoice_attachment_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'invoice_attachment_data', + $$ values (13, 'invoice.txt') + $$, + 'Should only list tax of products of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'invoice_attachment_data', + $$ values (14, 'invoice.html') + $$, + 'Should only list tax of products of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'invoice_attachment_data', + '42501', 'permission denied for table invoice_attachment', + 'Should not allow select to guest users' +); +reset role; + + +select * +from finish(); + +rollback; + diff --git a/verify/attach_to_invoice.sql b/verify/attach_to_invoice.sql new file mode 100644 index 0000000..d4e2af7 --- /dev/null +++ b/verify/attach_to_invoice.sql @@ -0,0 +1,7 @@ +-- Verify numerus:attach_to_invoice on pg + +begin; + +select has_function_privilege('numerus.attach_to_invoice(uuid, text, text, bytea)', 'execute'); + +rollback; diff --git a/verify/invoice_attachment.sql b/verify/invoice_attachment.sql new file mode 100644 index 0000000..d35d7aa --- /dev/null +++ b/verify/invoice_attachment.sql @@ -0,0 +1,15 @@ +-- Verify numerus:invoice_attachment on pg + +begin; + +select invoice_id + , original_filename + , mime_type + , content +from numerus.invoice_attachment +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.invoice_attachment'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.invoice_attachment'::regclass; + +rollback; diff --git a/web/template/invoices/edit.gohtml b/web/template/invoices/edit.gohtml index 84c8aec..04fe687 100644 --- a/web/template/invoices/edit.gohtml +++ b/web/template/invoices/edit.gohtml @@ -17,7 +17,7 @@ {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editInvoicePage*/ -}}

{{ printf (pgettext "Edit Invoice “%s”" "title") .Number }}

-
@@ -50,6 +50,7 @@ {{ template "tags-field" .Tags }} {{ template "select-field" .PaymentMethod }} {{ template "select-field" .InvoiceStatus }} + {{ template "file-field" .File }} {{ template "input-field" .Notes }} diff --git a/web/template/invoices/new.gohtml b/web/template/invoices/new.gohtml index ae5e43e..2b2bb68 100644 --- a/web/template/invoices/new.gohtml +++ b/web/template/invoices/new.gohtml @@ -17,7 +17,7 @@ {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newInvoicePage*/ -}}

{{(pgettext "New Invoice" "title")}}

- @@ -50,6 +50,7 @@ {{ template "input-field" .Date }} {{ template "tags-field" .Tags }} {{ template "select-field" .PaymentMethod }} + {{ template "file-field" .File }} {{ template "input-field" .Notes }} {{- range $product := .Products }} diff --git a/web/template/invoices/view.gohtml b/web/template/invoices/view.gohtml index f054595..cebc37e 100644 --- a/web/template/invoices/view.gohtml +++ b/web/template/invoices/view.gohtml @@ -20,6 +20,10 @@ {{( pgettext "Download invoice" "action" )}} + {{ if .OriginalFileName }} + {{( pgettext "Download invoice attachment" "action" )}} + {{ end }}

{{- end }}