diff --git a/demo/demo.sql b/demo/demo.sql index cd3c675..a9dc0c9 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -17,12 +17,18 @@ values (1, 1) , (1, 2) ; +alter sequence tax_class_tax_class_id_seq restart; +insert into tax_class (company_id, name) +values (1, 'IRPF') + , (1, 'IVA') +; + alter sequence tax_tax_id_seq restart; -insert into tax (company_id, name, rate) -values (1, 'Retenció 15 %', -0.15) - , (1, 'IVA 21 %', 0.21) - , (1, 'IVA 10 %', 0.10) - , (1, 'IVA 4 %', 0.04) +insert into tax (company_id, tax_class_id, name, rate) +values (1, 1, 'Retenció 15 %', -0.15) + , (1, 2, 'IVA 21 %', 0.21) + , (1, 2, 'IVA 10 %', 0.10) + , (1, 2, 'IVA 4 %', 0.04) ; alter sequence contact_contact_id_seq restart; diff --git a/deploy/tax.sql b/deploy/tax.sql index 26b8244..cf975cc 100644 --- a/deploy/tax.sql +++ b/deploy/tax.sql @@ -2,6 +2,7 @@ -- requires: schema_numerus -- requires: company -- requires: tax_rate +-- requires: tax_class begin; @@ -10,6 +11,7 @@ set search_path to numerus, public; create table tax ( tax_id serial primary key, company_id integer not null references company, + tax_class_id integer not null references tax_class, name text not null constraint name_not_empty check(length(trim(name)) > 0), rate tax_rate not null ); diff --git a/deploy/tax_class.sql b/deploy/tax_class.sql new file mode 100644 index 0000000..2d37693 --- /dev/null +++ b/deploy/tax_class.sql @@ -0,0 +1,34 @@ +-- Deploy numerus:tax_class to pg +-- requires: schema_numerus +-- requires: company + +begin; + +set search_path to numerus, public; + +create table tax_class ( + tax_class_id serial not null primary key, + company_id integer not null references company, + name text not null constraint name_not_empty check(length(trim(name)) > 0) +); + +grant select, insert, update, delete on table tax_class to invoicer; +grant select, insert, update, delete on table tax_class to admin; + +grant usage on sequence tax_class_tax_class_id_seq to invoicer; +grant usage on sequence tax_class_tax_class_id_seq to admin; + +alter table tax_class enable row level security; + +create policy company_policy +on tax_class +using ( + exists( + select 1 + from company_user + join user_profile using (user_id) + where company_user.company_id = tax_class.company_id + ) +); + +commit; diff --git a/pkg/company.go b/pkg/company.go index ded4f0c..54aec39 100644 --- a/pkg/company.go +++ b/pkg/company.go @@ -71,9 +71,10 @@ type CountryOption struct { } type Tax struct { - Id int - Name string - Rate int + Id int + Name string + Class string + Rate int } type taxDetailsForm struct { @@ -161,10 +162,9 @@ func HandleCompanyTaxDetailsForm(w http.ResponseWriter, r *http.Request, _ httpr } func mustRenderTaxDetailsForm(w http.ResponseWriter, r *http.Request, form *taxDetailsForm) { - locale := getLocale(r) page := &TaxDetailsPage{ DetailsForm: form, - NewTaxForm: newTaxForm(locale), + NewTaxForm: newTaxForm(r.Context(), getConn(r), mustGetCompany(r), getLocale(r)), } mustRenderTexDetailsPage(w, r, page) } @@ -193,7 +193,7 @@ func mustGetCompany(r *http.Request) *Company { } func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []*Tax { - rows, err := conn.Query(ctx, "select tax_id, name, (rate * 100)::integer from tax where company_id = $1 order by rate, name", company.Id) + rows, err := conn.Query(ctx, "select tax_id, tax.name, tax_class.name, (rate * 100)::integer from tax join tax_class using (tax_class_id) where tax.company_id = $1 order by rate, tax.name", company.Id) if err != nil { panic(err) } @@ -202,7 +202,7 @@ func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []*Tax { var taxes []*Tax for rows.Next() { tax := &Tax{} - err = rows.Scan(&tax.Id, &tax.Name, &tax.Rate) + err = rows.Scan(&tax.Id, &tax.Name, &tax.Class, &tax.Rate) if err != nil { panic(err) } @@ -218,10 +218,11 @@ func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []*Tax { type taxForm struct { locale *Locale Name *InputField + Class *SelectField Rate *InputField } -func newTaxForm(locale *Locale) *taxForm { +func newTaxForm(ctx context.Context, conn *Conn, company *Company, locale *Locale) *taxForm { return &taxForm{ locale: locale, Name: &InputField{ @@ -230,6 +231,13 @@ func newTaxForm(locale *Locale) *taxForm { Type: "text", Required: true, }, + Class: &SelectField{ + Name: "tax_class", + Label: pgettext("input", "Tax Class", locale), + Options: MustGetOptions(ctx, conn, "select tax_class_id::text, name from tax_class where company_id = $1 order by name", company.Id), + Required: true, + EmptyLabel: gettext("Select a tax class", locale), + }, Rate: &InputField{ Name: "tax_rate", Label: pgettext("input", "Rate (%)", locale), @@ -248,6 +256,7 @@ func (form *taxForm) Parse(r *http.Request) error { return err } form.Name.FillValue(r) + form.Class.FillValue(r) form.Rate.FillValue(r) return nil } @@ -255,6 +264,7 @@ func (form *taxForm) Parse(r *http.Request) error { func (form *taxForm) Validate() bool { validator := newFormValidator() validator.CheckRequiredInput(form.Name, gettext("Tax name can not be empty.", form.locale)) + validator.CheckValidSelectOption(form.Class, gettext("Selected tax class is not valid.", form.locale)) if validator.CheckRequiredInput(form.Rate, gettext("Tax rate can not be empty.", form.locale)) { validator.CheckValidInteger(form.Rate, -99, 99, gettext("Tax rate must be an integer between -99 and 99.", form.locale)) } @@ -278,7 +288,9 @@ func HandleDeleteCompanyTax(w http.ResponseWriter, r *http.Request, params httpr func HandleAddCompanyTax(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { locale := getLocale(r) - form := newTaxForm(locale) + conn := getConn(r) + company := mustGetCompany(r) + form := newTaxForm(r.Context(), conn, company, locale) if err := form.Parse(r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -292,8 +304,6 @@ func HandleAddCompanyTax(w http.ResponseWriter, r *http.Request, _ httprouter.Pa mustRenderTaxForm(w, r, form) return } - conn := getConn(r) - company := mustGetCompany(r) - conn.MustExec(r.Context(), "insert into tax (company_id, name, rate) values ($1, $2, $3 / 100::decimal)", company.Id, form.Name, form.Rate.Integer()) + conn.MustExec(r.Context(), "insert into tax (company_id, tax_class_id, name, rate) values ($1, $2, $3, $4 / 100::decimal)", company.Id, form.Class, form.Name, form.Rate.Integer()) http.Redirect(w, r, companyURI(company, "/tax-details"), http.StatusSeeOther) } diff --git a/po/ca.po b/po/ca.po index 194fffb..754832b 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: 2023-02-26 17:11+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 \n" "Language-Team: Catalan \n" @@ -69,7 +69,7 @@ msgstr "Preu" msgid "No products added yet." msgstr "No hi ha cap producte." -#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:59 +#: web/template/invoices/products.gohtml:65 web/template/invoices/new.gohtml:60 msgctxt "action" msgid "Add products" msgstr "Afegeix productes" @@ -85,12 +85,12 @@ msgctxt "title" msgid "Total" msgstr "Total" -#: web/template/invoices/new.gohtml:61 +#: web/template/invoices/new.gohtml:63 msgctxt "action" msgid "Update" msgstr "Actualitza" -#: web/template/invoices/new.gohtml:63 web/template/invoices/index.gohtml:13 +#: web/template/invoices/new.gohtml:65 web/template/invoices/index.gohtml:13 msgctxt "action" msgid "New invoice" msgstr "Nova factura" @@ -276,7 +276,7 @@ msgctxt "title" msgid "Language" msgstr "Idioma" -#: web/template/profile.gohtml:35 web/template/tax-details.gohtml:96 +#: web/template/profile.gohtml:35 web/template/tax-details.gohtml:101 msgctxt "action" msgid "Save changes" msgstr "Desa canvis" @@ -302,16 +302,21 @@ msgctxt "title" msgid "Rate (%)" msgstr "Percentatge" -#: web/template/tax-details.gohtml:71 +#: web/template/tax-details.gohtml:49 +msgctxt "title" +msgid "Class" +msgstr "Classe" + +#: web/template/tax-details.gohtml:73 msgid "No taxes added yet." msgstr "No hi ha cap impost." -#: web/template/tax-details.gohtml:77 +#: web/template/tax-details.gohtml:79 msgctxt "title" msgid "New Line" msgstr "Nova línia" -#: web/template/tax-details.gohtml:88 +#: web/template/tax-details.gohtml:93 msgctxt "action" msgid "Add new tax" msgstr "Afegeix nou impost" @@ -369,71 +374,84 @@ msgstr "No podeu deixar la contrasenya en blanc." msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." -#: pkg/products.go:165 pkg/invoices.go:435 +#: pkg/products.go:165 pkg/invoices.go:446 msgctxt "input" msgid "Name" msgstr "Nom" -#: pkg/products.go:171 pkg/invoices.go:440 +#: pkg/products.go:171 pkg/invoices.go:451 msgctxt "input" msgid "Description" msgstr "Descripció" -#: pkg/products.go:176 pkg/invoices.go:444 +#: pkg/products.go:176 pkg/invoices.go:455 msgctxt "input" msgid "Price" msgstr "Preu" -#: pkg/products.go:186 pkg/invoices.go:350 pkg/invoices.go:470 +#: 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:383 -#: pkg/invoices.go:506 +#: 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:507 +#: 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:508 +#: 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:387 pkg/invoices.go:516 +#: 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/company.go:89 +#: pkg/company.go:90 msgctxt "input" msgid "Currency" msgstr "Moneda" -#: pkg/company.go:107 +#: pkg/company.go:108 msgid "Selected currency is not valid." msgstr "Heu seleccionat una moneda que no és vàlida." -#: pkg/company.go:229 +#: pkg/company.go:230 msgctxt "input" msgid "Tax name" msgstr "Nom impost" -#: pkg/company.go:235 +#: pkg/company.go:236 +msgctxt "input" +msgid "Tax Class" +msgstr "Classe d’impost" + +#: pkg/company.go:239 +msgid "Select a tax class" +msgstr "Escolliu una classe d’impost" + +#: pkg/company.go:243 msgctxt "input" msgid "Rate (%)" msgstr "Percentatge" -#: pkg/company.go:257 +#: pkg/company.go:266 msgid "Tax name can not be empty." msgstr "No podeu deixar el nom de l’impost en blanc." -#: pkg/company.go:258 +#: pkg/company.go:267 +msgid "Selected tax class is not valid." +msgstr "Heu seleccionat una classe d’impost que no és vàlida." + +#: pkg/company.go:268 msgid "Tax rate can not be empty." msgstr "No podeu deixar percentatge en blanc." -#: pkg/company.go:259 +#: pkg/company.go:269 msgid "Tax rate must be an integer between -99 and 99." msgstr "El percentatge ha de ser entre -99 i 99." @@ -465,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:201 +#: pkg/invoices.go:207 msgid "Select a customer to bill." msgstr "Escolliu un client a facturar." -#: pkg/invoices.go:276 +#: pkg/invoices.go:300 msgid "Invalid action" msgstr "Acció invàlida." -#: pkg/invoices.go:327 +#: pkg/invoices.go:321 msgctxt "input" msgid "Customer" msgstr "Client" -#: pkg/invoices.go:333 +#: pkg/invoices.go:327 msgctxt "input" msgid "Number" msgstr "Número" -#: pkg/invoices.go:339 +#: pkg/invoices.go:333 msgctxt "input" msgid "Invoice Date" msgstr "Data de factura" -#: pkg/invoices.go:345 +#: pkg/invoices.go:339 msgctxt "input" msgid "Notes" msgstr "Notes" -#: pkg/invoices.go:384 +#: 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:385 +#: 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:430 +#: pkg/invoices.go:441 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/invoices.go:453 +#: pkg/invoices.go:464 msgctxt "input" msgid "Quantity" msgstr "Quantitat" -#: pkg/invoices.go:461 +#: pkg/invoices.go:472 msgctxt "input" msgid "Discount (%)" msgstr "Descompte (%)" -#: pkg/invoices.go:510 +#: pkg/invoices.go:521 msgid "Quantity can not be empty." msgstr "No podeu deixar la quantitat en blanc." -#: pkg/invoices.go:511 +#: 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:513 +#: pkg/invoices.go:524 msgid "Discount can not be empty." msgstr "No podeu deixar el descompte en blanc." -#: pkg/invoices.go:514 +#: 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." diff --git a/po/es.po b/po/es.po index 41577b6..11ebb60 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: 2023-02-26 17:11+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 \n" "Language-Team: Spanish \n" @@ -69,7 +69,7 @@ msgstr "Precio" msgid "No products added yet." msgstr "No hay productos." -#: web/template/invoices/products.gohtml:64 web/template/invoices/new.gohtml:59 +#: web/template/invoices/products.gohtml:65 web/template/invoices/new.gohtml:60 msgctxt "action" msgid "Add products" msgstr "Añadir productos" @@ -85,12 +85,12 @@ msgctxt "title" msgid "Total" msgstr "Total" -#: web/template/invoices/new.gohtml:61 +#: web/template/invoices/new.gohtml:63 msgctxt "action" msgid "Update" msgstr "Actualizar" -#: web/template/invoices/new.gohtml:63 web/template/invoices/index.gohtml:13 +#: web/template/invoices/new.gohtml:65 web/template/invoices/index.gohtml:13 msgctxt "action" msgid "New invoice" msgstr "Nueva factura" @@ -276,7 +276,7 @@ msgctxt "title" msgid "Language" msgstr "Idioma" -#: web/template/profile.gohtml:35 web/template/tax-details.gohtml:96 +#: web/template/profile.gohtml:35 web/template/tax-details.gohtml:101 msgctxt "action" msgid "Save changes" msgstr "Guardar cambios" @@ -302,16 +302,21 @@ msgctxt "title" msgid "Rate (%)" msgstr "Porcentaje" -#: web/template/tax-details.gohtml:71 +#: web/template/tax-details.gohtml:49 +msgctxt "title" +msgid "Class" +msgstr "Clase" + +#: web/template/tax-details.gohtml:73 msgid "No taxes added yet." msgstr "No hay impuestos." -#: web/template/tax-details.gohtml:77 +#: web/template/tax-details.gohtml:79 msgctxt "title" msgid "New Line" msgstr "Nueva línea" -#: web/template/tax-details.gohtml:88 +#: web/template/tax-details.gohtml:93 msgctxt "action" msgid "Add new tax" msgstr "Añadir nuevo impuesto" @@ -369,71 +374,84 @@ msgstr "No podéis dejar la contraseña en blanco." msgid "Invalid user or password." msgstr "Nombre de usuario o contraseña inválido." -#: pkg/products.go:165 pkg/invoices.go:435 +#: pkg/products.go:165 pkg/invoices.go:446 msgctxt "input" msgid "Name" msgstr "Nombre" -#: pkg/products.go:171 pkg/invoices.go:440 +#: pkg/products.go:171 pkg/invoices.go:451 msgctxt "input" msgid "Description" msgstr "Descripción" -#: pkg/products.go:176 pkg/invoices.go:444 +#: pkg/products.go:176 pkg/invoices.go:455 msgctxt "input" msgid "Price" msgstr "Precio" -#: pkg/products.go:186 pkg/invoices.go:350 pkg/invoices.go:470 +#: 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:383 -#: pkg/invoices.go:506 +#: 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:507 +#: 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:508 +#: 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:387 pkg/invoices.go:516 +#: 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/company.go:89 +#: pkg/company.go:90 msgctxt "input" msgid "Currency" msgstr "Moneda" -#: pkg/company.go:107 +#: pkg/company.go:108 msgid "Selected currency is not valid." msgstr "Habéis escogido una moneda que no es válida." -#: pkg/company.go:229 +#: pkg/company.go:230 msgctxt "input" msgid "Tax name" msgstr "Nombre impuesto" -#: pkg/company.go:235 +#: pkg/company.go:236 +msgctxt "input" +msgid "Tax Class" +msgstr "Clase de impuesto" + +#: pkg/company.go:239 +msgid "Select a tax class" +msgstr "Escoged una clase de impuesto" + +#: pkg/company.go:243 msgctxt "input" msgid "Rate (%)" msgstr "Porcentaje" -#: pkg/company.go:257 +#: pkg/company.go:266 msgid "Tax name can not be empty." msgstr "No podéis dejar el nombre del impuesto en blanco." -#: pkg/company.go:258 +#: pkg/company.go:267 +msgid "Selected tax class is not valid." +msgstr "Habéis escogido una clase impuesto que no es válida." + +#: pkg/company.go:268 msgid "Tax rate can not be empty." msgstr "No podéis dejar el porcentaje en blanco." -#: pkg/company.go:259 +#: pkg/company.go:269 msgid "Tax rate must be an integer between -99 and 99." msgstr "El porcentaje tiene que estar entre -99 y 99." @@ -465,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:201 +#: pkg/invoices.go:207 msgid "Select a customer to bill." msgstr "Escoged un cliente a facturar." -#: pkg/invoices.go:276 +#: pkg/invoices.go:300 msgid "Invalid action" msgstr "Acción inválida." -#: pkg/invoices.go:327 +#: pkg/invoices.go:321 msgctxt "input" msgid "Customer" msgstr "Cliente" -#: pkg/invoices.go:333 +#: pkg/invoices.go:327 msgctxt "input" msgid "Number" msgstr "Número" -#: pkg/invoices.go:339 +#: pkg/invoices.go:333 msgctxt "input" msgid "Invoice Date" msgstr "Fecha de factura" -#: pkg/invoices.go:345 +#: pkg/invoices.go:339 msgctxt "input" msgid "Notes" msgstr "Notas" -#: pkg/invoices.go:384 +#: 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:385 +#: pkg/invoices.go:379 msgid "Invoice date must be a valid date." msgstr "La fecha de factura debe ser válida." -#: pkg/invoices.go:430 +#: pkg/invoices.go:441 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/invoices.go:453 +#: pkg/invoices.go:464 msgctxt "input" msgid "Quantity" msgstr "Cantidad" -#: pkg/invoices.go:461 +#: pkg/invoices.go:472 msgctxt "input" msgid "Discount (%)" msgstr "Descuento (%)" -#: pkg/invoices.go:510 +#: pkg/invoices.go:521 msgid "Quantity can not be empty." msgstr "No podéis dejar la cantidad en blanco." -#: pkg/invoices.go:511 +#: 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:513 +#: pkg/invoices.go:524 msgid "Discount can not be empty." msgstr "No podéis dejar el descuento en blanco." -#: pkg/invoices.go:514 +#: 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." diff --git a/revert/tax_class.sql b/revert/tax_class.sql new file mode 100644 index 0000000..b434842 --- /dev/null +++ b/revert/tax_class.sql @@ -0,0 +1,7 @@ +-- Revert numerus:tax_class from pg + +begin; + +drop table if exists numerus.tax_class; + +commit; diff --git a/sqitch.plan b/sqitch.plan index dccb599..f9854ff 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -36,8 +36,9 @@ country_i18n [schema_numerus country_code language country] 2023-01-27T19:20:43Z available_countries [schema_numerus country country_i18n] 2023-01-27T18:49:28Z jordi fita mas # Add the list of available countries company [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-24T15:03:15Z jordi fita mas # Add the relation for companies company_user [schema_numerus user company] 2023-01-24T17:50:06Z jordi fita mas # Add the relation of companies and their users +tax_class [schema_numerus company] 2023-02-28T10:13:14Z jordi fita mas # Add the relation for tax classes tax_rate [schema_numerus] 2023-01-28T11:33:39Z jordi fita mas # Add domain for tax rates -tax [schema_numerus company tax_rate] 2023-01-28T11:45:47Z jordi fita mas # Add relation for taxes +tax [schema_numerus company tax_rate tax_class] 2023-01-28T11:45:47Z jordi fita mas # Add relation for taxes contact [schema_numerus company extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-29T12:59:18Z jordi fita mas # Add the relation for contacts product [schema_numerus company tax] 2023-02-04T09:17:24Z jordi fita mas # Add relation for products parse_price [schema_public] 2023-02-05T11:04:54Z jordi fita mas # Add function to convert from price to cents diff --git a/test/add_invoice.sql b/test/add_invoice.sql index 2714db2..81ddaf6 100644 --- a/test/add_invoice.sql +++ b/test/add_invoice.sql @@ -28,6 +28,7 @@ truncate invoice cascade; truncate contact cascade; truncate product cascade; truncate tax cascade; +truncate tax_class cascade; truncate company cascade; reset client_min_messages; @@ -41,11 +42,16 @@ values (1, 2023, '5') , (2, 2023, '55') ; -insert into tax (tax_id, company_id, name, rate) -values (3, 1, 'IRPF -15 %', -0.15) - , (4, 1, 'IVA 21 %', 0.21) - , (5, 2, 'IRPF -7 %', -0.07) - , (6, 2, 'IVA 10 %', 0.10) +insert into tax_class (tax_class_id, company_id, name) +values (11, 1, 'tax') + , (22, 2, 'tax') +; + +insert into tax (tax_id, company_id, tax_class_id, name, rate) +values (3, 1, 11, 'IRPF -15 %', -0.15) + , (4, 1, 11, 'IVA 21 %', 0.21) + , (5, 2, 22, 'IRPF -7 %', -0.07) + , (6, 2, 22, 'IVA 10 %', 0.10) ; insert into product (product_id, company_id, name, price) diff --git a/test/add_product.sql b/test/add_product.sql index 84a2fb1..e2e73d2 100644 --- a/test/add_product.sql +++ b/test/add_product.sql @@ -24,6 +24,7 @@ set client_min_messages to warning; truncate product_tax cascade; truncate product cascade; truncate tax cascade; +truncate tax_class cascade; truncate company cascade; reset client_min_messages; @@ -33,11 +34,16 @@ values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', ' , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD') ; -insert into tax (tax_id, company_id, name, rate) -values (3, 1, 'IRPF -15 %', -0.15) - , (4, 1, 'IVA 21 %', 0.21) - , (5, 2, 'IRPF -7 %', -0.07) - , (6, 2, 'IVA 10 %', 0.10) +insert into tax_class (tax_class_id, company_id, name) +values (11, 1, 'tax') + , (22, 2, 'tax') +; + +insert into tax (tax_id, company_id, tax_class_id, name, rate) +values (3, 1, 11, 'IRPF -15 %', -0.15) + , (4, 1, 11, 'IVA 21 %', 0.21) + , (5, 2, 22, 'IRPF -7 %', -0.07) + , (6, 2, 22, 'IVA 10 %', 0.10) ; diff --git a/test/compute_new_invoice_amount.sql b/test/compute_new_invoice_amount.sql index 79201d4..fb1d9c5 100644 --- a/test/compute_new_invoice_amount.sql +++ b/test/compute_new_invoice_amount.sql @@ -21,6 +21,7 @@ select function_privs_are('numerus', 'compute_new_invoice_amount', array ['integ set client_min_messages to warning; truncate tax cascade; +truncate tax_class cascade; truncate company cascade; reset client_min_messages; @@ -28,11 +29,15 @@ insert into company (company_id, business_name, vatin, trade_name, phone, email, values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR') ; -insert into tax (tax_id, company_id, name, rate) -values (2, 1, 'IRPF -15 %', -0.15) - , (3, 1, 'IVA 4 %', 0.04) - , (4, 1, 'IVA 10 %', 0.10) - , (5, 1, 'IVA 21 %', 0.21) +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) ; select is( diff --git a/test/edit_product.sql b/test/edit_product.sql index 46b95ff..88b7138 100644 --- a/test/edit_product.sql +++ b/test/edit_product.sql @@ -24,6 +24,7 @@ set client_min_messages to warning; truncate product_tax cascade; truncate product cascade; truncate tax cascade; +truncate tax_class cascade; truncate company cascade; reset client_min_messages; @@ -33,11 +34,16 @@ values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', ' , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD') ; -insert into tax (tax_id, company_id, name, rate) -values (3, 1, 'IRPF -15 %', -0.15) - , (4, 1, 'IVA 21 %', 0.21) - , (5, 2, 'IRPF -7 %', -0.07) - , (6, 2, 'IVA 10 %', 0.10) +insert into tax_class (tax_class_id, company_id, name) +values (11, 1, 'tax') + , (22, 2, 'tax') +; + +insert into tax (tax_id, company_id, tax_class_id, name, rate) +values (3, 1, 11, 'IRPF -15 %', -0.15) + , (4, 1, 11, 'IVA 21 %', 0.21) + , (5, 2, 22, 'IRPF -7 %', -0.07) + , (6, 2, 22, 'IVA 10 %', 0.10) ; insert into product (product_id, company_id, slug, name, description, price) diff --git a/test/invoice_amount.sql b/test/invoice_amount.sql index 9b01ed5..ba82012 100644 --- a/test/invoice_amount.sql +++ b/test/invoice_amount.sql @@ -32,6 +32,7 @@ truncate invoice cascade; truncate contact cascade; truncate product cascade; truncate tax cascade; +truncate tax_class cascade; truncate company cascade; reset client_min_messages; @@ -39,11 +40,15 @@ insert into company (company_id, business_name, vatin, trade_name, phone, email, values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR') ; -insert into tax (tax_id, company_id, name, rate) -values (2, 1, 'IRPF -15 %', -0.15) - , (3, 1, 'IVA 4 %', 0.04) - , (4, 1, 'IVA 10 %', 0.10) - , (5, 1, 'IVA 21 %', 0.21) +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) diff --git a/test/invoice_product_tax.sql b/test/invoice_product_tax.sql index c5309d9..db65b4e 100644 --- a/test/invoice_product_tax.sql +++ b/test/invoice_product_tax.sql @@ -43,6 +43,7 @@ truncate invoice_product cascade; truncate invoice cascade; truncate product cascade; truncate tax cascade; +truncate tax_class cascade; truncate contact cascade; truncate company_user cascade; truncate company cascade; @@ -64,9 +65,14 @@ values (2, 1) , (4, 5) ; -insert into tax (tax_id, company_id, name, rate) -values (3, 2, 'IVA 21 %', 0.21) - , (6, 4, 'IVA 10 %', 0.10) +insert into tax_class (tax_class_id, company_id, name) +values (22, 2, 'vat') + , (44, 4, 'vat') +; + +insert into tax (tax_id, company_id, tax_class_id, name, rate) +values (3, 2, 22, 'IVA 21 %', 0.21) + , (6, 4, 44, 'IVA 10 %', 0.10) ; insert into product (product_id, company_id, name, price) diff --git a/test/invoice_tax_amount.sql b/test/invoice_tax_amount.sql index 1b537da..605472a 100644 --- a/test/invoice_tax_amount.sql +++ b/test/invoice_tax_amount.sql @@ -32,6 +32,7 @@ truncate invoice cascade; truncate contact cascade; truncate product cascade; truncate tax cascade; +truncate tax_class cascade; truncate company cascade; reset client_min_messages; @@ -39,11 +40,15 @@ insert into company (company_id, business_name, vatin, trade_name, phone, email, values (1, 'Company 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR') ; -insert into tax (tax_id, company_id, name, rate) -values (2, 1, 'IRPF -15 %', -0.15) - , (3, 1, 'IVA 4 %', 0.04) - , (4, 1, 'IVA 10 %', 0.10) - , (5, 1, 'IVA 21 %', 0.21) +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) diff --git a/test/product_tax.sql b/test/product_tax.sql index 7f7ffd8..5fc699f 100644 --- a/test/product_tax.sql +++ b/test/product_tax.sql @@ -36,6 +36,7 @@ set client_min_messages to warning; truncate product_tax cascade; truncate product cascade; truncate tax cascade; +truncate tax_class cascade; truncate company_user cascade; truncate company cascade; truncate auth."user" cascade; @@ -56,9 +57,14 @@ values (2, 1) , (4, 5) ; -insert into tax (tax_id, company_id, name, rate) -values (3, 2, 'IVA 21 %', 0.21) - , (6, 4, 'IVA 10 %', 0.10) +insert into tax_class (tax_class_id, company_id, name) +values (22, 2, 'iva') + , (44, 4, 'iva') +; + +insert into tax (tax_id, company_id, tax_class_id, name, rate) +values (3, 2, 22, 'IVA 21 %', 0.21) + , (6, 4, 44, 'IVA 10 %', 0.10) ; insert into product (product_id, company_id, name, description, price) diff --git a/test/tax.sql b/test/tax.sql index 0137dfd..dbe3738 100644 --- a/test/tax.sql +++ b/test/tax.sql @@ -5,7 +5,7 @@ reset client_min_messages; begin; -select plan(36); +select plan(42); set search_path to numerus, auth, public; @@ -36,6 +36,13 @@ select col_type_is('tax', 'company_id', 'integer'); select col_not_null('tax', 'company_id'); select col_hasnt_default('tax', 'company_id'); +select has_column('tax', 'tax_class_id'); +select col_is_fk('tax', 'tax_class_id'); +select fk_ok('tax', 'tax_class_id', 'tax_class', 'tax_class_id'); +select col_type_is('tax', 'tax_class_id', 'integer'); +select col_not_null('tax', 'tax_class_id'); +select col_hasnt_default('tax', 'tax_class_id'); + select has_column('tax', 'name'); select col_type_is('tax', 'name', 'text'); select col_not_null('tax', 'name'); @@ -49,6 +56,7 @@ select col_hasnt_default('tax', 'rate'); set client_min_messages to warning; truncate tax cascade; +truncate tax_class cascade; truncate company_user cascade; truncate company cascade; truncate auth."user" cascade; @@ -69,14 +77,20 @@ values (2, 1) , (4, 5) ; -insert into tax (company_id, name, rate) -values (2, 'VAT 21 %', 0.21) - , (2, 'IRPF -15 %', -0.15) - , (4, 'VAT 21 %', 0.21) - , (4, 'VAT 10 %', 0.10) - , (4, 'VAT 5 %', 0.05) - , (4, 'VAT 4 %', 0.04) - , (4, 'VAT 0 %', 0.00) +insert into tax_class(tax_class_id, company_id, name) +values (5, 2, 'vat') + , (6, 2, 'irpf') + , (7, 4, 'vat') +; + +insert into tax (company_id, tax_class_id, name, rate) +values (2, 5, 'VAT 21 %', 0.21) + , (2, 6, 'IRPF -15 %', -0.15) + , (4, 7, 'VAT 21 %', 0.21) + , (4, 7, 'VAT 10 %', 0.10) + , (4, 7, 'VAT 5 %', 0.05) + , (4, 7, 'VAT 4 %', 0.04) + , (4, 7, 'VAT 0 %', 0.00) ; prepare tax_data as @@ -120,8 +134,8 @@ select throws_ok( reset role; select throws_ok( $$ - insert into tax (company_id, name, rate) - values (2, ' ', 0.22) + insert into tax (company_id, tax_class_id, name, rate) + values (2, 6, ' ', 0.22) $$, '23514', 'new row for relation "tax" violates check constraint "name_not_empty"', 'Should not allow taxs with blank name' diff --git a/test/tax_class.sql b/test/tax_class.sql new file mode 100644 index 0000000..3580251 --- /dev/null +++ b/test/tax_class.sql @@ -0,0 +1,124 @@ +-- Test tax_class +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(32); + +set search_path to numerus, auth, public; + +select has_table('tax_class'); +select has_pk('tax_class' ); +select table_privs_are('tax_class', 'guest', array []::text[]); +select table_privs_are('tax_class', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('tax_class', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('tax_class', 'authenticator', array []::text[]); + +select has_sequence('tax_class_tax_class_id_seq'); +select sequence_privs_are('tax_class_tax_class_id_seq', 'guest', array[]::text[]); +select sequence_privs_are('tax_class_tax_class_id_seq', 'invoicer', array['USAGE']); +select sequence_privs_are('tax_class_tax_class_id_seq', 'admin', array['USAGE']); +select sequence_privs_are('tax_class_tax_class_id_seq', 'authenticator', array[]::text[]); + +select has_column('tax_class', 'tax_class_id'); +select col_is_pk('tax_class', 'tax_class_id'); +select col_type_is('tax_class', 'tax_class_id', 'integer'); +select col_not_null('tax_class', 'tax_class_id'); +select col_has_default('tax_class', 'tax_class_id'); +select col_default_is('tax_class', 'tax_class_id', 'nextval(''tax_class_tax_class_id_seq''::regclass)'); + +select has_column('tax_class', 'company_id'); +select col_is_fk('tax_class', 'company_id'); +select fk_ok('tax_class', 'company_id', 'company', 'company_id'); +select col_type_is('tax_class', 'company_id', 'integer'); +select col_not_null('tax_class', 'company_id'); +select col_hasnt_default('tax_class', 'company_id'); + +select has_column('tax_class', 'name'); +select col_type_is('tax_class', 'name', 'text'); +select col_not_null('tax_class', 'name'); +select col_hasnt_default('tax_class', 'name'); + + +set client_min_messages to warning; +truncate tax_class cascade; +truncate company_user cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD') +; + +insert into company_user (company_id, user_id) +values (2, 1) + , (4, 5) +; + +insert into tax_class (company_id, name) +values (2, 'VAT') + , (2, 'IRPF') + , (4, 'VAT') + , (4, 'import') +; + +prepare tax_class_data as +select company_id, name +from tax_class +order by company_id; + +set role invoicer; +select is_empty('tax_class_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'tax_class_data', + $$ values (2, 'VAT') + , (2, 'IRPF') + $$, + 'Should only list taxes of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'tax_class_data', + $$ values (4, 'VAT') + , (4, 'import') + $$, + 'Should only list taxes of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'tax_class_data', + '42501', 'permission denied for table tax_class', + 'Should not allow select to guest users' +); +reset role; + +select throws_ok( $$ + insert into tax_class (company_id, name) + values (2, ' ') + $$, + '23514', 'new row for relation "tax_class" violates check constraint "name_not_empty"', + 'Should not allow classes with blank name' +); + + +select * +from finish(); + +rollback; + diff --git a/verify/tax.sql b/verify/tax.sql index 53a2036..a8836ce 100644 --- a/verify/tax.sql +++ b/verify/tax.sql @@ -4,6 +4,7 @@ begin; select tax_id , company_id + , tax_class_id , name , rate from numerus.tax diff --git a/verify/tax_class.sql b/verify/tax_class.sql new file mode 100644 index 0000000..8e71f1c --- /dev/null +++ b/verify/tax_class.sql @@ -0,0 +1,14 @@ +-- Verify numerus:tax_class on pg + +begin; + +select tax_class_id + , company_id + , name +from numerus.tax_class +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.tax_class'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.tax_class'::regclass; + +rollback; diff --git a/web/template/tax-details.gohtml b/web/template/tax-details.gohtml index 10ee1fe..f715ab4 100644 --- a/web/template/tax-details.gohtml +++ b/web/template/tax-details.gohtml @@ -46,6 +46,7 @@ {{( pgettext "Tax Name" "title" )}} {{( pgettext "Rate (%)" "title" )}} + {{( pgettext "Class" "title" )}} @@ -56,6 +57,7 @@ {{ .Name }} {{ .Rate }} + {{ .Class }}
{{ csrfToken }} @@ -78,9 +80,12 @@ {{ template "input-field" .NewTaxForm.Name | addInputAttr `form="newtax"` }} - + {{ template "input-field" .NewTaxForm.Rate | addInputAttr `form="newtax"` }} + + {{ template "select-field" .NewTaxForm.Class | addSelectAttr `form="newtax"` }} +