Add invoice view, with print CSS

Had to group name and description rows in tbody because i do not want
to break them on pagination.

I also could not use tfoot for subtotal, taxes, and total because then
they appear on every page.

The disclaimer should appear only at the very bottom of the last page,
but i do not know how to do that; using position fixed shows it on
every page.
This commit is contained in:
jordi fita mas 2023-02-24 12:22:15 +01:00
parent 985f843e8e
commit 18fba2964f
6 changed files with 421 additions and 72 deletions

View File

@ -64,6 +64,73 @@ func GetInvoiceForm(w http.ResponseWriter, r *http.Request, params httprouter.Pa
mustRenderNewInvoiceForm(w, r, form)
return
}
invoice := mustGetInvoice(r.Context(), conn, company, slug)
if invoice == nil {
http.NotFound(w, r)
return
}
mustRenderAppTemplate(w, r, "invoices/view.gohtml", invoice)
}
type invoice struct {
Number string
Date time.Time
Invoicer taxDetails
Invoicee taxDetails
Notes string
Products []*invoiceProduct
Subtotal string
Taxes [][]string
Total string
}
type taxDetails struct {
Name string
VATIN string
Address string
City string
PostalCode string
Province string
Email string
Phone string
}
type invoiceProduct struct {
Name string
Description string
Price string
Quantity int
Total string
}
func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug string) *invoice {
inv := &invoice{}
var invoiceId int
var decimalDigits int
if notFoundErrorOrPanic(conn.QueryRow(ctx, "select invoice_id, decimal_digits, invoice_number, invoice_date, notes, business_name, vatin, phone, email, address, city, province, postal_code, to_price(subtotal, decimal_digits), to_price(total, decimal_digits) from invoice join contact using (contact_id) join invoice_amount using (invoice_id) join currency using (currency_code) where invoice.slug = $1", slug).Scan(&invoiceId, &decimalDigits, &inv.Number, &inv.Date, &inv.Notes, &inv.Invoicee.Name, &inv.Invoicee.VATIN, &inv.Invoicee.Phone, &inv.Invoicee.Email, &inv.Invoicee.Address, &inv.Invoicee.City, &inv.Invoicee.Province, &inv.Invoicee.PostalCode, &inv.Subtotal, &inv.Total)) {
return nil
}
if err := conn.QueryRow(ctx, "select business_name, vatin, phone, email, address, city, province, postal_code from company where company_id = $1", company.Id).Scan(&inv.Invoicer.Name, &inv.Invoicer.VATIN, &inv.Invoicer.Phone, &inv.Invoicer.Email, &inv.Invoicer.Address, &inv.Invoicer.City, &inv.Invoicer.Province, &inv.Invoicer.PostalCode); err != nil {
panic(err)
}
if err := conn.QueryRow(ctx, "select array_agg(array[name, to_price(amount, $2)]) from invoice_tax_amount join tax using (tax_id) where invoice_id = $1", invoiceId, decimalDigits).Scan(&inv.Taxes); err != nil {
panic(err)
}
rows := conn.MustQuery(ctx, "select name, description, to_price(price, $2), quantity, to_price(round(price * quantity * (1 - discount_rate))::integer, 2) from invoice_product where invoice_id = $1", invoiceId, decimalDigits)
defer rows.Close()
for rows.Next() {
product := &invoiceProduct{}
if err := rows.Scan(&product.Name, &product.Description, &product.Price, &product.Quantity, &product.Total); err != nil {
panic(err)
}
inv.Products = append(inv.Products, product)
}
if rows.Err() != nil {
panic(rows.Err())
}
return inv
}
type newInvoicePage struct {

View File

@ -40,8 +40,8 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
}
return p.Sprintf(locale.CurrencyPattern, company.DecimalDigits, number.Decimal(f), company.CurrencySymbol)
},
"formatDate": func(time time.Time) string {
return time.Format("02/01/2006")
"formatDate": func(time time.Time) template.HTML {
return template.HTML(`<time datetime="` + time.Format("2006-01-02") + `">` + time.Format("02/01/2006") + "</time>")
},
"csrfToken": func() template.HTML {
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, csrfTokenField, user.CsrfToken))

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-02-22 14:35+0100\n"
"POT-Creation-Date: 2023-02-24 11:52+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"
@ -24,17 +24,18 @@ msgid "Add Products to Invoice"
msgstr "Afegeix productes a la factura"
#: web/template/invoices/products.gohtml:9 web/template/invoices/new.gohtml:9
#: web/template/invoices/index.gohtml:8 web/template/contacts/new.gohtml:9
#: web/template/contacts/index.gohtml:8 web/template/contacts/edit.gohtml:9
#: web/template/profile.gohtml:9 web/template/tax-details.gohtml:8
#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:8
#: web/template/products/edit.gohtml:9
#: web/template/invoices/index.gohtml:8 web/template/invoices/view.gohtml:9
#: web/template/contacts/new.gohtml:9 web/template/contacts/index.gohtml:8
#: web/template/contacts/edit.gohtml:9 web/template/profile.gohtml:9
#: web/template/tax-details.gohtml:8 web/template/products/new.gohtml:9
#: web/template/products/index.gohtml:8 web/template/products/edit.gohtml:9
msgctxt "title"
msgid "Home"
msgstr "Inici"
#: web/template/invoices/products.gohtml:10 web/template/invoices/new.gohtml:10
#: web/template/invoices/index.gohtml:2 web/template/invoices/index.gohtml:9
#: web/template/invoices/view.gohtml:10
msgctxt "title"
msgid "Invoices"
msgstr "Factures"
@ -58,7 +59,7 @@ msgid "Name"
msgstr "Nom"
#: web/template/invoices/products.gohtml:43
#: web/template/products/index.gohtml:23
#: web/template/invoices/view.gohtml:42 web/template/products/index.gohtml:23
msgctxt "title"
msgid "Price"
msgstr "Preu"
@ -68,17 +69,28 @@ msgstr "Preu"
msgid "No products added yet."
msgstr "No hi ha cap producte."
#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:38
#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:59
msgctxt "action"
msgid "Add products"
msgstr "Afegeix productes"
#: web/template/invoices/new.gohtml:40
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:44
#: web/template/invoices/view.gohtml:71
msgctxt "title"
msgid "Subtotal"
msgstr "Subtotal"
#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:81
msgctxt "title"
msgid "Total"
msgstr "Total"
#: web/template/invoices/new.gohtml:61
msgctxt "action"
msgid "Update"
msgstr "Actualitza"
#: web/template/invoices/new.gohtml:42 web/template/invoices/index.gohtml:13
#: web/template/invoices/new.gohtml:63 web/template/invoices/index.gohtml:13
msgctxt "action"
msgid "New invoice"
msgstr "Nova factura"
@ -88,7 +100,7 @@ msgctxt "invoice"
msgid "All"
msgstr "Totes"
#: web/template/invoices/index.gohtml:22
#: web/template/invoices/index.gohtml:22 web/template/invoices/view.gohtml:18
msgctxt "title"
msgid "Date"
msgstr "Data"
@ -127,6 +139,21 @@ msgstr "Descàrrega"
msgid "No invoices added yet."
msgstr "No hi ha cap factura."
#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:17
msgctxt "title"
msgid "Invoice %s"
msgstr "Factura %s"
#: web/template/invoices/view.gohtml:41
msgctxt "title"
msgid "Concept"
msgstr "Concepte"
#: web/template/invoices/view.gohtml:43
msgctxt "title"
msgid "Units"
msgstr "Unitats"
#: web/template/dashboard.gohtml:2
msgctxt "title"
msgid "Dashboard"
@ -337,40 +364,40 @@ msgstr "No podeu deixar la contrasenya en blanc."
msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/products.go:165 pkg/invoices.go:304
#: pkg/products.go:165 pkg/invoices.go:392
msgctxt "input"
msgid "Name"
msgstr "Nom"
#: pkg/products.go:171 pkg/invoices.go:309
#: pkg/products.go:171 pkg/invoices.go:397
msgctxt "input"
msgid "Description"
msgstr "Descripció"
#: pkg/products.go:176 pkg/invoices.go:313
#: pkg/products.go:176 pkg/invoices.go:401
msgctxt "input"
msgid "Price"
msgstr "Preu"
#: pkg/products.go:186 pkg/invoices.go:219 pkg/invoices.go:339
#: pkg/products.go:186 pkg/invoices.go:307 pkg/invoices.go:427
msgctxt "input"
msgid "Taxes"
msgstr "Imposts"
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:252
#: pkg/invoices.go:375
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:340
#: pkg/invoices.go:463
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
#: pkg/products.go:207 pkg/invoices.go:376
#: pkg/products.go:207 pkg/invoices.go:464
msgid "Price can not be empty."
msgstr "No podeu deixar el preu en blanc."
#: pkg/products.go:208 pkg/invoices.go:377
#: pkg/products.go:208 pkg/invoices.go:465
msgid "Price must be a number greater than zero."
msgstr "El preu ha de ser un número major a zero."
#: pkg/products.go:210 pkg/invoices.go:256 pkg/invoices.go:385
#: pkg/products.go:210 pkg/invoices.go:344 pkg/invoices.go:473
msgid "Selected tax is not valid."
msgstr "Heu seleccionat un impost que no és vàlid."
@ -433,70 +460,70 @@ msgstr "La confirmació no és igual a la contrasenya."
msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid."
#: pkg/invoices.go:71
#: pkg/invoices.go:158
msgid "Select a customer to bill."
msgstr "Escolliu un client a facturar."
#: pkg/invoices.go:145
#: pkg/invoices.go:233
msgid "Invalid action"
msgstr "Acció invàlida."
#: pkg/invoices.go:196
#: pkg/invoices.go:284
msgctxt "input"
msgid "Customer"
msgstr "Client"
#: pkg/invoices.go:202
#: pkg/invoices.go:290
msgctxt "input"
msgid "Number"
msgstr "Número"
#: pkg/invoices.go:208
#: pkg/invoices.go:296
msgctxt "input"
msgid "Invoice Date"
msgstr "Data de factura"
#: pkg/invoices.go:214
#: pkg/invoices.go:302
msgctxt "input"
msgid "Notes"
msgstr "Notes"
#: pkg/invoices.go:253
#: pkg/invoices.go:341
msgid "Invoice date can not be empty."
msgstr "No podeu deixar la data de la factura en blanc."
#: pkg/invoices.go:254
#: pkg/invoices.go:342
msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida."
#: pkg/invoices.go:299
#: pkg/invoices.go:387
msgctxt "input"
msgid "Id"
msgstr "Identificador"
#: pkg/invoices.go:322
#: pkg/invoices.go:410
msgctxt "input"
msgid "Quantity"
msgstr "Quantitat"
#: pkg/invoices.go:330
#: pkg/invoices.go:418
msgctxt "input"
msgid "Discount (%)"
msgstr "Descompte (%)"
#: pkg/invoices.go:379
#: pkg/invoices.go:467
msgid "Quantity can not be empty."
msgstr "No podeu deixar la quantitat en blanc."
#: pkg/invoices.go:380
#: pkg/invoices.go:468
msgid "Quantity must be a number greater than zero."
msgstr "La quantitat ha de ser un número major a zero."
#: pkg/invoices.go:382
#: pkg/invoices.go:470
msgid "Discount can not be empty."
msgstr "No podeu deixar el descompte en blanc."
#: pkg/invoices.go:383
#: pkg/invoices.go:471
msgid "Discount must be a percentage between 0 and 100."
msgstr "El descompte ha de ser un percentatge entre 0 i 100."

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-02-22 14:35+0100\n"
"POT-Creation-Date: 2023-02-24 11:52+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"
@ -24,17 +24,18 @@ msgid "Add Products to Invoice"
msgstr "Añadir productos a la factura"
#: web/template/invoices/products.gohtml:9 web/template/invoices/new.gohtml:9
#: web/template/invoices/index.gohtml:8 web/template/contacts/new.gohtml:9
#: web/template/contacts/index.gohtml:8 web/template/contacts/edit.gohtml:9
#: web/template/profile.gohtml:9 web/template/tax-details.gohtml:8
#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:8
#: web/template/products/edit.gohtml:9
#: web/template/invoices/index.gohtml:8 web/template/invoices/view.gohtml:9
#: web/template/contacts/new.gohtml:9 web/template/contacts/index.gohtml:8
#: web/template/contacts/edit.gohtml:9 web/template/profile.gohtml:9
#: web/template/tax-details.gohtml:8 web/template/products/new.gohtml:9
#: web/template/products/index.gohtml:8 web/template/products/edit.gohtml:9
msgctxt "title"
msgid "Home"
msgstr "Inicio"
#: web/template/invoices/products.gohtml:10 web/template/invoices/new.gohtml:10
#: web/template/invoices/index.gohtml:2 web/template/invoices/index.gohtml:9
#: web/template/invoices/view.gohtml:10
msgctxt "title"
msgid "Invoices"
msgstr "Facturas"
@ -58,7 +59,7 @@ msgid "Name"
msgstr "Nombre"
#: web/template/invoices/products.gohtml:43
#: web/template/products/index.gohtml:23
#: web/template/invoices/view.gohtml:42 web/template/products/index.gohtml:23
msgctxt "title"
msgid "Price"
msgstr "Precio"
@ -68,17 +69,28 @@ msgstr "Precio"
msgid "No products added yet."
msgstr "No hay productos."
#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:38
#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:59
msgctxt "action"
msgid "Add products"
msgstr "Añadir productos"
#: web/template/invoices/new.gohtml:40
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:44
#: web/template/invoices/view.gohtml:71
msgctxt "title"
msgid "Subtotal"
msgstr "Subtotal"
#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:81
msgctxt "title"
msgid "Total"
msgstr "Total"
#: web/template/invoices/new.gohtml:61
msgctxt "action"
msgid "Update"
msgstr "Actualizar"
#: web/template/invoices/new.gohtml:42 web/template/invoices/index.gohtml:13
#: web/template/invoices/new.gohtml:63 web/template/invoices/index.gohtml:13
msgctxt "action"
msgid "New invoice"
msgstr "Nueva factura"
@ -88,7 +100,7 @@ msgctxt "invoice"
msgid "All"
msgstr "Todas"
#: web/template/invoices/index.gohtml:22
#: web/template/invoices/index.gohtml:22 web/template/invoices/view.gohtml:18
msgctxt "title"
msgid "Date"
msgstr "Fecha"
@ -127,6 +139,21 @@ msgstr "Descargar"
msgid "No invoices added yet."
msgstr "No hay facturas."
#: web/template/invoices/view.gohtml:2 web/template/invoices/view.gohtml:17
msgctxt "title"
msgid "Invoice %s"
msgstr "Factura %s"
#: web/template/invoices/view.gohtml:41
msgctxt "title"
msgid "Concept"
msgstr "Concepto"
#: web/template/invoices/view.gohtml:43
msgctxt "title"
msgid "Units"
msgstr "Unidades"
#: web/template/dashboard.gohtml:2
msgctxt "title"
msgid "Dashboard"
@ -337,40 +364,40 @@ msgstr "No podéis dejar la contraseña en blanco."
msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido."
#: pkg/products.go:165 pkg/invoices.go:304
#: pkg/products.go:165 pkg/invoices.go:392
msgctxt "input"
msgid "Name"
msgstr "Nombre"
#: pkg/products.go:171 pkg/invoices.go:309
#: pkg/products.go:171 pkg/invoices.go:397
msgctxt "input"
msgid "Description"
msgstr "Descripción"
#: pkg/products.go:176 pkg/invoices.go:313
#: pkg/products.go:176 pkg/invoices.go:401
msgctxt "input"
msgid "Price"
msgstr "Precio"
#: pkg/products.go:186 pkg/invoices.go:219 pkg/invoices.go:339
#: pkg/products.go:186 pkg/invoices.go:307 pkg/invoices.go:427
msgctxt "input"
msgid "Taxes"
msgstr "Impuestos"
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:252
#: pkg/invoices.go:375
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:340
#: pkg/invoices.go:463
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
#: pkg/products.go:207 pkg/invoices.go:376
#: pkg/products.go:207 pkg/invoices.go:464
msgid "Price can not be empty."
msgstr "No podéis dejar el precio en blanco."
#: pkg/products.go:208 pkg/invoices.go:377
#: pkg/products.go:208 pkg/invoices.go:465
msgid "Price must be a number greater than zero."
msgstr "El precio tiene que ser un número mayor a cero."
#: pkg/products.go:210 pkg/invoices.go:256 pkg/invoices.go:385
#: pkg/products.go:210 pkg/invoices.go:344 pkg/invoices.go:473
msgid "Selected tax is not valid."
msgstr "Habéis escogido un impuesto que no es válido."
@ -433,70 +460,70 @@ msgstr "La confirmación no corresponde con la contraseña."
msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido."
#: pkg/invoices.go:71
#: pkg/invoices.go:158
msgid "Select a customer to bill."
msgstr "Escoged un cliente a facturar."
#: pkg/invoices.go:145
#: pkg/invoices.go:233
msgid "Invalid action"
msgstr "Acción inválida."
#: pkg/invoices.go:196
#: pkg/invoices.go:284
msgctxt "input"
msgid "Customer"
msgstr "Cliente"
#: pkg/invoices.go:202
#: pkg/invoices.go:290
msgctxt "input"
msgid "Number"
msgstr "Número"
#: pkg/invoices.go:208
#: pkg/invoices.go:296
msgctxt "input"
msgid "Invoice Date"
msgstr "Fecha de factura"
#: pkg/invoices.go:214
#: pkg/invoices.go:302
msgctxt "input"
msgid "Notes"
msgstr "Notas"
#: pkg/invoices.go:253
#: pkg/invoices.go:341
msgid "Invoice date can not be empty."
msgstr "No podéis dejar la fecha de la factura en blanco."
#: pkg/invoices.go:254
#: pkg/invoices.go:342
msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida."
#: pkg/invoices.go:299
#: pkg/invoices.go:387
msgctxt "input"
msgid "Id"
msgstr "Identificador"
#: pkg/invoices.go:322
#: pkg/invoices.go:410
msgctxt "input"
msgid "Quantity"
msgstr "Cantidad"
#: pkg/invoices.go:330
#: pkg/invoices.go:418
msgctxt "input"
msgid "Discount (%)"
msgstr "Descuento (%)"
#: pkg/invoices.go:379
#: pkg/invoices.go:467
msgid "Quantity can not be empty."
msgstr "No podéis dejar la cantidad en blanco."
#: pkg/invoices.go:380
#: pkg/invoices.go:468
msgid "Quantity must be a number greater than zero."
msgstr "La cantidad tiene que ser un número mayor a cero."
#: pkg/invoices.go:382
#: pkg/invoices.go:470
msgid "Discount can not be empty."
msgstr "No podéis dejar el descuento en blanco."
#: pkg/invoices.go:383
#: pkg/invoices.go:471
msgid "Discount must be a percentage between 0 and 100."
msgstr "El descuento tiene que ser un percentage entre 0 y 100."

