Compare commits

..

No commits in common. "d6034ad7327166cef5457c3a4b93627b2ec97c10" and "11d51df7fa67517dcf7a65d5b6049f2f6bd3e7b7" have entirely different histories.

16 changed files with 123 additions and 398 deletions

View File

@ -43,7 +43,7 @@ values (1, 'Melcior', 'IR1', 'Rei Blanc', parse_packed_phone_number('0732621', '
alter sequence product_product_id_seq restart;
insert into product(company_id, name, description, price)
values (1, 'Or', 'Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.', 5592)
values (1, 'Or', 'Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.', 5592)
, (1, 'Encens', 'Goma resina fragrant que desprèn una olor característica quan es crema.', 215)
, (1, 'Mirra', 'Goma resinosa aromàtica de color gris groguenc i gust amargant.', 690)
, (1, 'Paper higiènic (pack de 32 U)', 'Paper que susa per mantenir la higiene personal després de defecar o orinar.', 799)

View File

@ -1,19 +1,29 @@
-- Deploy numerus:invoice_amount to pg
-- requires: schema_numerus
-- requires: invoice_product
-- requires: invoice_product_amount
-- requires: invoice_tax_amount
begin;
set search_path to numerus, public;
create or replace view invoice_amount as
with taxable as (
select invoice_id
, sum(subtotal)::integer as subtotal
, sum(total)::integer as total
, sum(round(price * quantity * (1 - discount_rate))::integer)::integer as subtotal
from invoice_product
join invoice_product_amount using (invoice_product_id)
group by invoice_id
), taxes as (
select invoice_id
, sum(amount)::integer as tax_amount
from invoice_tax_amount
group by invoice_id
)
select invoice_id
, subtotal
, subtotal + coalesce(tax_amount, 0) as total
from taxable
left join taxes using (invoice_id)
;
grant select on table invoice_amount to invoicer;

View File

@ -1,22 +0,0 @@
-- Deploy numerus:invoice_product_amount to pg
-- requires: schema_numerus
-- requires: invoice_product
-- requires: invoice_product_tax
begin;
set search_path to numerus, public;
create or replace view invoice_product_amount as
select invoice_product_id
, round(price * quantity * (1 - discount_rate))::integer as subtotal
, max(round(price * quantity * (1 - discount_rate))::integer) + coalesce(sum(round(round(price * quantity * (1 - discount_rate))::integer * tax_rate)::integer)::integer, 0) as total
from invoice_product
left join invoice_product_tax using (invoice_product_id)
group by invoice_product_id, price, quantity, discount_rate
;
grant select on table invoice_product_amount to invoicer;
grant select on table invoice_product_amount to admin;
commit;

View File

@ -66,7 +66,6 @@ func (field *InputField) Float64() float64 {
type SelectOption struct {
Value string
Label string
Group string
}
type SelectField struct {
@ -127,17 +126,13 @@ func (field *SelectField) IsSelected(v string) bool {
return false
}
func (field *SelectField) FindOption(value string) *SelectOption {
for _, option := range field.Options {
if option.Value == value {
return option
}
}
return nil
}
func (field *SelectField) isValidOption(selected string) bool {
return field.FindOption(selected) != nil
for _, option := range field.Options {
if option.Value == selected {
return true
}
}
return false
}
func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption {
@ -163,29 +158,6 @@ func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interfa
return options
}
func MustGetGroupedOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption {
rows, err := conn.Query(ctx, sql, args...)
if err != nil {
panic(err)
}
defer rows.Close()
var options []*SelectOption
for rows.Next() {
option := &SelectOption{}
err = rows.Scan(&option.Value, &option.Label, &option.Group)
if err != nil {
panic(err)
}
options = append(options, option)
}
if rows.Err() != nil {
panic(rows.Err())
}
return options
}
func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []*SelectOption {
return MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", locale.Language)
}
@ -229,20 +201,6 @@ func (v *FormValidator) CheckValidSelectOption(field *SelectField, message strin
return v.checkSelect(field, field.HasValidOptions(), message)
}
func (v *FormValidator) CheckAtMostOneOfEachGroup(field *SelectField, message string) bool {
repeated := false
groups := map[string]bool{}
for _, selected := range field.Selected {
option := field.FindOption(selected)
if exists := groups[option.Group]; exists {
repeated = true
break
}
groups[option.Group] = true
}
return v.checkSelect(field, !repeated, message)
}
func (v *FormValidator) CheckValidURL(field *InputField, message string) bool {
_, err := url.ParseRequestURI(field.Val)
return v.checkInput(field, err == nil, message)

View File

@ -11,7 +11,6 @@ import (
"math"
"net/http"
"os/exec"
"sort"
"strconv"
"strings"
"time"
@ -130,8 +129,6 @@ type invoice struct {
Products []*invoiceProduct
Subtotal string
Taxes [][]string
TaxClasses []string
HasDiscounts bool
Total string
}
@ -150,10 +147,7 @@ type invoiceProduct struct {
Name string
Description string
Price string
Discount int
Quantity int
Taxes map[string]int
Subtotal string
Total string
}
@ -172,31 +166,15 @@ func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug stri
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 invoice_product.name, description, to_price(price, $2), (discount_rate * 100)::integer, quantity, to_price(subtotal, $2), to_price(total, $2), array_agg(array[tax_class.name, (tax_rate * 100)::integer::text]) from invoice_product join invoice_product_amount using (invoice_product_id) left join invoice_product_tax using (invoice_product_id) left join tax using (tax_id) left join tax_class using (tax_class_id) where invoice_id = $1 group by invoice_product.name, description, discount_rate, price, quantity, subtotal, total", invoiceId, decimalDigits)
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()
taxClasses := map[string]bool{}
for rows.Next() {
product := &invoiceProduct{
Taxes: make(map[string]int),
}
var taxes [][]string
if err := rows.Scan(&product.Name, &product.Description, &product.Price, &product.Discount, &product.Quantity, &product.Subtotal, &product.Total, &taxes); err != nil {
product := &invoiceProduct{}
if err := rows.Scan(&product.Name, &product.Description, &product.Price, &product.Quantity, &product.Total); err != nil {
panic(err)
}
for _, tax := range taxes {
taxClass := tax[0]
taxClasses[taxClass] = true
product.Taxes[taxClass], _ = strconv.Atoi(tax[1])
}
if product.Discount > 0 {
inv.HasDiscounts = true
}
inv.Products = append(inv.Products, product)
}
for taxClass := range taxClasses {
inv.TaxClasses = append(inv.TaxClasses, taxClass)
}
sort.Strings(inv.TaxClasses)
if rows.Err() != nil {
panic(rows.Err())
}
@ -440,7 +418,7 @@ func (form *invoiceForm) AddProducts(ctx context.Context, conn *Conn, productsId
}
func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*SelectOption {
return MustGetGroupedOptions(ctx, conn, "select tax_id::text, tax.name, tax_class.name from tax join tax_class using (tax_class_id) where tax.company_id = $1 order by tax_class.name, tax.name", company.Id)
return MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id)
}
type invoiceProductForm struct {
@ -547,6 +525,5 @@ func (form *invoiceProductForm) Validate() bool {
validator.CheckValidInteger(form.Discount, 0, 100, gettext("Discount must be a percentage between 0 and 100.", form.locale))
}
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
return validator.AllOK()
}

View File

@ -208,6 +208,5 @@ func (form *productForm) Validate() bool {
validator.CheckValidDecimal(form.Price, form.company.MinCents(), math.MaxFloat64, gettext("Price must be a number greater than zero.", form.locale))
}
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
return validator.AllOK()
}

View File

@ -43,9 +43,6 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
"formatDate": func(time time.Time) template.HTML {
return template.HTML(`<time datetime="` + time.Format("2006-01-02") + `">` + time.Format("02/01/2006") + "</time>")
},
"formatPercent": func(value int) string {
return fmt.Sprintf("%d %%", value)
},
"csrfToken": func() template.HTML {
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, csrfTokenField, user.CsrfToken))
},
@ -57,16 +54,6 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
field.Attributes = append(field.Attributes, template.HTMLAttr(attr))
return field
},
"boolToInt": func(b bool) int {
if b {
return 1
} else {
return 0
}
},
"add": func(y, x int) int {
return x + y
},
"sub": func(y, x int) int {
return x - y
},

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-03-01 14:00+0100\n"
"POT-Creation-Date: 2023-02-28 11:56+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"
@ -59,7 +59,7 @@ msgid "Name"
msgstr "Nom"
#: web/template/invoices/products.gohtml:43
#: web/template/invoices/view.gohtml:60 web/template/products/index.gohtml:23
#: web/template/invoices/view.gohtml:59 web/template/products/index.gohtml:23
msgctxt "title"
msgid "Price"
msgstr "Preu"
@ -74,13 +74,13 @@ msgctxt "action"
msgid "Add products"
msgstr "Afegeix productes"
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:65
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:61
#: web/template/invoices/view.gohtml:87
msgctxt "title"
msgid "Subtotal"
msgstr "Subtotal"
#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:69
#: web/template/invoices/view.gohtml:109
#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:97
msgctxt "title"
msgid "Total"
msgstr "Total"
@ -149,26 +149,16 @@ msgctxt "action"
msgid "Download invoice"
msgstr "Descarrega factura"
#: web/template/invoices/view.gohtml:59
#: web/template/invoices/view.gohtml:58
msgctxt "title"
msgid "Concept"
msgstr "Concepte"
#: web/template/invoices/view.gohtml:62
msgctxt "title"
msgid "Discount"
msgstr "Descompte"
#: web/template/invoices/view.gohtml:64
#: web/template/invoices/view.gohtml:60
msgctxt "title"
msgid "Units"
msgstr "Unitats"
#: web/template/invoices/view.gohtml:99
msgctxt "title"
msgid "Tax Base"
msgstr "Base imposable"
#: web/template/dashboard.gohtml:2
msgctxt "title"
msgid "Dashboard"
@ -384,47 +374,43 @@ 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:468
#: pkg/products.go:165 pkg/invoices.go:446
msgctxt "input"
msgid "Name"
msgstr "Nom"
#: pkg/products.go:171 pkg/invoices.go:473
#: pkg/products.go:171 pkg/invoices.go:451
msgctxt "input"
msgid "Description"
msgstr "Descripció"
#: pkg/products.go:176 pkg/invoices.go:477
#: pkg/products.go:176 pkg/invoices.go:455
msgctxt "input"
msgid "Price"
msgstr "Preu"
#: pkg/products.go:186 pkg/invoices.go:366 pkg/invoices.go:503
#: pkg/products.go:186 pkg/invoices.go:344 pkg/invoices.go:481
msgctxt "input"
msgid "Taxes"
msgstr "Imposts"
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:399
#: pkg/invoices.go:539
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:377
#: pkg/invoices.go:517
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
#: pkg/products.go:207 pkg/invoices.go:540
#: pkg/products.go:207 pkg/invoices.go:518
msgid "Price can not be empty."
msgstr "No podeu deixar el preu en blanc."
#: pkg/products.go:208 pkg/invoices.go:541
#: pkg/products.go:208 pkg/invoices.go:519
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:403 pkg/invoices.go:549
#: pkg/products.go:210 pkg/invoices.go:381 pkg/invoices.go:527
msgid "Selected tax is not valid."
msgstr "Heu seleccionat un impost que no és vàlid."
#: pkg/products.go:211 pkg/invoices.go:550
msgid "You can only select a tax of each class."
msgstr "Només podeu seleccionar un impost de cada classe."
#: pkg/company.go:90
msgctxt "input"
msgid "Currency"
@ -497,70 +483,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:229
#: pkg/invoices.go:207
msgid "Select a customer to bill."
msgstr "Escolliu un client a facturar."
#: pkg/invoices.go:322
#: pkg/invoices.go:300
msgid "Invalid action"
msgstr "Acció invàlida."
#: pkg/invoices.go:343
#: pkg/invoices.go:321
msgctxt "input"
msgid "Customer"
msgstr "Client"
#: pkg/invoices.go:349
#: pkg/invoices.go:327
msgctxt "input"
msgid "Number"
msgstr "Número"
#: pkg/invoices.go:355
#: pkg/invoices.go:333
msgctxt "input"
msgid "Invoice Date"
msgstr "Data de factura"
#: pkg/invoices.go:361
#: pkg/invoices.go:339
msgctxt "input"
msgid "Notes"
msgstr "Notes"
#: pkg/invoices.go:400
#: pkg/invoices.go:378
msgid "Invoice date can not be empty."
msgstr "No podeu deixar la data de la factura en blanc."
#: pkg/invoices.go:401
#: pkg/invoices.go:379
msgid "Invoice date must be a valid date."
msgstr "La data de facturació ha de ser vàlida."
#: pkg/invoices.go:463
#: pkg/invoices.go:441
msgctxt "input"
msgid "Id"
msgstr "Identificador"
#: pkg/invoices.go:486
#: pkg/invoices.go:464
msgctxt "input"
msgid "Quantity"
msgstr "Quantitat"
#: pkg/invoices.go:494
#: pkg/invoices.go:472
msgctxt "input"
msgid "Discount (%)"
msgstr "Descompte (%)"
#: pkg/invoices.go:543
#: pkg/invoices.go:521
msgid "Quantity can not be empty."
msgstr "No podeu deixar la quantitat en blanc."
#: pkg/invoices.go:544
#: pkg/invoices.go:522
msgid "Quantity must be a number greater than zero."
msgstr "La quantitat ha de ser un número major a zero."
#: pkg/invoices.go:546
#: pkg/invoices.go:524
msgid "Discount can not be empty."
msgstr "No podeu deixar el descompte en blanc."
#: pkg/invoices.go:547
#: pkg/invoices.go:525
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-03-01 14:00+0100\n"
"POT-Creation-Date: 2023-02-28 11:56+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"
@ -59,7 +59,7 @@ msgid "Name"
msgstr "Nombre"
#: web/template/invoices/products.gohtml:43
#: web/template/invoices/view.gohtml:60 web/template/products/index.gohtml:23
#: web/template/invoices/view.gohtml:59 web/template/products/index.gohtml:23
msgctxt "title"
msgid "Price"
msgstr "Precio"
@ -74,13 +74,13 @@ msgctxt "action"
msgid "Add products"
msgstr "Añadir productos"
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:65
#: web/template/invoices/new.gohtml:41 web/template/invoices/view.gohtml:61
#: web/template/invoices/view.gohtml:87
msgctxt "title"
msgid "Subtotal"
msgstr "Subtotal"
#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:69
#: web/template/invoices/view.gohtml:109
#: web/template/invoices/new.gohtml:51 web/template/invoices/view.gohtml:97
msgctxt "title"
msgid "Total"
msgstr "Total"
@ -149,26 +149,16 @@ msgctxt "action"
msgid "Download invoice"
msgstr "Descargar factura"
#: web/template/invoices/view.gohtml:59
#: web/template/invoices/view.gohtml:58
msgctxt "title"
msgid "Concept"
msgstr "Concepto"
#: web/template/invoices/view.gohtml:62
msgctxt "title"
msgid "Discount"
msgstr "Descuento"
#: web/template/invoices/view.gohtml:64
#: web/template/invoices/view.gohtml:60
msgctxt "title"
msgid "Units"
msgstr "Unidades"
#: web/template/invoices/view.gohtml:99
msgctxt "title"
msgid "Tax Base"
msgstr "Base imponible"
#: web/template/dashboard.gohtml:2
msgctxt "title"
msgid "Dashboard"
@ -384,47 +374,43 @@ 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:468
#: pkg/products.go:165 pkg/invoices.go:446
msgctxt "input"
msgid "Name"
msgstr "Nombre"
#: pkg/products.go:171 pkg/invoices.go:473
#: pkg/products.go:171 pkg/invoices.go:451
msgctxt "input"
msgid "Description"
msgstr "Descripción"
#: pkg/products.go:176 pkg/invoices.go:477
#: pkg/products.go:176 pkg/invoices.go:455
msgctxt "input"
msgid "Price"
msgstr "Precio"
#: pkg/products.go:186 pkg/invoices.go:366 pkg/invoices.go:503
#: pkg/products.go:186 pkg/invoices.go:344 pkg/invoices.go:481
msgctxt "input"
msgid "Taxes"
msgstr "Impuestos"
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:399
#: pkg/invoices.go:539
#: pkg/products.go:206 pkg/profile.go:92 pkg/invoices.go:377
#: pkg/invoices.go:517
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
#: pkg/products.go:207 pkg/invoices.go:540
#: pkg/products.go:207 pkg/invoices.go:518
msgid "Price can not be empty."
msgstr "No podéis dejar el precio en blanco."
#: pkg/products.go:208 pkg/invoices.go:541
#: pkg/products.go:208 pkg/invoices.go:519
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:403 pkg/invoices.go:549
#: pkg/products.go:210 pkg/invoices.go:381 pkg/invoices.go:527
msgid "Selected tax is not valid."
msgstr "Habéis escogido un impuesto que no es válido."
#: pkg/products.go:211 pkg/invoices.go:550
msgid "You can only select a tax of each class."
msgstr "Solo podéis escojer un impuesto de cada clase."
#: pkg/company.go:90
msgctxt "input"
msgid "Currency"
@ -497,70 +483,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:229
#: pkg/invoices.go:207
msgid "Select a customer to bill."
msgstr "Escoged un cliente a facturar."
#: pkg/invoices.go:322
#: pkg/invoices.go:300
msgid "Invalid action"
msgstr "Acción inválida."
#: pkg/invoices.go:343
#: pkg/invoices.go:321
msgctxt "input"
msgid "Customer"
msgstr "Cliente"
#: pkg/invoices.go:349
#: pkg/invoices.go:327
msgctxt "input"
msgid "Number"
msgstr "Número"
#: pkg/invoices.go:355
#: pkg/invoices.go:333
msgctxt "input"
msgid "Invoice Date"
msgstr "Fecha de factura"
#: pkg/invoices.go:361
#: pkg/invoices.go:339
msgctxt "input"
msgid "Notes"
msgstr "Notas"
#: pkg/invoices.go:400
#: pkg/invoices.go:378
msgid "Invoice date can not be empty."
msgstr "No podéis dejar la fecha de la factura en blanco."
#: pkg/invoices.go:401
#: pkg/invoices.go:379
msgid "Invoice date must be a valid date."
msgstr "La fecha de factura debe ser válida."
#: pkg/invoices.go:463
#: pkg/invoices.go:441
msgctxt "input"
msgid "Id"
msgstr "Identificador"
#: pkg/invoices.go:486
#: pkg/invoices.go:464
msgctxt "input"
msgid "Quantity"
msgstr "Cantidad"
#: pkg/invoices.go:494
#: pkg/invoices.go:472
msgctxt "input"
msgid "Discount (%)"
msgstr "Descuento (%)"
#: pkg/invoices.go:543
#: pkg/invoices.go:521
msgid "Quantity can not be empty."
msgstr "No podéis dejar la cantidad en blanco."
#: pkg/invoices.go:544
#: pkg/invoices.go:522
msgid "Quantity must be a number greater than zero."
msgstr "La cantidad tiene que ser un número mayor a cero."
#: pkg/invoices.go:546
#: pkg/invoices.go:524
msgid "Discount can not be empty."
msgstr "No podéis dejar el descuento en blanco."
#: pkg/invoices.go:547
#: pkg/invoices.go:525
msgid "Discount must be a percentage between 0 and 100."
msgstr "El descuento tiene que ser un percentage entre 0 y 100."

View File

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

View File

@ -58,7 +58,6 @@ invoice_number_counter [schema_numerus company] 2023-02-17T13:04:48Z jordi fita
next_invoice_number [schema_numerus invoice_number_counter] 2023-02-17T13:21:48Z jordi fita mas <jordi@tandem.blog> # Add function to retrieve the next invoice number
add_invoice [schema_numerus invoice company currency parse_price new_invoice_product tax invoice_product invoice_product_tax next_invoice_number] 2023-02-16T21:12:46Z jordi fita mas <jordi@tandem.blog> # Add function to create new invoices
invoice_tax_amount [schema_numerus invoice_product invoice_product_tax] 2023-02-22T12:08:35Z jordi fita mas <jordi@tandem.blog> # Add view for invoice tax amount
invoice_product_amount [schema_numerus invoice_product invoice_product_tax] 2023-03-01T11:18:05Z jordi fita mas <jordi@tandem.blog> # Add view for invoice product subtotal and total
invoice_amount [schema_numerus invoice_product invoice_product_amount] 2023-02-22T12:58:46Z jordi fita mas <jordi@tandem.blog> # Add view to compute subtotal and total for invoices
invoice_amount [schema_numerus invoice_product invoice_tax_amount] 2023-02-22T12:58:46Z jordi fita mas <jordi@tandem.blog> # Add view to compute subtotal and total for invoices
new_invoice_amount [schema_numerus] 2023-02-23T12:08:25Z jordi fita mas <jordi@tandem.blog> # Add type to return when computing new invoice amounts
compute_new_invoice_amount [schema_numerus company currency tax new_invoice_product new_invoice_amount] 2023-02-23T12:20:13Z jordi fita mas <jordi@tandem.blog> # Add function to compute the subtotal, taxes, and total amounts for a new invoice

View File

@ -1,110 +0,0 @@
-- Test invoice_product_amount
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(12);
set search_path to numerus, auth, public;
select has_view('invoice_product_amount');
select table_privs_are('invoice_product_amount', 'guest', array[]::text[]);
select table_privs_are('invoice_product_amount', 'invoicer', array['SELECT']);
select table_privs_are('invoice_product_amount', 'admin', array['SELECT']);
select table_privs_are('invoice_product_amount', 'authenticator', array[]::text[]);
select has_column('invoice_product_amount', 'invoice_product_id');
select col_type_is('invoice_product_amount', 'invoice_product_id', 'integer');
select has_column('invoice_product_amount', 'subtotal');
select col_type_is('invoice_product_amount', 'subtotal', 'integer');
select has_column('invoice_product_amount', 'total');
select col_type_is('invoice_product_amount', 'total', 'integer');
set client_min_messages to warning;
truncate invoice_product_tax cascade;
truncate invoice_product cascade;
truncate invoice cascade;
truncate contact cascade;
truncate product cascade;
truncate tax cascade;
truncate tax_class cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code)
values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR')
;
insert into tax_class (tax_class_id, company_id, name)
values (11, 1, 'tax')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (2, 1, 11, 'IRPF -15 %', -0.15)
, (3, 1, 11, 'IVA 4 %', 0.04)
, (4, 1, 11, 'IVA 10 %', 0.10)
, (5, 1, 11, 'IVA 21 %', 0.21)
;
insert into product (product_id, company_id, name, price)
values (6, 1, 'Product', 1212)
;
insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code)
values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code)
values ( 8, 1, 'I1', current_date, 7, 'EUR')
, ( 9, 1, 'I2', current_date, 7, 'EUR')
, (10, 1, 'I3', current_date, 7, 'EUR')
, (11, 1, 'I4', current_date, 7, 'EUR')
;
insert into invoice_product (invoice_product_id, invoice_id, product_id, name, price, quantity, discount_rate)
values (12, 8, 6, 'P', 100, 1, 0.0)
, (13, 8, 6, 'P', 200, 2, 0.1)
, (14, 9, 6, 'P', 222, 3, 0.0)
, (15, 9, 6, 'P', 333, 4, 0.2)
, (16, 10, 6, 'P', 444, 5, 0.0)
, (17, 10, 6, 'P', 555, 6, 0.1)
, (18, 11, 6, 'P', 777, 8, 0.0)
;
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
values (12, 2, -0.15)
, (12, 5, 0.21)
, (13, 3, 0.04)
, (14, 4, 0.10)
, (14, 5, 0.21)
, (14, 2, -0.07)
, (15, 4, 0.10)
, (16, 4, 0.10)
, (16, 5, 0.21)
, (17, 5, 0.21)
, (17, 3, 0.04)
;
select bag_eq(
$$ select invoice_product_id, subtotal, total from invoice_product_amount $$,
$$ values (12, 100, 106)
, (13, 360, 374)
, (14, 666, 826)
, (15, 1066, 1173)
, (16, 2220, 2908)
, (17, 2997, 3746)
, (18, 6216, 6216)
$$,
'Should compute the subtotal and total for all products.'
);
select *
from finish();
rollback;

