Add a column for each tax type when exporting invoices and expenses

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
This commit is contained in:
jordi fita mas 2024-01-26 02:29:51 +01:00
parent 6fcc19bebf
commit 65413637ac
5 changed files with 324 additions and 173 deletions

View File

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

View File

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

View File

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

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

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