126
web/static/invoice.css Normal file
View File

@ -0,0 +1,126 @@
.invoice {
display: flex;
gap: 2.5rem;
}
.invoice header {
flex: 1;
min-width: 20rem;
padding: 0 1rem 0 0;
display: block;
background: initial;
border-right: 2px dashed lightgray;
}
.invoice h1 {
font-size: 1.6rem;
}
.invoice .invoicer {
margin-top: 20rem;
}
.invoice > div {
display: flex;
flex-direction: column;
flex: 3;
gap: 5rem;
align-items: end;
}
.invoice th {
font-weight: normal;
text-transform: uppercase;
}
.invoice thead th {
text-align: left;
border-bottom: 2px solid black;
}
.invoice .tfoot th, .invoice .numeric {
text-align: right;
}
.invoice .notes, .invoice footer {
white-space: pre;
text-align: right;
}
.invoice td {
vertical-align: top;
}
.invoice tbody, .invoice footer {
page-break-inside: avoid;
}
.invoice tbody tr {
background-color: initial;
}
.invoice tbody:not(:first-of-type) tr:first-child td, .invoice .tfoot th, .invoice .tfoot td {
padding-top: 1em;
}
.invoice tbody .name td:first-child {
font-weight: bold;
}
.invoice tbody td:first-child {
max-width: 20rem;
}
.invoice footer {
margin-top: 8rem;
font-size: 1.2rem;
}
@media print {
@page {
size: A4;
}
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100%;
}
html {
font-family: sans-serif;
font-size: 62.5%;
}
body {
background-color: white;
color: black;
font-size: 1.6rem;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
table {
width: 100%;
border-collapse: collapse;
}
body > header, nav {
display: none;
}
.invoice {
height: 100vh;
}
}

