From 9894925742a3ba047ef0979c72c0ed3bfa893a49 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Fri, 3 Mar 2023 16:49:06 +0100 Subject: [PATCH] Add the payment method relation and corresponding form --- deploy/payment_method.sql | 35 +++++++ pkg/company.go | 136 +++++++++++++++++++++++++-- pkg/router.go | 2 + po/ca.po | 158 +++++++++++++++++++++----------- po/es.po | 158 +++++++++++++++++++++----------- revert/payment_method.sql | 7 ++ sqitch.plan | 1 + test/payment_method.sql | 126 +++++++++++++++++++++++++ verify/payment_method.sql | 15 +++ web/template/tax-details.gohtml | 57 ++++++++++++ 10 files changed, 586 insertions(+), 109 deletions(-) create mode 100644 deploy/payment_method.sql create mode 100644 revert/payment_method.sql create mode 100644 test/payment_method.sql create mode 100644 verify/payment_method.sql diff --git a/deploy/payment_method.sql b/deploy/payment_method.sql new file mode 100644 index 0000000..3bccd0b --- /dev/null +++ b/deploy/payment_method.sql @@ -0,0 +1,35 @@ +-- Deploy numerus:payment_method to pg +-- requires: schema_numerus +-- requires: company + +begin; + +set search_path to numerus, public; + +create table payment_method ( + payment_method_id serial primary key, + company_id integer not null references company, + name text not null constraint name_not_empty check(length(trim(name)) > 0), + instructions text not null +); + +grant select, insert, update, delete on table payment_method to invoicer; +grant select, insert, update, delete on table payment_method to admin; + +grant usage on sequence payment_method_payment_method_id_seq to invoicer; +grant usage on sequence payment_method_payment_method_id_seq to admin; + +alter table payment_method enable row level security; + +create policy company_policy +on payment_method +using ( + exists( + select 1 + from company_user + join user_profile using (user_id) + where company_user.company_id = payment_method.company_id + ) +); + +commit; diff --git a/pkg/company.go b/pkg/company.go index d677423..654a0a6 100644 --- a/pkg/company.go +++ b/pkg/company.go @@ -77,6 +77,12 @@ type Tax struct { Rate int } +type PaymentMethod struct { + Id int + Name string + Instructions string +} + type taxDetailsForm struct { *contactForm Currency *SelectField @@ -134,9 +140,11 @@ func (form *taxDetailsForm) mustFillFromDatabase(ctx context.Context, conn *Conn } type TaxDetailsPage struct { - DetailsForm *taxDetailsForm - NewTaxForm *taxForm - Taxes []*Tax + DetailsForm *taxDetailsForm + NewTaxForm *taxForm + Taxes []*Tax + NewPaymentMethodForm *paymentMethodForm + PaymentMethods []*PaymentMethod } func GetCompanyTaxDetailsForm(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { @@ -178,17 +186,30 @@ func HandleCompanyTaxDetailsForm(w http.ResponseWriter, r *http.Request, _ httpr } func mustRenderTaxDetailsForm(w http.ResponseWriter, r *http.Request, form *taxDetailsForm) { + conn := getConn(r) + locale := getLocale(r) page := &TaxDetailsPage{ - DetailsForm: form, - NewTaxForm: newTaxForm(r.Context(), getConn(r), mustGetCompany(r), getLocale(r)), + DetailsForm: form, + NewTaxForm: newTaxForm(r.Context(), conn, mustGetCompany(r), locale), + NewPaymentMethodForm: newPaymentMethodForm(locale), } mustRenderTexDetailsPage(w, r, page) } func mustRenderTaxForm(w http.ResponseWriter, r *http.Request, form *taxForm) { page := &TaxDetailsPage{ - DetailsForm: newTaxDetailsFormFromDatabase(r), - NewTaxForm: form, + DetailsForm: newTaxDetailsFormFromDatabase(r), + NewTaxForm: form, + NewPaymentMethodForm: newPaymentMethodForm(getLocale(r)), + } + mustRenderTexDetailsPage(w, r, page) +} + +func mustRenderPaymentMethodForm(w http.ResponseWriter, r *http.Request, form *paymentMethodForm) { + page := &TaxDetailsPage{ + DetailsForm: newTaxDetailsFormFromDatabase(r), + NewTaxForm: newTaxForm(r.Context(), getConn(r), mustGetCompany(r), getLocale(r)), + NewPaymentMethodForm: form, } mustRenderTexDetailsPage(w, r, page) } @@ -197,6 +218,7 @@ func mustRenderTexDetailsPage(w http.ResponseWriter, r *http.Request, page *TaxD conn := getConn(r) company := mustGetCompany(r) page.Taxes = mustGetTaxes(r.Context(), conn, company) + page.PaymentMethods = mustCollectPaymentMethods(r.Context(), conn, company) mustRenderAppTemplate(w, r, "tax-details.gohtml", page) } @@ -231,6 +253,29 @@ func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []*Tax { return taxes } +func mustCollectPaymentMethods(ctx context.Context, conn *Conn, company *Company) []*PaymentMethod { + rows, err := conn.Query(ctx, "select payment_method_id, name, instructions from payment_method where company_id = $1 order by name", company.Id) + if err != nil { + panic(err) + } + defer rows.Close() + + var methods []*PaymentMethod + for rows.Next() { + method := &PaymentMethod{} + err = rows.Scan(&method.Id, &method.Name, &method.Instructions) + if err != nil { + panic(err) + } + methods = append(methods, method) + } + if rows.Err() != nil { + panic(rows.Err()) + } + + return methods +} + type taxForm struct { locale *Locale Name *InputField @@ -323,3 +368,80 @@ func HandleAddCompanyTax(w http.ResponseWriter, r *http.Request, _ httprouter.Pa 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) } + +type paymentMethodForm struct { + locale *Locale + Name *InputField + Instructions *InputField +} + +func newPaymentMethodForm(locale *Locale) *paymentMethodForm { + return &paymentMethodForm{ + locale: locale, + Name: &InputField{ + Name: "method_name", + Label: pgettext("input", "Payment method name", locale), + Type: "text", + Required: true, + }, + Instructions: &InputField{ + Name: "method_instructions", + Label: pgettext("input", "Instructions", locale), + Type: "textarea", + Required: true, + }, + } +} + +func (form *paymentMethodForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + form.Name.FillValue(r) + form.Instructions.FillValue(r) + return nil +} + +func (form *paymentMethodForm) Validate() bool { + validator := newFormValidator() + validator.CheckRequiredInput(form.Name, gettext("Payment method name can not be empty.", form.locale)) + validator.CheckRequiredInput(form.Instructions, gettext("Payment instructions can not be empty.", form.locale)) + return validator.AllOK() +} + +func HandleDeletePaymentMethod(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + paymentMethodId, err := strconv.Atoi(params[0].Value) + if err != nil { + http.NotFound(w, r) + return + } + if err := verifyCsrfTokenValid(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + conn := getConn(r) + conn.MustExec(r.Context(), "delete from payment_method where payment_method_id = $1", paymentMethodId) + http.Redirect(w, r, companyURI(mustGetCompany(r), "/tax-details"), http.StatusSeeOther) +} + +func HandleAddPaymentMethod(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + locale := getLocale(r) + conn := getConn(r) + company := mustGetCompany(r) + form := newPaymentMethodForm(locale) + if err := form.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := verifyCsrfTokenValid(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + if !form.Validate() { + w.WriteHeader(http.StatusUnprocessableEntity) + mustRenderPaymentMethodForm(w, r, form) + return + } + conn.MustExec(r.Context(), "insert into payment_method (company_id, name, instructions) values ($1, $2, $3)", company.Id, form.Name, form.Instructions) + http.Redirect(w, r, companyURI(company, "/tax-details"), http.StatusSeeOther) +} diff --git a/pkg/router.go b/pkg/router.go index 6362526..069c61f 100644 --- a/pkg/router.go +++ b/pkg/router.go @@ -14,6 +14,8 @@ func NewRouter(db *Db) http.Handler { companyRouter.POST("/tax-details", HandleCompanyTaxDetailsForm) companyRouter.POST("/tax", HandleAddCompanyTax) companyRouter.DELETE("/tax/:taxId", HandleDeleteCompanyTax) + companyRouter.POST("/payment-method", HandleAddPaymentMethod) + companyRouter.DELETE("/payment-method/:paymentMethodId", HandleDeletePaymentMethod) companyRouter.GET("/contacts", IndexContacts) companyRouter.POST("/contacts", HandleAddContact) companyRouter.GET("/contacts/:slug", GetContactForm) diff --git a/po/ca.po b/po/ca.po index a5b9126..5f58f40 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-03-01 14:00+0100\n" +"POT-Creation-Date: 2023-03-03 16:38+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \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:53 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:58 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:62 +#: web/template/invoices/view.gohtml:102 msgctxt "title" msgid "Total" msgstr "Total" @@ -149,22 +149,22 @@ msgctxt "action" msgid "Download invoice" msgstr "Descarrega factura" -#: web/template/invoices/view.gohtml:59 +#: web/template/invoices/view.gohtml:52 msgctxt "title" msgid "Concept" msgstr "Concepte" -#: web/template/invoices/view.gohtml:62 +#: web/template/invoices/view.gohtml:55 msgctxt "title" msgid "Discount" msgstr "Descompte" -#: web/template/invoices/view.gohtml:64 +#: web/template/invoices/view.gohtml:57 msgctxt "title" msgid "Units" msgstr "Unitats" -#: web/template/invoices/view.gohtml:99 +#: web/template/invoices/view.gohtml:92 msgctxt "title" msgid "Tax Base" msgstr "Base imposable" @@ -286,7 +286,7 @@ msgctxt "title" msgid "Language" msgstr "Idioma" -#: web/template/profile.gohtml:35 web/template/tax-details.gohtml:101 +#: web/template/profile.gohtml:35 web/template/tax-details.gohtml:165 msgctxt "action" msgid "Save changes" msgstr "Desa canvis" @@ -302,35 +302,59 @@ msgctxt "title" msgid "Currency" msgstr "Moneda" -#: web/template/tax-details.gohtml:47 +#: web/template/tax-details.gohtml:37 +msgctxt "title" +msgid "Invoicing" +msgstr "Facturació" + +#: web/template/tax-details.gohtml:54 msgctxt "title" msgid "Tax Name" msgstr "Nom impost" -#: web/template/tax-details.gohtml:48 +#: web/template/tax-details.gohtml:55 msgctxt "title" msgid "Rate (%)" msgstr "Percentatge" -#: web/template/tax-details.gohtml:49 +#: web/template/tax-details.gohtml:56 msgctxt "title" msgid "Class" msgstr "Classe" -#: web/template/tax-details.gohtml:73 +#: web/template/tax-details.gohtml:80 msgid "No taxes added yet." msgstr "No hi ha cap impost." -#: web/template/tax-details.gohtml:79 +#: web/template/tax-details.gohtml:86 web/template/tax-details.gohtml:146 msgctxt "title" msgid "New Line" msgstr "Nova línia" -#: web/template/tax-details.gohtml:93 +#: web/template/tax-details.gohtml:100 msgctxt "action" msgid "Add new tax" msgstr "Afegeix nou impost" +#: web/template/tax-details.gohtml:116 +msgctxt "title" +msgid "Payment Method" +msgstr "Mètode de pagament" + +#: web/template/tax-details.gohtml:117 +msgctxt "title" +msgid "Instructions" +msgstr "Instruccions" + +#: web/template/tax-details.gohtml:140 +msgid "No payment methods added yet." +msgstr "No hi ha cap mètode de pagament." + +#: web/template/tax-details.gohtml:157 +msgctxt "action" +msgid "Add new payment method" +msgstr "Afegeix nou mètode de pagament" + #: web/template/products/new.gohtml:2 web/template/products/new.gohtml:11 #: web/template/products/new.gohtml:15 msgctxt "title" @@ -384,91 +408,123 @@ 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:468 +#: pkg/products.go:165 pkg/invoices.go:469 msgctxt "input" msgid "Name" msgstr "Nom" -#: pkg/products.go:171 pkg/invoices.go:473 +#: pkg/products.go:171 pkg/invoices.go:474 msgctxt "input" msgid "Description" msgstr "Descripció" -#: pkg/products.go:176 pkg/invoices.go:477 +#: pkg/products.go:176 pkg/invoices.go:478 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:367 pkg/invoices.go:504 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:400 +#: pkg/invoices.go:540 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:541 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:542 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:404 pkg/invoices.go:550 msgid "Selected tax is not valid." msgstr "Heu seleccionat un impost que no és vàlid." -#: pkg/products.go:211 pkg/invoices.go:550 +#: pkg/products.go:211 pkg/invoices.go:551 msgid "You can only select a tax of each class." msgstr "Només podeu seleccionar un impost de cada classe." -#: pkg/company.go:90 +#: pkg/company.go:98 msgctxt "input" msgid "Currency" msgstr "Moneda" -#: pkg/company.go:108 +#: pkg/company.go:105 +msgctxt "input" +msgid "Invoice number format" +msgstr "Format del número de factura" + +#: pkg/company.go:111 +msgctxt "input" +msgid "Legal disclaimer" +msgstr "Nota legal" + +#: pkg/company.go:129 msgid "Selected currency is not valid." msgstr "Heu seleccionat una moneda que no és vàlida." -#: pkg/company.go:230 +#: pkg/company.go:130 +msgid "Invoice number format can not be empty." +msgstr "No podeu deixar el format del número de factura en blanc." + +#: pkg/company.go:291 msgctxt "input" msgid "Tax name" msgstr "Nom impost" -#: pkg/company.go:236 +#: pkg/company.go:297 msgctxt "input" msgid "Tax Class" msgstr "Classe d’impost" -#: pkg/company.go:239 +#: pkg/company.go:300 msgid "Select a tax class" msgstr "Escolliu una classe d’impost" -#: pkg/company.go:243 +#: pkg/company.go:304 msgctxt "input" msgid "Rate (%)" msgstr "Percentatge" -#: pkg/company.go:266 +#: pkg/company.go:327 msgid "Tax name can not be empty." msgstr "No podeu deixar el nom de l’impost en blanc." -#: pkg/company.go:267 +#: pkg/company.go:328 msgid "Selected tax class is not valid." msgstr "Heu seleccionat una classe d’impost que no és vàlida." -#: pkg/company.go:268 +#: pkg/company.go:329 msgid "Tax rate can not be empty." msgstr "No podeu deixar percentatge en blanc." -#: pkg/company.go:269 +#: pkg/company.go:330 msgid "Tax rate must be an integer between -99 and 99." msgstr "El percentatge ha de ser entre -99 i 99." +#: pkg/company.go:383 +msgctxt "input" +msgid "Payment method name" +msgstr "Nom del mètode de pagament" + +#: pkg/company.go:389 +msgctxt "input" +msgid "Instructions" +msgstr "Instruccions" + +#: pkg/company.go:407 +msgid "Payment method name can not be empty." +msgstr "No podeu deixar el nom del mètode de pagament en blanc." + +#: pkg/company.go:408 +msgid "Payment instructions can not be empty." +msgstr "No podeu deixar les instruccions de pagament en blanc." + #: pkg/profile.go:25 msgctxt "language option" msgid "Automatic" @@ -497,70 +553,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:230 msgid "Select a customer to bill." msgstr "Escolliu un client a facturar." -#: pkg/invoices.go:322 +#: pkg/invoices.go:323 msgid "Invalid action" msgstr "Acció invàlida." -#: pkg/invoices.go:343 +#: pkg/invoices.go:344 msgctxt "input" msgid "Customer" msgstr "Client" -#: pkg/invoices.go:349 +#: pkg/invoices.go:350 msgctxt "input" msgid "Number" msgstr "Número" -#: pkg/invoices.go:355 +#: pkg/invoices.go:356 msgctxt "input" msgid "Invoice Date" msgstr "Data de factura" -#: pkg/invoices.go:361 +#: pkg/invoices.go:362 msgctxt "input" msgid "Notes" msgstr "Notes" -#: pkg/invoices.go:400 +#: pkg/invoices.go:401 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:402 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:464 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/invoices.go:486 +#: pkg/invoices.go:487 msgctxt "input" msgid "Quantity" msgstr "Quantitat" -#: pkg/invoices.go:494 +#: pkg/invoices.go:495 msgctxt "input" msgid "Discount (%)" msgstr "Descompte (%)" -#: pkg/invoices.go:543 +#: pkg/invoices.go:544 msgid "Quantity can not be empty." msgstr "No podeu deixar la quantitat en blanc." -#: pkg/invoices.go:544 +#: pkg/invoices.go:545 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:547 msgid "Discount can not be empty." msgstr "No podeu deixar el descompte en blanc." -#: pkg/invoices.go:547 +#: pkg/invoices.go:548 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 71cf600..d54ea8f 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-03-01 14:00+0100\n" +"POT-Creation-Date: 2023-03-03 16:38+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \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:53 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:58 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:62 +#: web/template/invoices/view.gohtml:102 msgctxt "title" msgid "Total" msgstr "Total" @@ -149,22 +149,22 @@ msgctxt "action" msgid "Download invoice" msgstr "Descargar factura" -#: web/template/invoices/view.gohtml:59 +#: web/template/invoices/view.gohtml:52 msgctxt "title" msgid "Concept" msgstr "Concepto" -#: web/template/invoices/view.gohtml:62 +#: web/template/invoices/view.gohtml:55 msgctxt "title" msgid "Discount" msgstr "Descuento" -#: web/template/invoices/view.gohtml:64 +#: web/template/invoices/view.gohtml:57 msgctxt "title" msgid "Units" msgstr "Unidades" -#: web/template/invoices/view.gohtml:99 +#: web/template/invoices/view.gohtml:92 msgctxt "title" msgid "Tax Base" msgstr "Base imponible" @@ -286,7 +286,7 @@ msgctxt "title" msgid "Language" msgstr "Idioma" -#: web/template/profile.gohtml:35 web/template/tax-details.gohtml:101 +#: web/template/profile.gohtml:35 web/template/tax-details.gohtml:165 msgctxt "action" msgid "Save changes" msgstr "Guardar cambios" @@ -302,35 +302,59 @@ msgctxt "title" msgid "Currency" msgstr "Moneda" -#: web/template/tax-details.gohtml:47 +#: web/template/tax-details.gohtml:37 +msgctxt "title" +msgid "Invoicing" +msgstr "Facturació" + +#: web/template/tax-details.gohtml:54 msgctxt "title" msgid "Tax Name" msgstr "Nombre impuesto" -#: web/template/tax-details.gohtml:48 +#: web/template/tax-details.gohtml:55 msgctxt "title" msgid "Rate (%)" msgstr "Porcentaje" -#: web/template/tax-details.gohtml:49 +#: web/template/tax-details.gohtml:56 msgctxt "title" msgid "Class" msgstr "Clase" -#: web/template/tax-details.gohtml:73 +#: web/template/tax-details.gohtml:80 msgid "No taxes added yet." msgstr "No hay impuestos." -#: web/template/tax-details.gohtml:79 +#: web/template/tax-details.gohtml:86 web/template/tax-details.gohtml:146 msgctxt "title" msgid "New Line" msgstr "Nueva línea" -#: web/template/tax-details.gohtml:93 +#: web/template/tax-details.gohtml:100 msgctxt "action" msgid "Add new tax" msgstr "Añadir nuevo impuesto" +#: web/template/tax-details.gohtml:116 +msgctxt "title" +msgid "Payment Method" +msgstr "Método de pago" + +#: web/template/tax-details.gohtml:117 +msgctxt "title" +msgid "Instructions" +msgstr "Instrucciones" + +#: web/template/tax-details.gohtml:140 +msgid "No payment methods added yet." +msgstr "No hay métodos de pago." + +#: web/template/tax-details.gohtml:157 +msgctxt "action" +msgid "Add new payment method" +msgstr "Añadir nuevo método de pago" + #: web/template/products/new.gohtml:2 web/template/products/new.gohtml:11 #: web/template/products/new.gohtml:15 msgctxt "title" @@ -384,91 +408,123 @@ 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:469 msgctxt "input" msgid "Name" msgstr "Nombre" -#: pkg/products.go:171 pkg/invoices.go:473 +#: pkg/products.go:171 pkg/invoices.go:474 msgctxt "input" msgid "Description" msgstr "Descripción" -#: pkg/products.go:176 pkg/invoices.go:477 +#: pkg/products.go:176 pkg/invoices.go:478 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:367 pkg/invoices.go:504 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:400 +#: pkg/invoices.go:540 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:541 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:542 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:404 pkg/invoices.go:550 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 +#: pkg/products.go:211 pkg/invoices.go:551 msgid "You can only select a tax of each class." msgstr "Solo podéis escojer un impuesto de cada clase." -#: pkg/company.go:90 +#: pkg/company.go:98 msgctxt "input" msgid "Currency" msgstr "Moneda" -#: pkg/company.go:108 +#: pkg/company.go:105 +msgctxt "input" +msgid "Invoice number format" +msgstr "Formato del número de factura" + +#: pkg/company.go:111 +msgctxt "input" +msgid "Legal disclaimer" +msgstr "Nota legal" + +#: pkg/company.go:129 msgid "Selected currency is not valid." msgstr "Habéis escogido una moneda que no es válida." -#: pkg/company.go:230 +#: pkg/company.go:130 +msgid "Invoice number format can not be empty." +msgstr "No podéis dejar el formato del número de factura en blanco." + +#: pkg/company.go:291 msgctxt "input" msgid "Tax name" msgstr "Nombre impuesto" -#: pkg/company.go:236 +#: pkg/company.go:297 msgctxt "input" msgid "Tax Class" msgstr "Clase de impuesto" -#: pkg/company.go:239 +#: pkg/company.go:300 msgid "Select a tax class" msgstr "Escoged una clase de impuesto" -#: pkg/company.go:243 +#: pkg/company.go:304 msgctxt "input" msgid "Rate (%)" msgstr "Porcentaje" -#: pkg/company.go:266 +#: pkg/company.go:327 msgid "Tax name can not be empty." msgstr "No podéis dejar el nombre del impuesto en blanco." -#: pkg/company.go:267 +#: pkg/company.go:328 msgid "Selected tax class is not valid." msgstr "Habéis escogido una clase impuesto que no es válida." -#: pkg/company.go:268 +#: pkg/company.go:329 msgid "Tax rate can not be empty." msgstr "No podéis dejar el porcentaje en blanco." -#: pkg/company.go:269 +#: pkg/company.go:330 msgid "Tax rate must be an integer between -99 and 99." msgstr "El porcentaje tiene que estar entre -99 y 99." +#: pkg/company.go:383 +msgctxt "input" +msgid "Payment method name" +msgstr "Nombre del método de pago" + +#: pkg/company.go:389 +msgctxt "input" +msgid "Instructions" +msgstr "Instrucciones" + +#: pkg/company.go:407 +msgid "Payment method name can not be empty." +msgstr "No podéis dejar el nombre del método de pago en blanco." + +#: pkg/company.go:408 +msgid "Payment instructions can not be empty." +msgstr "No podéis dejar las instrucciones de pago en blanco." + #: pkg/profile.go:25 msgctxt "language option" msgid "Automatic" @@ -497,70 +553,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:230 msgid "Select a customer to bill." msgstr "Escoged un cliente a facturar." -#: pkg/invoices.go:322 +#: pkg/invoices.go:323 msgid "Invalid action" msgstr "Acción inválida." -#: pkg/invoices.go:343 +#: pkg/invoices.go:344 msgctxt "input" msgid "Customer" msgstr "Cliente" -#: pkg/invoices.go:349 +#: pkg/invoices.go:350 msgctxt "input" msgid "Number" msgstr "Número" -#: pkg/invoices.go:355 +#: pkg/invoices.go:356 msgctxt "input" msgid "Invoice Date" msgstr "Fecha de factura" -#: pkg/invoices.go:361 +#: pkg/invoices.go:362 msgctxt "input" msgid "Notes" msgstr "Notas" -#: pkg/invoices.go:400 +#: pkg/invoices.go:401 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:402 msgid "Invoice date must be a valid date." msgstr "La fecha de factura debe ser válida." -#: pkg/invoices.go:463 +#: pkg/invoices.go:464 msgctxt "input" msgid "Id" msgstr "Identificador" -#: pkg/invoices.go:486 +#: pkg/invoices.go:487 msgctxt "input" msgid "Quantity" msgstr "Cantidad" -#: pkg/invoices.go:494 +#: pkg/invoices.go:495 msgctxt "input" msgid "Discount (%)" msgstr "Descuento (%)" -#: pkg/invoices.go:543 +#: pkg/invoices.go:544 msgid "Quantity can not be empty." msgstr "No podéis dejar la cantidad en blanco." -#: pkg/invoices.go:544 +#: pkg/invoices.go:545 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:547 msgid "Discount can not be empty." msgstr "No podéis dejar el descuento en blanco." -#: pkg/invoices.go:547 +#: pkg/invoices.go:548 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/payment_method.sql b/revert/payment_method.sql new file mode 100644 index 0000000..c621a2e --- /dev/null +++ b/revert/payment_method.sql @@ -0,0 +1,7 @@ +-- Revert numerus:payment_method from pg + +begin; + +drop table if exists numerus.payment_method; + +commit; diff --git a/sqitch.plan b/sqitch.plan index 9e2bc7c..0c85e29 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -62,3 +62,4 @@ invoice_product_amount [schema_numerus invoice_product invoice_product_tax] 2023 invoice_amount [schema_numerus invoice_product invoice_product_amount] 2023-02-22T12:58:46Z jordi fita mas # Add view to compute subtotal and total for invoices new_invoice_amount [schema_numerus] 2023-02-23T12:08:25Z jordi fita mas # 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 # Add function to compute the subtotal, taxes, and total amounts for a new invoice +payment_method [schema_numerus company] 2023-03-03T15:00:41Z jordi fita mas # Add relation of payment method diff --git a/test/payment_method.sql b/test/payment_method.sql new file mode 100644 index 0000000..657fff7 --- /dev/null +++ b/test/payment_method.sql @@ -0,0 +1,126 @@ +-- Test payment_method +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(36); + +set search_path to numerus, auth, public; + +select has_table('payment_method'); +select has_pk('payment_method' ); +select table_privs_are('payment_method', 'guest', array []::text[]); +select table_privs_are('payment_method', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment_method', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment_method', 'authenticator', array []::text[]); + +select has_sequence('payment_method_payment_method_id_seq'); +select sequence_privs_are('payment_method_payment_method_id_seq', 'guest', array[]::text[]); +select sequence_privs_are('payment_method_payment_method_id_seq', 'invoicer', array['USAGE']); +select sequence_privs_are('payment_method_payment_method_id_seq', 'admin', array['USAGE']); +select sequence_privs_are('payment_method_payment_method_id_seq', 'authenticator', array[]::text[]); + +select has_column('payment_method', 'payment_method_id'); +select col_is_pk('payment_method', 'payment_method_id'); +select col_type_is('payment_method', 'payment_method_id', 'integer'); +select col_not_null('payment_method', 'payment_method_id'); +select col_has_default('payment_method', 'payment_method_id'); +select col_default_is('payment_method', 'payment_method_id', 'nextval(''payment_method_payment_method_id_seq''::regclass)'); + +select has_column('payment_method', 'company_id'); +select col_is_fk('payment_method', 'company_id'); +select fk_ok('payment_method', 'company_id', 'company', 'company_id'); +select col_type_is('payment_method', 'company_id', 'integer'); +select col_not_null('payment_method', 'company_id'); +select col_hasnt_default('payment_method', 'company_id'); + +select has_column('payment_method', 'name'); +select col_type_is('payment_method', 'name', 'text'); +select col_not_null('payment_method', 'name'); +select col_hasnt_default('payment_method', 'name'); + +select has_column('payment_method', 'instructions'); +select col_type_is('payment_method', 'instructions', 'text'); +select col_not_null('payment_method', 'instructions'); +select col_hasnt_default('payment_method', 'instructions'); + + +set client_min_messages to warning; +truncate payment_method 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 payment_method(company_id, name, instructions) +values (2, 'Cash', 'Payment in cash.') + , (2, 'Wire Transfer', 'Please, transfer money to my Nigerian bank') + , (4, 'Pigeon', 'Send money via carrier pigeon.') +; + +prepare payment_method_data as +select company_id, name, instructions +from payment_method; + +set role invoicer; +select is_empty('payment_method_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'payment_method_data', + $$ values ( 2, 'Cash', 'Payment in cash.' ) + , ( 2, 'Wire Transfer', 'Please, transfer money to my Nigerian bank' ) + $$, + 'Should only list payment methods of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'payment_method_data', + $$ values (4, 'Pigeon', 'Send money via carrier pigeon.' ) + $$, + 'Should only list payment methods of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'payment_method_data', + '42501', 'permission denied for table payment_method', + 'Should not allow select to guest users' +); +reset role; + +select throws_ok( $$ + insert into payment_method (company_id, name, instructions) + values (2, ' ', 'atasht') + $$, + '23514', 'new row for relation "payment_method" violates check constraint "name_not_empty"', + 'Should not allow payment methods with blank name' +); + + +select * +from finish(); + +rollback; + diff --git a/verify/payment_method.sql b/verify/payment_method.sql new file mode 100644 index 0000000..c260804 --- /dev/null +++ b/verify/payment_method.sql @@ -0,0 +1,15 @@ +-- Verify numerus:payment_method on pg + +begin; + +select payment_method_id + , company_id + , name + , instructions +from numerus.payment_method +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.payment_method'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.payment_method'::regclass; + +rollback; diff --git a/web/template/tax-details.gohtml b/web/template/tax-details.gohtml index 70cacd7..11e4c05 100644 --- a/web/template/tax-details.gohtml +++ b/web/template/tax-details.gohtml @@ -104,6 +104,63 @@ +
+ {{ csrfToken }} +
+ +
+ + + + + + + + + + + {{ with .PaymentMethods }} + {{- range $method := . }} + + + + + + + {{- end }} + {{ else }} + + + + {{ end }} + + + + + + + + + + + + +
{{( pgettext "Payment Method" "title" )}}{{( pgettext "Instructions" "title" )}}
{{ .Name }}{{ .Instructions }} +
+ {{ csrfToken }} + {{ deleteMethod }} + +
+
{{( gettext "No payment methods added yet." )}}
{{( pgettext "New Line" "title")}} + {{ template "input-field" .NewPaymentMethodForm.Name | addInputAttr `form="new-payment-method"` }} + + {{ template "input-field" .NewPaymentMethodForm.Instructions | addInputAttr `form="new-payment-method"` }} +
+ +
+
+