Compare commits

...

2 Commits

Author SHA1 Message Date
jordi fita mas bb7af20a17 Add attachments to invoices
Works exactly the same as for expenses, and this is sometimes convenient
for keeping transfer slips from customers and such.

I actually did not know where to add the download from this attachment,
because if add a column to the index it can easily be confused with the
download icon for the actual invoice.

Part of #66.
2023-07-12 20:06:53 +02:00
jordi fita mas 66ab3b4bf7 Remove an unnecessary truncate from expense_attachment test 2023-07-12 20:03:36 +02:00
17 changed files with 622 additions and 197 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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)
}

View File

@ -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)

208
po/ca.po
View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\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 ladjunt 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 dusuari 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 dacceptació"
#: 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 "LID 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 "LID 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 limport en blanc."
#: pkg/expenses.go:216
#: pkg/expenses.go:267
msgid "Amount must be a number greater than zero."
msgstr "Limport 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 "LID del producte de factura ha de ser un número major a zero."

208
po/es.po
View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\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."

View File

@ -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;

View File

@ -0,0 +1,7 @@
-- Revert numerus:invoice_attachment from pg
begin;
drop table if exists numerus.invoice_attachment;
commit;

View File

@ -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 <jordi@tandem.blog> # Add expense_status to expense relation
add_expense [add_expense@v1 expense_status expense_expense_status] 2023-07-11T13:16:16Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add expense_status parameter to edit_expense
invoice_attachment [schema_numerus roles invoice] 2023-07-12T17:10:58Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice attachment
attach_to_invoice [schema_numerus roles invoice invoice_attachment] 2023-07-12T17:21:19Z jordi fita mas <jordi@tandem.blog> # Add function to attachment a document to invoices

View File

@ -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('<html><p>Total 42€</p></html>', 'UTF-8')) $$,
'Should be able to replace the second invoices 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('<html><p>Total 42€</p></html>', 'UTF-8'))
$$,
'Should have attached all documents'
);
select *
from finish();
rollback;

View File

@ -43,7 +43,6 @@ select col_hasnt_default('expense_attachment', 'content');
set client_min_messages to warning;
truncate expense_attachment cascade;
truncate expense cascade;
truncate invoice cascade;
truncate tax cascade;
truncate tax_class cascade;
truncate contact cascade;

150
test/invoice_attachment.sql Normal file
View File

@ -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('<html>IOU <em>42</em></html>', '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;

View File

@ -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;

View File

@ -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;

View File

@ -17,7 +17,7 @@
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editInvoicePage*/ -}}
<section id="invoice-dialog-content" data-hx-target="main">
<h2>{{ printf (pgettext "Edit Invoice “%s”" "title") .Number }}</h2>
<form method="POST" action="{{ companyURI "/invoices/" }}{{ .Slug }}/edit"
<form enctype="multipart/form-data" method="POST" action="{{ companyURI "/invoices/" }}{{ .Slug }}/edit"
data-hx-boost="true"
data-hx-swap="innerHTML show:false"
>
@ -50,6 +50,7 @@
{{ template "tags-field" .Tags }}
{{ template "select-field" .PaymentMethod }}
{{ template "select-field" .InvoiceStatus }}
{{ template "file-field" .File }}
{{ template "input-field" .Notes }}
</div>

View File

@ -17,7 +17,7 @@
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newInvoicePage*/ -}}
<section id="invoice-dialog-content" data-hx-target="main">
<h2>{{(pgettext "New Invoice" "title")}}</h2>
<form method="POST" action="{{ companyURI "/invoices/new" }}"
<form enctype="multipart/form-data" method="POST" action="{{ companyURI "/invoices/new" }}"
data-hx-boost="true"
data-hx-swap="innerHTML show:false"
>
@ -50,6 +50,7 @@
{{ template "input-field" .Date }}
{{ template "tags-field" .Tags }}
{{ template "select-field" .PaymentMethod }}
{{ template "file-field" .File }}
{{ template "input-field" .Notes }}
</div>
{{- range $product := .Products }}

View File

@ -20,6 +20,10 @@
<a class="primary button"
href="{{ companyURI "/invoices/" }}{{ .Slug }}.pdf"
download="{{ .Number}}-{{ .Invoicee.Name | slugify }}.pdf">{{( pgettext "Download invoice" "action" )}}</a>
{{ if .OriginalFileName }}
<a class="primary button"
href="{{ companyURI "/invoices/"}}{{ .Slug }}/download/{{.OriginalFileName}}">{{( pgettext "Download invoice attachment" "action" )}}</a>
{{ end }}
</p>
</nav>
{{- end }}