View File

@ -0,0 +1,102 @@
{{ define "title" -}}
{{ .Number | printf ( pgettext "Invoice %s" "title" )}}
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoice*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a href="{{ companyURI "/invoices"}}">{{( pgettext "Invoices" "title" )}}</a> /
<a>{{ .Number }}</a>
</p>
</nav>
<link rel="stylesheet" type="text/css" href="/static/invoice.css">
<article class="invoice">
<header>
<h1>{{ .Number | printf ( pgettext "Invoice %s" "title" )}}</h1>
<p class="date">{{( pgettext "Date" "title" )}} {{ .Date | formatDate }}</p>
<address class="invoicer">
{{ .Invoicer.Name }}<br>
{{ .Invoicer.VATIN }}<br>
{{ .Invoicer.Address }}<br>
{{ .Invoicer.City }} ({{ .Invoicer.PostalCode}}), {{ .Invoicer.Province }}<br>
{{ .Invoicer.Email }}<br>
{{ .Invoicer.Phone }}<br>
</address>
</header>
<div>
<address class="invoicee">
{{ .Invoicee.Name }}<br>
{{ .Invoicee.VATIN }}<br>
{{ .Invoicee.Address }}<br>
{{ .Invoicee.City }} ({{ .Invoicee.PostalCode}}), {{ .Invoicee.Province }}<br>
</address>
<table>
<thead>
<tr>
<th>{{( pgettext "Concept" "title" )}}</th>
<th class="numeric">{{( pgettext "Price" "title" )}}</th>
<th class="numeric">{{( pgettext "Units" "title" )}}</th>
<th class="numeric">{{( pgettext "Subtotal" "title" )}}</th>
</tr>
</thead>
{{ range $product := .Products -}}
{{ if .Description }}
<tbody>
<tr class="name">
<td colspan="4">{{ .Name }}</td>
</tr>
<tr>
<td>{{ .Description }}</td>
<td class="numeric">{{ .Price | formatPrice }}</td>
<td class="numeric">{{ .Quantity }}</td>
<td class="numeric">{{ .Total | formatPrice }}</td>
</tr>
</tbody>
{{ else }}
<tbody>
<tr class="name">
<td>{{ .Name }}</td>
<td class="numeric">{{ .Price | formatPrice }}</td>
<td class="numeric">{{ .Quantity }}</td>
<td class="numeric">{{ .Total | formatPrice }}</td>
</tr>
</tbody>
{{- end }}
{{- end }}
<tbody class="tfoot">
<tr>
<th scope="row" colspan="3">{{( pgettext "Subtotal" "title" )}}</th>
<td class="numeric">{{ .Subtotal | formatPrice }}</td>
</tr>
{{ range $tax := .Taxes -}}
<tr>
<th scope="row" colspan="3">{{ index . 0 }}</th>
<td class="numeric">{{ index . 1 | formatPrice }}</td>
</tr>
{{- end }}
<tr>
<th scope="row" colspan="3">{{( pgettext "Total" "title" )}}</th>
<td class="numeric">{{ .Total | formatPrice }}</td>
</tr>
</tbody>
</table>
<div class="notes">{{ .Notes }}</div>
<footer>Oriol Carbonell Pujolàs és Responsable del Tractament de les seves dades d'acord
amb el RGPD i la LOPDGDDGDD, i les tracta per a mantenir una relació
mercantil/comercial amb vostè. Les conservarà mentre es mantingui aquesta relació
i no es comunicaran a tercers. Pot exercir els drets d'accés, rectificació, portabilitat,
supressió, limitació i oposició a Oriol Carbonell pUJOLÀS, amb domicili Carrer dels
Sastres 14, 17800 Olot o enviant un correu electrònic a info@visavis.cat. Per a
qualsevol reclamació pot acudir a agpd.es. Per a més informació pot consultar la
nostra política de privacitat a www.visavis.cat.
</footer>
</div>
</article>
{{- end}}