From 65413637ac7b51ef36e0b0f402b3cc634b8f3c9c Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Fri, 26 Jan 2024 02:29:51 +0100 Subject: [PATCH] Add a column for each tax type when exporting invoices and expenses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the HTML tables i only compute the aggregated amount by tax class (e.g., IVA, IRPF), but here we need the actual tax (e.g., IVA 4 %) because this spreadsheet is intended for accountants. I can easily extract the amounts from invoice_tax_amount and expense_tax_amount, but i also need to add the columns to the spreadsheet, and always with the same order—does not matter much which, only the same—, that’s why i had to sort the tax IDs when exporting, as Go does not guarantee an order for maps. Closes #92 --- pkg/expenses.go | 29 ++++++++-- pkg/invoices.go | 99 +++++++++++++++++++++++++++++++- pkg/ods.go | 73 ++++++++++++++++++------ po/ca.po | 148 ++++++++++++++++++++++++------------------------ po/es.po | 148 ++++++++++++++++++++++++------------------------ 5 files changed, 324 insertions(+), 173 deletions(-) diff --git a/pkg/expenses.go b/pkg/expenses.go index b5d9423..e3d5efe 100644 --- a/pkg/expenses.go +++ b/pkg/expenses.go @@ -13,6 +13,7 @@ import ( ) type ExpenseEntry struct { + ID int Slug string InvoiceDate time.Time InvoiceNumber string @@ -58,7 +59,8 @@ func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params) func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale, filters *expenseFilterForm) []*ExpenseEntry { where, args := filters.BuildQuery([]interface{}{locale.Language.String()}) rows := conn.MustQuery(ctx, fmt.Sprintf(` - select expense.slug + select expense_id + , expense.slug , invoice_date , invoice_number , to_price(expense.amount, decimal_digits) as amount @@ -78,7 +80,8 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale, join expense_status_i18n esi18n on expense.expense_status = esi18n.expense_status and esi18n.lang_tag = $1 join currency using (currency_code) where (%s) - group by expense.slug + group by expense_id + , expense.slug , invoice_date , invoice_number , expense.amount @@ -98,7 +101,7 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale, Taxes: make(map[string]string), } var taxes [][]string - if err := rows.Scan(&entry.Slug, &entry.InvoiceDate, &entry.InvoiceNumber, &entry.Amount, &taxes, &entry.Total, &entry.InvoicerName, &entry.OriginalFileName, &entry.Tags, &entry.Status, &entry.StatusLabel); err != nil { + if err := rows.Scan(&entry.ID, &entry.Slug, &entry.InvoiceDate, &entry.InvoiceNumber, &entry.Amount, &taxes, &entry.Total, &entry.InvoicerName, &entry.OriginalFileName, &entry.Tags, &entry.Status, &entry.StatusLabel); err != nil { panic(err) } for _, tax := range taxes { @@ -737,9 +740,27 @@ func HandleBatchExpenseAction(w http.ResponseWriter, r *http.Request, _ httprout http.Error(w, err.Error(), http.StatusBadRequest) return } - ods := mustWriteExpensesOds(mustCollectExpenseEntries(r.Context(), conn, locale, filters), locale, company) + entries := mustCollectExpenseEntries(r.Context(), conn, locale, filters) + taxes := mustCollectExpenseEntriesTaxes(r.Context(), conn, entries) + taxColumns := mustCollectTaxColumns(r.Context(), conn, company) + ods := mustWriteExpensesOds(entries, taxes, taxColumns, locale, company) writeOdsResponse(w, ods, gettext("expenses.ods", locale)) default: http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest) } } + +func mustCollectExpenseEntriesTaxes(ctx context.Context, conn *Conn, entries []*ExpenseEntry) map[int]taxMap { + ids := mustMakeIDArray(entries, func(entry *ExpenseEntry) int { + return entry.ID + }) + return mustMakeTaxMap(ctx, conn, ids, ` + select expense_id + , tax_id + , to_price(tax.amount, decimal_digits) + from expense_tax_amount as tax + join expense using (expense_id) + join currency using (currency_code) + where expense_id = any ($1) + `) +} diff --git a/pkg/invoices.go b/pkg/invoices.go index 358028c..15c7d25 100644 --- a/pkg/invoices.go +++ b/pkg/invoices.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "github.com/jackc/pgtype" "github.com/julienschmidt/httprouter" "html/template" "io" @@ -23,6 +24,7 @@ import ( const removedProductSuffix = ".removed" type InvoiceEntry struct { + ID int Slug string Date time.Time Number string @@ -61,7 +63,8 @@ func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, locale *Locale, filters *invoiceFilterForm) []*InvoiceEntry { where, args := filters.BuildQuery([]interface{}{locale.Language.String()}) rows := conn.MustQuery(ctx, fmt.Sprintf(` - select invoice.slug + select invoice_id + , invoice.slug , invoice_date , invoice_number , contact.name @@ -83,7 +86,7 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, locale *Locale, var entries []*InvoiceEntry for rows.Next() { entry := &InvoiceEntry{} - if err := rows.Scan(&entry.Slug, &entry.Date, &entry.Number, &entry.CustomerName, &entry.Tags, &entry.Status, &entry.StatusLabel, &entry.Total); err != nil { + if err := rows.Scan(&entry.ID, &entry.Slug, &entry.Date, &entry.Number, &entry.CustomerName, &entry.Tags, &entry.Status, &entry.StatusLabel, &entry.Total); err != nil { panic(err) } entries = append(entries, entry) @@ -671,13 +674,103 @@ func HandleBatchInvoiceAction(w http.ResponseWriter, r *http.Request, _ httprout http.Error(w, err.Error(), http.StatusBadRequest) return } - ods := mustWriteInvoicesOds(mustCollectInvoiceEntries(r.Context(), conn, locale, filters), locale, company) + entries := mustCollectInvoiceEntries(r.Context(), conn, locale, filters) + taxes := mustCollectInvoiceEntriesTaxes(r.Context(), conn, entries) + taxColumns := mustCollectTaxColumns(r.Context(), conn, company) + ods := mustWriteInvoicesOds(entries, taxes, taxColumns, locale, company) writeOdsResponse(w, ods, gettext("invoices.ods", locale)) default: http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest) } } +func mustCollectTaxColumns(ctx context.Context, conn *Conn, company *Company) map[int]string { + rows, err := conn.Query(ctx, ` + select tax_id + , name + from tax + where company_id = $1 + `, company.Id) + if err != nil { + panic(err) + } + defer rows.Close() + + columns := make(map[int]string) + for rows.Next() { + var taxID int + var name string + err = rows.Scan(&taxID, &name) + if err != nil { + panic(err) + } + + columns[taxID] = name + } + return columns +} + +type taxMap map[int]string + +func mustCollectInvoiceEntriesTaxes(ctx context.Context, conn *Conn, entries []*InvoiceEntry) map[int]taxMap { + ids := mustMakeIDArray(entries, func(entry *InvoiceEntry) int { + return entry.ID + }) + return mustMakeTaxMap(ctx, conn, ids, ` + select invoice_id + , tax_id + , to_price(amount, decimal_digits) + from invoice_tax_amount + join invoice using (invoice_id) + join currency using (currency_code) + where invoice_id = any ($1) + `) +} + +func mustMakeIDArray[T any](entries []*T, id func(entry *T) int) *pgtype.Int4Array { + ids := make([]int, len(entries)) + i := 0 + for _, entry := range entries { + ids[i] = id(entry) + i++ + } + idArray := &pgtype.Int4Array{} + if err := idArray.Set(ids); err != nil { + panic(err) + } + return idArray +} + +func mustMakeTaxMap(ctx context.Context, conn *Conn, ids *pgtype.Int4Array, sql string) map[int]taxMap { + rows, err := conn.Query(ctx, sql, ids) + if err != nil { + panic(err) + } + defer rows.Close() + + taxes := make(map[int]taxMap) + for rows.Next() { + var entryID int + var taxID int + var amount string + err := rows.Scan(&entryID, &taxID, &amount) + if err != nil { + panic(err) + } + + entryTaxes := taxes[entryID] + if entryTaxes == nil { + entryTaxes = make(taxMap) + taxes[entryID] = entryTaxes + } + entryTaxes[taxID] = amount + } + if rows.Err() != nil { + panic(rows.Err()) + } + return taxes +} + func mustWriteInvoicesPdf(r *http.Request, slugs []string) []byte { conn := getConn(r) company := mustGetCompany(r) diff --git a/pkg/ods.go b/pkg/ods.go index ca5e27c..d6ac517 100644 --- a/pkg/ods.go +++ b/pkg/ods.go @@ -6,6 +6,7 @@ import ( "encoding/xml" "fmt" "net/http" + "sort" "strings" "time" ) @@ -41,22 +42,39 @@ const ( ` ) -func mustWriteInvoicesOds(invoices []*InvoiceEntry, locale *Locale, company *Company) []byte { - columns := []string{ - "Date", - "Invoice Num.", - "Customer", - "Status", - "Tags", - "Amount", +func extractTaxIDs(taxColumns map[int]string) []int { + taxIDs := make([]int, len(taxColumns)) + i := 0 + for k := range taxColumns { + taxIDs[i] = k + i++ } + sort.Ints(taxIDs[:]) + return taxIDs +} + +func mustWriteInvoicesOds(invoices []*InvoiceEntry, taxes map[int]taxMap, taxColumns map[int]string, locale *Locale, company *Company) []byte { + taxIDs := extractTaxIDs(taxColumns) + columns := make([]string, 6+len(taxIDs)) + columns[0] = "Date" + columns[1] = "Invoice Num." + columns[2] = "Customer" + columns[3] = "Status" + i := 4 + for _, taxID := range taxIDs { + columns[i] = taxColumns[taxID] + i++ + } + columns[i] = "Amount" + columns[i+1] = "Tags" return mustWriteTableOds(invoices, columns, locale, func(sb *strings.Builder, invoice *InvoiceEntry) { writeCellDate(sb, invoice.Date) writeCellString(sb, invoice.Number) writeCellString(sb, invoice.CustomerName) writeCellString(sb, invoice.StatusLabel) - writeCellString(sb, strings.Join(invoice.Tags, ",")) + writeTaxes(sb, taxes[invoice.ID], taxIDs, locale, company) writeCellFloat(sb, invoice.Total, locale, company) + writeCellString(sb, strings.Join(invoice.Tags, ",")) }) } @@ -79,22 +97,31 @@ func mustWriteQuotesOds(quotes []*QuoteEntry, locale *Locale, company *Company) }) } -func mustWriteExpensesOds(expenses []*ExpenseEntry, locale *Locale, company *Company) []byte { - columns := []string{ - "Contact", - "Invoice Date", - "Invoice Number", - "Status", - "Tags", - "Amount", +func mustWriteExpensesOds(expenses []*ExpenseEntry, taxes map[int]taxMap, taxColumns map[int]string, locale *Locale, company *Company) []byte { + taxIDs := extractTaxIDs(taxColumns) + columns := make([]string, 7+len(taxIDs)) + columns[0] = "Contact" + columns[1] = "Invoice Date" + columns[2] = "Invoice Number" + columns[3] = "Status" + columns[4] = "Amount" + i := 5 + for _, taxID := range taxIDs { + columns[i] = taxColumns[taxID] + i++ } + columns[i] = "Total" + columns[i+1] = "Tags" return mustWriteTableOds(expenses, columns, locale, func(sb *strings.Builder, expense *ExpenseEntry) { + writeCellString(sb, expense.InvoicerName) writeCellDate(sb, expense.InvoiceDate) writeCellString(sb, expense.InvoiceNumber) writeCellString(sb, expense.StatusLabel) - writeCellString(sb, strings.Join(expense.Tags, ",")) writeCellFloat(sb, expense.Amount, locale, company) + writeTaxes(sb, taxes[expense.ID], taxIDs, locale, company) + writeCellFloat(sb, expense.Total, locale, company) + writeCellString(sb, strings.Join(expense.Tags, ",")) }) } @@ -211,3 +238,13 @@ func writeOdsResponse(w http.ResponseWriter, ods []byte, filename string) { panic(err) } } + +func writeTaxes(sb *strings.Builder, taxes taxMap, taxIDs []int, locale *Locale, company *Company) { + for _, taxID := range taxIDs { + var amount string + if taxes != nil { + amount = taxes[taxID] + } + writeCellFloat(sb, amount, locale, company) + } +} diff --git a/po/ca.po b/po/ca.po index 0fec549..5e89ede 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: 2024-01-19 22:33+0100\n" +"POT-Creation-Date: 2024-01-26 02:25+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -288,34 +288,34 @@ msgctxt "title" msgid "Edit Invoice “%s”" msgstr "Edició de la factura «%s»" -#: web/template/web.gohtml:14 +#: web/template/web.gohtml:15 msgctxt "link" msgid "Login" msgstr "Entrada" -#: web/template/web.gohtml:15 +#: web/template/web.gohtml:16 msgctxt "link" msgid "Demo" msgstr "Demo" -#: web/template/web.gohtml:16 +#: web/template/web.gohtml:17 msgctxt "link" msgid "Code" msgstr "Codi" -#: web/template/web.gohtml:27 web/template/legal.gohtml:2 +#: web/template/web.gohtml:28 web/template/legal.gohtml:2 #: web/template/legal.gohtml:7 msgctxt "title" msgid "Legal Disclaimer" msgstr "Avís legal" -#: web/template/web.gohtml:28 web/template/privacy.gohtml:2 +#: web/template/web.gohtml:29 web/template/privacy.gohtml:2 #: web/template/privacy.gohtml:7 msgctxt "title" msgid "Privacy Policy" msgstr "Política de privacitat" -#: web/template/web.gohtml:29 web/template/cookies.gohtml:2 +#: web/template/web.gohtml:30 web/template/cookies.gohtml:2 #: web/template/cookies.gohtml:7 msgctxt "title" msgid "Cookies Policy" @@ -741,100 +741,100 @@ msgctxt "input" msgid "Password" msgstr "Contrasenya" -#: pkg/login.go:70 pkg/company.go:283 pkg/profile.go:89 +#: pkg/login.go:75 pkg/company.go:283 pkg/profile.go:89 msgid "Email can not be empty." msgstr "No podeu deixar el correu-e en blanc." -#: pkg/login.go:71 pkg/company.go:284 pkg/profile.go:90 pkg/contacts.go:420 +#: pkg/login.go:76 pkg/company.go:284 pkg/profile.go:90 pkg/contacts.go:420 msgid "This value is not a valid email. It should be like name@domain.com." msgstr "Aquest valor no és un correu-e vàlid. Hauria de ser similar a nom@domini.cat." -#: pkg/login.go:73 +#: pkg/login.go:78 msgid "Password can not be empty." msgstr "No podeu deixar la contrasenya en blanc." -#: pkg/login.go:109 +#: pkg/login.go:114 msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." #: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 -#: pkg/invoices.go:1016 pkg/contacts.go:149 pkg/contacts.go:262 +#: pkg/invoices.go:1109 pkg/contacts.go:149 pkg/contacts.go:262 msgctxt "input" msgid "Name" msgstr "Nom" #: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708 -#: pkg/expenses.go:340 pkg/expenses.go:508 pkg/invoices.go:174 -#: pkg/invoices.go:746 pkg/invoices.go:1331 pkg/contacts.go:154 +#: pkg/expenses.go:343 pkg/expenses.go:511 pkg/invoices.go:177 +#: pkg/invoices.go:839 pkg/invoices.go:1424 pkg/contacts.go:154 #: pkg/contacts.go:362 msgctxt "input" msgid "Tags" msgstr "Etiquetes" -#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:518 pkg/invoices.go:178 +#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:521 pkg/invoices.go:181 #: pkg/contacts.go:158 msgctxt "input" msgid "Tags Condition" msgstr "Condició de les etiquetes" -#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:522 pkg/invoices.go:182 +#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:525 pkg/invoices.go:185 #: pkg/contacts.go:162 msgctxt "tag condition" msgid "All" msgstr "Totes" -#: pkg/products.go:186 pkg/expenses.go:523 pkg/invoices.go:183 +#: pkg/products.go:186 pkg/expenses.go:526 pkg/invoices.go:186 #: pkg/contacts.go:163 msgid "Invoices must have all the specified labels." msgstr "Les factures han de tenir totes les etiquetes." -#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:527 pkg/invoices.go:187 +#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:530 pkg/invoices.go:190 #: pkg/contacts.go:167 msgctxt "tag condition" msgid "Any" msgstr "Qualsevol" -#: pkg/products.go:191 pkg/expenses.go:528 pkg/invoices.go:188 +#: pkg/products.go:191 pkg/expenses.go:531 pkg/invoices.go:191 #: pkg/contacts.go:168 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:282 pkg/quote.go:915 pkg/invoices.go:1030 +#: pkg/products.go:282 pkg/quote.go:915 pkg/invoices.go:1123 msgctxt "input" msgid "Description" msgstr "Descripció" -#: pkg/products.go:287 pkg/quote.go:919 pkg/invoices.go:1034 +#: pkg/products.go:287 pkg/quote.go:919 pkg/invoices.go:1127 msgctxt "input" msgid "Price" msgstr "Preu" -#: pkg/products.go:297 pkg/quote.go:948 pkg/expenses.go:308 -#: pkg/invoices.go:1063 +#: pkg/products.go:297 pkg/quote.go:948 pkg/expenses.go:311 +#: pkg/invoices.go:1156 msgctxt "input" msgid "Taxes" msgstr "Imposts" -#: pkg/products.go:322 pkg/quote.go:997 pkg/profile.go:92 pkg/invoices.go:1112 +#: pkg/products.go:322 pkg/quote.go:997 pkg/profile.go:92 pkg/invoices.go:1205 #: pkg/contacts.go:412 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." -#: pkg/products.go:323 pkg/quote.go:998 pkg/invoices.go:1113 +#: pkg/products.go:323 pkg/quote.go:998 pkg/invoices.go:1206 msgid "Price can not be empty." msgstr "No podeu deixar el preu en blanc." -#: pkg/products.go:324 pkg/quote.go:999 pkg/invoices.go:1114 +#: pkg/products.go:324 pkg/quote.go:999 pkg/invoices.go:1207 msgid "Price must be a number greater than zero." msgstr "El preu ha de ser un número major a zero." -#: pkg/products.go:326 pkg/quote.go:1007 pkg/expenses.go:376 -#: pkg/invoices.go:1122 +#: pkg/products.go:326 pkg/quote.go:1007 pkg/expenses.go:379 +#: pkg/invoices.go:1215 msgid "Selected tax is not valid." msgstr "Heu seleccionat un impost que no és vàlid." -#: pkg/products.go:327 pkg/quote.go:1008 pkg/expenses.go:377 -#: pkg/invoices.go:1123 +#: pkg/products.go:327 pkg/quote.go:1008 pkg/expenses.go:380 +#: pkg/invoices.go:1216 msgid "You can only select a tax of each class." msgstr "Només podeu seleccionar un impost de cada classe." @@ -1043,12 +1043,12 @@ 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:686 pkg/invoices.go:147 pkg/invoices.go:729 +#: pkg/quote.go:147 pkg/quote.go:686 pkg/invoices.go:150 pkg/invoices.go:822 msgctxt "input" msgid "Customer" msgstr "Client" -#: pkg/quote.go:148 pkg/invoices.go:148 +#: pkg/quote.go:148 pkg/invoices.go:151 msgid "All customers" msgstr "Tots els clients" @@ -1057,7 +1057,7 @@ msgctxt "input" msgid "Quotation Status" msgstr "Estat del pressupost" -#: pkg/quote.go:154 pkg/expenses.go:513 pkg/invoices.go:154 +#: pkg/quote.go:154 pkg/expenses.go:516 pkg/invoices.go:157 msgid "All status" msgstr "Tots els estats" @@ -1066,12 +1066,12 @@ msgctxt "input" msgid "Quotation Number" msgstr "Número de pressupost" -#: pkg/quote.go:164 pkg/expenses.go:498 pkg/invoices.go:164 +#: pkg/quote.go:164 pkg/expenses.go:501 pkg/invoices.go:167 msgctxt "input" msgid "From Date" msgstr "A partir de la data" -#: pkg/quote.go:169 pkg/expenses.go:503 pkg/invoices.go:169 +#: pkg/quote.go:169 pkg/expenses.go:506 pkg/invoices.go:172 msgctxt "input" msgid "To Date" msgstr "Fins la data" @@ -1092,9 +1092,9 @@ msgstr "pressuposts.zip" msgid "quotations.ods" msgstr "pressuposts.ods" -#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:717 -#: pkg/expenses.go:743 pkg/invoices.go:677 pkg/invoices.go:1306 -#: pkg/invoices.go:1314 +#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:720 +#: pkg/expenses.go:749 pkg/invoices.go:683 pkg/invoices.go:1399 +#: pkg/invoices.go:1407 msgid "Invalid action" msgstr "Acció invàlida." @@ -1112,12 +1112,12 @@ msgctxt "input" msgid "Terms and conditions" msgstr "Condicions d’acceptació" -#: pkg/quote.go:703 pkg/invoices.go:741 +#: pkg/quote.go:703 pkg/invoices.go:834 msgctxt "input" msgid "Notes" msgstr "Notes" -#: pkg/quote.go:712 pkg/invoices.go:751 +#: pkg/quote.go:712 pkg/invoices.go:844 msgctxt "input" msgid "Payment Method" msgstr "Mètode de pagament" @@ -1130,7 +1130,7 @@ msgstr "Escolliu un mètode de pagament." msgid "Selected quotation status is not valid." msgstr "Heu seleccionat un estat de pressupost que no és vàlid." -#: pkg/quote.go:751 pkg/invoices.go:806 +#: pkg/quote.go:751 pkg/invoices.go:899 msgid "Selected customer is not valid." msgstr "Heu seleccionat un client que no és vàlid." @@ -1142,21 +1142,21 @@ msgstr "No podeu deixar la data del pressupost en blanc." msgid "Quotation date must be a valid date." msgstr "La data del pressupost ha de ser vàlida." -#: pkg/quote.go:757 pkg/invoices.go:810 +#: pkg/quote.go:757 pkg/invoices.go:903 msgid "Selected payment method is not valid." msgstr "Heu seleccionat un mètode de pagament que no és vàlid." -#: pkg/quote.go:891 pkg/quote.go:896 pkg/invoices.go:1006 pkg/invoices.go:1011 +#: pkg/quote.go:891 pkg/quote.go:896 pkg/invoices.go:1099 pkg/invoices.go:1104 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/quote.go:929 pkg/invoices.go:1044 +#: pkg/quote.go:929 pkg/invoices.go:1137 msgctxt "input" msgid "Quantity" msgstr "Quantitat" -#: pkg/quote.go:938 pkg/invoices.go:1053 +#: pkg/quote.go:938 pkg/invoices.go:1146 msgctxt "input" msgid "Discount (%)" msgstr "Descompte (%)" @@ -1165,23 +1165,23 @@ msgstr "Descompte (%)" 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:995 pkg/invoices.go:1110 +#: pkg/quote.go:995 pkg/invoices.go:1203 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:1001 pkg/invoices.go:1116 +#: pkg/quote.go:1001 pkg/invoices.go:1209 msgid "Quantity can not be empty." msgstr "No podeu deixar la quantitat en blanc." -#: pkg/quote.go:1002 pkg/invoices.go:1117 +#: pkg/quote.go:1002 pkg/invoices.go:1210 msgid "Quantity must be a number greater than zero." msgstr "La quantitat ha de ser un número major a zero." -#: pkg/quote.go:1004 pkg/invoices.go:1119 +#: pkg/quote.go:1004 pkg/invoices.go:1212 msgid "Discount can not be empty." msgstr "No podeu deixar el descompte en blanc." -#: pkg/quote.go:1005 pkg/invoices.go:1120 +#: pkg/quote.go:1005 pkg/invoices.go:1213 msgid "Discount must be a percentage between 0 and 100." msgstr "El descompte ha de ser un percentatge entre 0 i 100." @@ -1248,109 +1248,109 @@ msgctxt "period option" msgid "Previous year" msgstr "Any anterior" -#: pkg/expenses.go:234 +#: pkg/expenses.go:237 msgid "Select a contact." msgstr "Escolliu un contacte." -#: pkg/expenses.go:291 pkg/expenses.go:487 +#: pkg/expenses.go:294 pkg/expenses.go:490 msgctxt "input" msgid "Contact" msgstr "Contacte" -#: pkg/expenses.go:297 +#: pkg/expenses.go:300 msgctxt "input" msgid "Invoice number" msgstr "Número de factura" -#: pkg/expenses.go:302 pkg/invoices.go:735 +#: pkg/expenses.go:305 pkg/invoices.go:828 msgctxt "input" msgid "Invoice Date" msgstr "Data de factura" -#: pkg/expenses.go:317 +#: pkg/expenses.go:320 msgctxt "input" msgid "Amount" msgstr "Import" -#: pkg/expenses.go:328 pkg/invoices.go:757 +#: pkg/expenses.go:331 pkg/invoices.go:850 msgctxt "input" msgid "File" msgstr "Fitxer" -#: pkg/expenses.go:334 pkg/expenses.go:512 +#: pkg/expenses.go:337 pkg/expenses.go:515 msgctxt "input" msgid "Expense Status" msgstr "Estat de la despesa" -#: pkg/expenses.go:374 +#: pkg/expenses.go:377 msgid "Selected contact is not valid." msgstr "Heu seleccionat un contacte que no és vàlid." -#: pkg/expenses.go:375 pkg/invoices.go:808 +#: pkg/expenses.go:378 pkg/invoices.go:901 msgid "Invoice date must be a valid date." msgstr "La data de facturació ha de ser vàlida." -#: pkg/expenses.go:378 +#: pkg/expenses.go:381 msgid "Amount can not be empty." msgstr "No podeu deixar l’import en blanc." -#: pkg/expenses.go:379 +#: pkg/expenses.go:382 msgid "Amount must be a number greater than zero." msgstr "L’import ha de ser un número major a zero." -#: pkg/expenses.go:381 +#: pkg/expenses.go:384 msgid "Selected expense status is not valid." msgstr "Heu seleccionat un estat de despesa que no és vàlid." -#: pkg/expenses.go:488 +#: pkg/expenses.go:491 msgid "All contacts" msgstr "Tots els contactes" -#: pkg/expenses.go:493 pkg/invoices.go:159 +#: pkg/expenses.go:496 pkg/invoices.go:162 msgctxt "input" msgid "Invoice Number" msgstr "Número de factura" -#: pkg/expenses.go:741 +#: pkg/expenses.go:747 msgid "expenses.ods" msgstr "despeses.ods" -#: pkg/invoices.go:153 pkg/invoices.go:723 +#: pkg/invoices.go:156 pkg/invoices.go:816 msgctxt "input" msgid "Invoice Status" msgstr "Estat de la factura" -#: pkg/invoices.go:557 +#: pkg/invoices.go:560 msgid "Select a customer to bill." msgstr "Escolliu un client a facturar." -#: pkg/invoices.go:661 +#: pkg/invoices.go:664 msgid "invoices.zip" msgstr "factures.zip" -#: pkg/invoices.go:675 +#: pkg/invoices.go:681 msgid "invoices.ods" msgstr "factures.ods" -#: pkg/invoices.go:805 +#: pkg/invoices.go:898 msgid "Selected invoice status is not valid." msgstr "Heu seleccionat un estat de factura que no és vàlid." -#: pkg/invoices.go:807 +#: pkg/invoices.go:900 msgid "Invoice date can not be empty." msgstr "No podeu deixar la data de la factura en blanc." -#: pkg/invoices.go:943 +#: pkg/invoices.go:1036 #, c-format msgid "Re: quotation #%s of %s" msgstr "Ref: pressupost núm. %s del %s" -#: pkg/invoices.go:944 +#: pkg/invoices.go:1037 msgctxt "to_char" msgid "MM/DD/YYYY" msgstr "DD/MM/YYYY" -#: pkg/invoices.go:1107 +#: pkg/invoices.go:1200 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 4ecd313..7599fd0 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: 2024-01-19 22:33+0100\n" +"POT-Creation-Date: 2024-01-26 02:25+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -288,34 +288,34 @@ msgctxt "title" msgid "Edit Invoice “%s”" msgstr "Edición de la factura «%s»" -#: web/template/web.gohtml:14 +#: web/template/web.gohtml:15 msgctxt "link" msgid "Login" msgstr "Entrada" -#: web/template/web.gohtml:15 +#: web/template/web.gohtml:16 msgctxt "link" msgid "Demo" msgstr "Demo" -#: web/template/web.gohtml:16 +#: web/template/web.gohtml:17 msgctxt "link" msgid "Code" msgstr "Código" -#: web/template/web.gohtml:27 web/template/legal.gohtml:2 +#: web/template/web.gohtml:28 web/template/legal.gohtml:2 #: web/template/legal.gohtml:7 msgctxt "title" msgid "Legal Disclaimer" msgstr "Aviso legal" -#: web/template/web.gohtml:28 web/template/privacy.gohtml:2 +#: web/template/web.gohtml:29 web/template/privacy.gohtml:2 #: web/template/privacy.gohtml:7 msgctxt "title" msgid "Privacy Policy" msgstr "Política de privacidad" -#: web/template/web.gohtml:29 web/template/cookies.gohtml:2 +#: web/template/web.gohtml:30 web/template/cookies.gohtml:2 #: web/template/cookies.gohtml:7 msgctxt "title" msgid "Cookies Policy" @@ -741,100 +741,100 @@ msgctxt "input" msgid "Password" msgstr "Contraseña" -#: pkg/login.go:70 pkg/company.go:283 pkg/profile.go:89 +#: pkg/login.go:75 pkg/company.go:283 pkg/profile.go:89 msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." -#: pkg/login.go:71 pkg/company.go:284 pkg/profile.go:90 pkg/contacts.go:420 +#: pkg/login.go:76 pkg/company.go:284 pkg/profile.go:90 pkg/contacts.go:420 msgid "This value is not a valid email. It should be like name@domain.com." msgstr "Este valor no es un correo-e válido. Tiene que ser parecido a nombre@dominio.es." -#: pkg/login.go:73 +#: pkg/login.go:78 msgid "Password can not be empty." msgstr "No podéis dejar la contraseña en blanco." -#: pkg/login.go:109 +#: pkg/login.go:114 msgid "Invalid user or password." msgstr "Nombre de usuario o contraseña inválido." #: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 -#: pkg/invoices.go:1016 pkg/contacts.go:149 pkg/contacts.go:262 +#: pkg/invoices.go:1109 pkg/contacts.go:149 pkg/contacts.go:262 msgctxt "input" msgid "Name" msgstr "Nombre" #: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708 -#: pkg/expenses.go:340 pkg/expenses.go:508 pkg/invoices.go:174 -#: pkg/invoices.go:746 pkg/invoices.go:1331 pkg/contacts.go:154 +#: pkg/expenses.go:343 pkg/expenses.go:511 pkg/invoices.go:177 +#: pkg/invoices.go:839 pkg/invoices.go:1424 pkg/contacts.go:154 #: pkg/contacts.go:362 msgctxt "input" msgid "Tags" msgstr "Etiquetes" -#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:518 pkg/invoices.go:178 +#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:521 pkg/invoices.go:181 #: pkg/contacts.go:158 msgctxt "input" msgid "Tags Condition" msgstr "Condición de las etiquetas" -#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:522 pkg/invoices.go:182 +#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:525 pkg/invoices.go:185 #: pkg/contacts.go:162 msgctxt "tag condition" msgid "All" msgstr "Todas" -#: pkg/products.go:186 pkg/expenses.go:523 pkg/invoices.go:183 +#: pkg/products.go:186 pkg/expenses.go:526 pkg/invoices.go:186 #: pkg/contacts.go:163 msgid "Invoices must have all the specified labels." msgstr "Las facturas deben tener todas las etiquetas." -#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:527 pkg/invoices.go:187 +#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:530 pkg/invoices.go:190 #: pkg/contacts.go:167 msgctxt "tag condition" msgid "Any" msgstr "Cualquiera" -#: pkg/products.go:191 pkg/expenses.go:528 pkg/invoices.go:188 +#: pkg/products.go:191 pkg/expenses.go:531 pkg/invoices.go:191 #: pkg/contacts.go:168 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:282 pkg/quote.go:915 pkg/invoices.go:1030 +#: pkg/products.go:282 pkg/quote.go:915 pkg/invoices.go:1123 msgctxt "input" msgid "Description" msgstr "Descripción" -#: pkg/products.go:287 pkg/quote.go:919 pkg/invoices.go:1034 +#: pkg/products.go:287 pkg/quote.go:919 pkg/invoices.go:1127 msgctxt "input" msgid "Price" msgstr "Precio" -#: pkg/products.go:297 pkg/quote.go:948 pkg/expenses.go:308 -#: pkg/invoices.go:1063 +#: pkg/products.go:297 pkg/quote.go:948 pkg/expenses.go:311 +#: pkg/invoices.go:1156 msgctxt "input" msgid "Taxes" msgstr "Impuestos" -#: pkg/products.go:322 pkg/quote.go:997 pkg/profile.go:92 pkg/invoices.go:1112 +#: pkg/products.go:322 pkg/quote.go:997 pkg/profile.go:92 pkg/invoices.go:1205 #: pkg/contacts.go:412 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." -#: pkg/products.go:323 pkg/quote.go:998 pkg/invoices.go:1113 +#: pkg/products.go:323 pkg/quote.go:998 pkg/invoices.go:1206 msgid "Price can not be empty." msgstr "No podéis dejar el precio en blanco." -#: pkg/products.go:324 pkg/quote.go:999 pkg/invoices.go:1114 +#: pkg/products.go:324 pkg/quote.go:999 pkg/invoices.go:1207 msgid "Price must be a number greater than zero." msgstr "El precio tiene que ser un número mayor a cero." -#: pkg/products.go:326 pkg/quote.go:1007 pkg/expenses.go:376 -#: pkg/invoices.go:1122 +#: pkg/products.go:326 pkg/quote.go:1007 pkg/expenses.go:379 +#: pkg/invoices.go:1215 msgid "Selected tax is not valid." msgstr "Habéis escogido un impuesto que no es válido." -#: pkg/products.go:327 pkg/quote.go:1008 pkg/expenses.go:377 -#: pkg/invoices.go:1123 +#: pkg/products.go:327 pkg/quote.go:1008 pkg/expenses.go:380 +#: pkg/invoices.go:1216 msgid "You can only select a tax of each class." msgstr "Solo podéis escoger un impuesto de cada clase." @@ -1043,12 +1043,12 @@ 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:686 pkg/invoices.go:147 pkg/invoices.go:729 +#: pkg/quote.go:147 pkg/quote.go:686 pkg/invoices.go:150 pkg/invoices.go:822 msgctxt "input" msgid "Customer" msgstr "Cliente" -#: pkg/quote.go:148 pkg/invoices.go:148 +#: pkg/quote.go:148 pkg/invoices.go:151 msgid "All customers" msgstr "Todos los clientes" @@ -1057,7 +1057,7 @@ msgctxt "input" msgid "Quotation Status" msgstr "Estado del presupuesto" -#: pkg/quote.go:154 pkg/expenses.go:513 pkg/invoices.go:154 +#: pkg/quote.go:154 pkg/expenses.go:516 pkg/invoices.go:157 msgid "All status" msgstr "Todos los estados" @@ -1066,12 +1066,12 @@ msgctxt "input" msgid "Quotation Number" msgstr "Número de presupuesto" -#: pkg/quote.go:164 pkg/expenses.go:498 pkg/invoices.go:164 +#: pkg/quote.go:164 pkg/expenses.go:501 pkg/invoices.go:167 msgctxt "input" msgid "From Date" msgstr "A partir de la fecha" -#: pkg/quote.go:169 pkg/expenses.go:503 pkg/invoices.go:169 +#: pkg/quote.go:169 pkg/expenses.go:506 pkg/invoices.go:172 msgctxt "input" msgid "To Date" msgstr "Hasta la fecha" @@ -1092,9 +1092,9 @@ msgstr "presupuestos.zip" msgid "quotations.ods" msgstr "presupuestos.ods" -#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:717 -#: pkg/expenses.go:743 pkg/invoices.go:677 pkg/invoices.go:1306 -#: pkg/invoices.go:1314 +#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:720 +#: pkg/expenses.go:749 pkg/invoices.go:683 pkg/invoices.go:1399 +#: pkg/invoices.go:1407 msgid "Invalid action" msgstr "Acción inválida." @@ -1112,12 +1112,12 @@ msgctxt "input" msgid "Terms and conditions" msgstr "Condiciones de aceptación" -#: pkg/quote.go:703 pkg/invoices.go:741 +#: pkg/quote.go:703 pkg/invoices.go:834 msgctxt "input" msgid "Notes" msgstr "Notas" -#: pkg/quote.go:712 pkg/invoices.go:751 +#: pkg/quote.go:712 pkg/invoices.go:844 msgctxt "input" msgid "Payment Method" msgstr "Método de pago" @@ -1130,7 +1130,7 @@ msgstr "Escoged un método e pago." msgid "Selected quotation status is not valid." msgstr "Habéis escogido un estado de presupuesto que no es válido." -#: pkg/quote.go:751 pkg/invoices.go:806 +#: pkg/quote.go:751 pkg/invoices.go:899 msgid "Selected customer is not valid." msgstr "Habéis escogido un cliente que no es válido." @@ -1142,21 +1142,21 @@ msgstr "No podéis dejar la fecha del presupuesto en blanco." msgid "Quotation date must be a valid date." msgstr "La fecha de presupuesto debe ser válida." -#: pkg/quote.go:757 pkg/invoices.go:810 +#: pkg/quote.go:757 pkg/invoices.go:903 msgid "Selected payment method is not valid." msgstr "Habéis escogido un método de pago que no es válido." -#: pkg/quote.go:891 pkg/quote.go:896 pkg/invoices.go:1006 pkg/invoices.go:1011 +#: pkg/quote.go:891 pkg/quote.go:896 pkg/invoices.go:1099 pkg/invoices.go:1104 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/quote.go:929 pkg/invoices.go:1044 +#: pkg/quote.go:929 pkg/invoices.go:1137 msgctxt "input" msgid "Quantity" msgstr "Cantidad" -#: pkg/quote.go:938 pkg/invoices.go:1053 +#: pkg/quote.go:938 pkg/invoices.go:1146 msgctxt "input" msgid "Discount (%)" msgstr "Descuento (%)" @@ -1165,23 +1165,23 @@ msgstr "Descuento (%)" 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:995 pkg/invoices.go:1110 +#: pkg/quote.go:995 pkg/invoices.go:1203 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:1001 pkg/invoices.go:1116 +#: pkg/quote.go:1001 pkg/invoices.go:1209 msgid "Quantity can not be empty." msgstr "No podéis dejar la cantidad en blanco." -#: pkg/quote.go:1002 pkg/invoices.go:1117 +#: pkg/quote.go:1002 pkg/invoices.go:1210 msgid "Quantity must be a number greater than zero." msgstr "La cantidad tiene que ser un número mayor a cero." -#: pkg/quote.go:1004 pkg/invoices.go:1119 +#: pkg/quote.go:1004 pkg/invoices.go:1212 msgid "Discount can not be empty." msgstr "No podéis dejar el descuento en blanco." -#: pkg/quote.go:1005 pkg/invoices.go:1120 +#: pkg/quote.go:1005 pkg/invoices.go:1213 msgid "Discount must be a percentage between 0 and 100." msgstr "El descuento tiene que ser un porcentaje entre 0 y 100." @@ -1248,109 +1248,109 @@ msgctxt "period option" msgid "Previous year" msgstr "Año anterior" -#: pkg/expenses.go:234 +#: pkg/expenses.go:237 msgid "Select a contact." msgstr "Escoged un contacto" -#: pkg/expenses.go:291 pkg/expenses.go:487 +#: pkg/expenses.go:294 pkg/expenses.go:490 msgctxt "input" msgid "Contact" msgstr "Contacto" -#: pkg/expenses.go:297 +#: pkg/expenses.go:300 msgctxt "input" msgid "Invoice number" msgstr "Número de factura" -#: pkg/expenses.go:302 pkg/invoices.go:735 +#: pkg/expenses.go:305 pkg/invoices.go:828 msgctxt "input" msgid "Invoice Date" msgstr "Fecha de factura" -#: pkg/expenses.go:317 +#: pkg/expenses.go:320 msgctxt "input" msgid "Amount" msgstr "Importe" -#: pkg/expenses.go:328 pkg/invoices.go:757 +#: pkg/expenses.go:331 pkg/invoices.go:850 msgctxt "input" msgid "File" msgstr "Archivo" -#: pkg/expenses.go:334 pkg/expenses.go:512 +#: pkg/expenses.go:337 pkg/expenses.go:515 msgctxt "input" msgid "Expense Status" msgstr "Estado del gasto" -#: pkg/expenses.go:374 +#: pkg/expenses.go:377 msgid "Selected contact is not valid." msgstr "Habéis escogido un contacto que no es válido." -#: pkg/expenses.go:375 pkg/invoices.go:808 +#: pkg/expenses.go:378 pkg/invoices.go:901 msgid "Invoice date must be a valid date." msgstr "La fecha de factura debe ser válida." -#: pkg/expenses.go:378 +#: pkg/expenses.go:381 msgid "Amount can not be empty." msgstr "No podéis dejar el importe en blanco." -#: pkg/expenses.go:379 +#: pkg/expenses.go:382 msgid "Amount must be a number greater than zero." msgstr "El importe tiene que ser un número mayor a cero." -#: pkg/expenses.go:381 +#: pkg/expenses.go:384 msgid "Selected expense status is not valid." msgstr "Habéis escogido un estado de gasto que no es válido." -#: pkg/expenses.go:488 +#: pkg/expenses.go:491 msgid "All contacts" msgstr "Todos los contactos" -#: pkg/expenses.go:493 pkg/invoices.go:159 +#: pkg/expenses.go:496 pkg/invoices.go:162 msgctxt "input" msgid "Invoice Number" msgstr "Número de factura" -#: pkg/expenses.go:741 +#: pkg/expenses.go:747 msgid "expenses.ods" msgstr "gastos.ods" -#: pkg/invoices.go:153 pkg/invoices.go:723 +#: pkg/invoices.go:156 pkg/invoices.go:816 msgctxt "input" msgid "Invoice Status" msgstr "Estado de la factura" -#: pkg/invoices.go:557 +#: pkg/invoices.go:560 msgid "Select a customer to bill." msgstr "Escoged un cliente a facturar." -#: pkg/invoices.go:661 +#: pkg/invoices.go:664 msgid "invoices.zip" msgstr "facturas.zip" -#: pkg/invoices.go:675 +#: pkg/invoices.go:681 msgid "invoices.ods" msgstr "facturas.ods" -#: pkg/invoices.go:805 +#: pkg/invoices.go:898 msgid "Selected invoice status is not valid." msgstr "Habéis escogido un estado de factura que no es válido." -#: pkg/invoices.go:807 +#: pkg/invoices.go:900 msgid "Invoice date can not be empty." msgstr "No podéis dejar la fecha de la factura en blanco." -#: pkg/invoices.go:943 +#: pkg/invoices.go:1036 #, c-format msgid "Re: quotation #%s of %s" msgstr "Ref: presupuesto n.º %s del %s" -#: pkg/invoices.go:944 +#: pkg/invoices.go:1037 msgctxt "to_char" msgid "MM/DD/YYYY" msgstr "DD/MM/YYYY" -#: pkg/invoices.go:1107 +#: pkg/invoices.go:1200 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."