View File

@ -1,11 +0,0 @@
-- Verify numerus:invoice_product_amount on pg
begin;
select invoice_product_id
, subtotal
, total
from numerus.invoice_product_amount
where false;
rollback;

View File

@ -12,7 +12,7 @@
}
.invoice h1 {
font-size: 1em;
font-size: 1.6rem;
}
.invoice > div {
@ -76,20 +76,16 @@
padding-top: 1em;
}
.invoice .tfoot.separator th, .invoice .tfoot.separator td {
padding-top: 3em;
}
.invoice tbody .name td:first-child {
font-weight: bold;
}
.invoice tbody td:first-child {
max-width: 15em;
max-width: 20rem;
}
.invoice .legal {
font-size: .75em;
font-size: 1.2rem;
text-align: justify;
}
@ -125,7 +121,7 @@
body {
background-color: white;
color: black;
font-size: 1rem;
font-size: 1.6rem;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
@ -153,6 +149,6 @@
left: 0;
top: 0;
transform-origin: top left;
transform: translateY(250mm) rotate(-90deg);
transform: translateY(260mm) rotate(-90deg);
}
}

View File

@ -43,27 +43,16 @@
<div class="input {{ if .Errors }}has-errors{{ end }}">
<select id="{{ .Name }}-field" name="{{ .Name }}"
{{- range $attribute := .Attributes }} {{$attribute}} {{ end -}}
{{ if .Multiple }} multiple="multiple"{{ end -}}
{{ if .Required }} required="required"{{ end -}}
{{ if .Multiple }}multiple="multiple"{{ end }}
{{ if .Required }}required="required"{{ end }}
>
{{- with .EmptyLabel }}
<option value="">{{ . }}</option>
{{- end}}
{{- $withinGroup := "" -}}
{{- range $option := .Options }}
{{- if ne .Group $withinGroup }}
{{- if ne $withinGroup "" }}
</optgroup>
{{ end }}
<optgroup label="{{ .Group }}">
{{- $withinGroup = .Group -}}
{{ end }}
<option value="{{ .Value }}"
{{- if $.IsSelected .Value }} selected="selected"{{ end }}>{{ .Label }}</option>
{{- end }}
{{- if ne $withinGroup "" }}
</optgroup>
{{- end }}
</select>
<label for="{{ .Name }}-field">{{ .Label }}</label>
{{- if .Errors }}

