From 11d51df7fa67517dcf7a65d5b6049f2f6bd3e7b7 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 28 Feb 2023 12:02:27 +0100 Subject: [PATCH] Introduce the concept of tax class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We want to show the percentage of the tax as columns in the invoice, but until now it was not possible to have a single VAT column when products have different VAT (e.g., 4 % and 10 %), because, as far as the application is concerned, these where ”different taxes”. We also think it would be hard later on to compute the tax due to the government. So, tax classes is just a taxonomy to be able to have different names and rates for the same type of tax, mostly VAT and retention in our case. --- demo/demo.sql | 16 ++-- deploy/tax.sql | 2 + deploy/tax_class.sql | 34 ++++++++ pkg/company.go | 34 +++++--- po/ca.po | 96 ++++++++++++--------- po/es.po | 96 ++++++++++++--------- revert/tax_class.sql | 7 ++ sqitch.plan | 3 +- test/add_invoice.sql | 16 ++-- test/add_product.sql | 16 ++-- test/compute_new_invoice_amount.sql | 15 ++-- test/edit_product.sql | 16 ++-- test/invoice_amount.sql | 15 ++-- test/invoice_product_tax.sql | 12 ++- test/invoice_tax_amount.sql | 15 ++-- test/product_tax.sql | 12 ++- test/tax.sql | 36 +++++--- test/tax_class.sql | 124 ++++++++++++++++++++++++++++ verify/tax.sql | 1 + verify/tax_class.sql | 14 ++++ web/template/tax-details.gohtml | 7 +- 21 files changed, 443 insertions(+), 144 deletions(-) create mode 100644 deploy/tax_class.sql create mode 100644 revert/tax_class.sql create mode 100644 test/tax_class.sql create mode 100644 verify/tax_class.sql 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"` }} +