View File

@ -52,61 +52,49 @@
{{ .Invoicee.City }} ({{ .Invoicee.PostalCode}}), {{ .Invoicee.Province }}<br>
</address>
{{- $columns := 5 | add (len .TaxClasses) | add (boolToInt .HasDiscounts) -}}
<table>
<thead>
<tr>
<th>{{( pgettext "Concept" "title" )}}</th>
<th class="numeric">{{( pgettext "Price" "title" )}}</th>
{{ if .HasDiscounts -}}
<th class="numeric">{{( pgettext "Discount" "title" )}}</th>
{{ end -}}
<th class="numeric">{{( pgettext "Units" "title" )}}</th>
<th class="numeric">{{( pgettext "Subtotal" "title" )}}</th>
{{ range $class := .TaxClasses -}}
<th class="numeric">{{ . }}</th>
{{ end -}}
<th class="numeric">{{( pgettext "Total" "title" )}}</th>
</tr>
</thead>
{{ $lastIndex := len .Products | sub 1 }}
{{ range $index, $product := .Products -}}
<tbody>
{{- if .Description }}
{{ if .Description }}
<tr class="name">
<td colspan="{{ $columns }}">{{ .Name }}</td>
<td colspan="4">{{ .Name }}</td>
</tr>
{{ end -}}
<tr>
{{- if .Description }}
<td>{{ .Description }}</td>
{{- else }}
<td>{{ .Name }}</td>
{{- end -}}
<td class="numeric">{{ .Price | formatPrice }}</td>
{{ if $.HasDiscounts -}}
<td class="numeric">{{ $product.Discount | formatPercent }}</td>
{{ end -}}
<td class="numeric">{{ .Quantity }}</td>
<td class="numeric">{{ .Subtotal | formatPrice }}</td>
{{ range $class := $.TaxClasses -}}
<td class="numeric">{{ index $product.Taxes $class | formatPercent }}</td>
{{ end -}}
<td class="numeric">{{ .Total | formatPrice }}</td>
</tr>
{{ else }}
<tr class="name">
<td>{{ .Name }}</td>
<td class="numeric">{{ .Price | formatPrice }}</td>
<td class="numeric">{{ .Quantity }}</td>
<td class="numeric">{{ .Total | formatPrice }}</td>
</tr>
{{- end }}
{{ if (eq $index $lastIndex) }}
<tr class="tfoot separator">
<th scope="row" colspan="{{ $columns | sub 1 }}">{{( pgettext "Tax Base" "title" )}}</th>
<tr class="tfoot">
<th scope="row" colspan="3">{{( pgettext "Subtotal" "title" )}}</th>
<td class="numeric">{{ $.Subtotal | formatPrice }}</td>
</tr>
{{ range $tax := $.Taxes -}}
<tr class="tfoot">
<th scope="row" colspan="{{ $columns | sub 1 }}">{{ index . 0 }}</th>
<th scope="row" colspan="3">{{ index . 0 }}</th>
<td class="numeric">{{ index . 1 | formatPrice }}</td>
</tr>
{{- end }}
<tr class="tfoot">
<th scope="row" colspan="{{ $columns | sub 1 }}">{{( pgettext "Total" "title" )}}</th>
<th scope="row" colspan="3">{{( pgettext "Total" "title" )}}</th>
<td class="numeric">{{ $.Total | formatPrice }}</td>
</tr>
{{ end }}