Compare commits

..

No commits in common. "master" and "v0" have entirely different histories.
master ... v0

398 changed files with 2549 additions and 18434 deletions

View File

@ -2,7 +2,6 @@ package main
import ( import (
"context" "context"
"errors"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -20,12 +19,9 @@ func main() {
} }
defer db.Close() defer db.Close()
var demo bool
_ = db.QueryRow(context.Background(), "select database_is_numerus_demo()").Scan(&demo)
srv := http.Server{ srv := http.Server{
Addr: ":8080", Addr: ":8080",
Handler: numerus.NewRouter(db, demo), Handler: numerus.NewRouter(db),
ReadTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,
IdleTimeout: 2 * time.Minute, IdleTimeout: 2 * time.Minute,
@ -33,7 +29,7 @@ func main() {
go func() { go func() {
log.Printf("INFO - listening on %s\n", srv.Addr) log.Printf("INFO - listening on %s\n", srv.Addr)
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("http server: %v", err) log.Fatalf("http server: %v", err)
} }
}() }()

18
debian/control vendored
View File

@ -10,17 +10,14 @@ Build-Depends:
golang-github-jackc-pgx-v4-dev, golang-github-jackc-pgx-v4-dev,
golang-github-julienschmidt-httprouter-dev, golang-github-julienschmidt-httprouter-dev,
golang-github-leonelquinteros-gotext-dev, golang-github-leonelquinteros-gotext-dev,
golang-github-rainycape-unidecode-dev,
golang-github-tealeg-xlsx-dev,
golang-golang-x-text-dev, golang-golang-x-text-dev,
postgresql-all (>= 217~), postgresql-all (>= 217~),
sqitch, sqitch,
pgtap, pgtap,
postgresql-15-pg-libphonenumber, postgresql-13-pg-libphonenumber,
postgresql-15-pgtap, postgresql-13-pgtap,
postgresql-15-pguri, postgresql-13-pguri,
postgresql-15-vat, postgresql-13-vat
postgresql-15-iban
Standards-Version: 4.6.0 Standards-Version: 4.6.0
XS-Go-Import-Path: dev.tandem.ws/tandem/numerus XS-Go-Import-Path: dev.tandem.ws/tandem/numerus
Vcs-Browser: https://dev.tandem.ws/tandem/numerus Vcs-Browser: https://dev.tandem.ws/tandem/numerus
@ -55,10 +52,9 @@ Package: numerus-sqitch
Architecture: all Architecture: all
Depends: Depends:
${misc:Depends}, ${misc:Depends},
postgresql-15-pg-libphonenumber, postgresql-13-pg-libphonenumber,
postgresql-15-pguri, postgresql-13-pguri,
postgresql-15-vat, postgresql-13-vat,
postgresql-15-iban,
sqitch sqitch
Description: Simple invoicing and accounting web application Description: Simple invoicing and accounting web application
A simple web application to keep invoice and accouting records, intended for A simple web application to keep invoice and accouting records, intended for

View File

@ -1,40 +0,0 @@
#!/bin/sh
set -e
. /usr/share/debconf/confmodule
case "$1" in
configure)
# Create numerus user and group
if ! getent group numerus >/dev/null; then
addgroup --system --quiet numerus
fi
if ! getent passwd numerus >/dev/null; then
adduser --quiet \
--system \
--disabled-login \
--no-create-home \
--shell /bin/bash \
--ingroup numerus \
--home /usr/share/numerus \
--gecos "Numerus Daemon" \
numerus
fi
# Make sure log directory has correct permissions set
dpkg-statoverride --list "/var/log/numerus" >/dev/null || \
dpkg-statoverride --add --force --quiet --update numerus adm 0750 /var/log/numerus
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

View File

@ -1,15 +0,0 @@
[Unit]
Description=Numerus application server
Requires=postgresql.service
After=postgresql.service
[Service]
Type=simple
User=numerus
Group=numerus
WorkingDirectory=/usr/share/numerus
EnvironmentFile=-/etc/default/numerus
ExecStart=/usr/bin/numerus
Restart=always
StandardOutput=append:/var/log/numerus/access.log
StandardError=append:/var/log/numerus/error.log

6
debian/rules vendored
View File

@ -1,14 +1,10 @@
#!/usr/bin/make -f #!/usr/bin/make -f
include /usr/share/dpkg/pkg-info.mk
%: %:
dh $@ --builddirectory=_build --buildsystem=golang --with=golang dh $@ --builddirectory=_build --buildsystem=golang --with=golang
execute_before_dh_auto_build: execute_before_dh_auto_build:
printf 'package pkg\n\nconst Version = "%s"\n' "$(DEB_VERSION_UPSTREAM)" > pkg/build.go
cp pkg/build.go _build/src/dev.tandem.ws/tandem/numerus/pkg/build.go
make make
execute_after_dh_auto_test: execute_after_dh_auto_test:
pg_virtualenv -v 15 make test-deploy pg_virtualenv -v 13 make test-deploy

View File

@ -2,9 +2,7 @@ begin;
set search_path to auth, numerus, public; set search_path to auth, numerus, public;
create or replace function public.database_is_numerus_demo() returns bool as $$ select true $$ language sql; alter sequence user_user_id_seq restart;
alter sequence user_user_id_seq restart with 123;
insert into auth."user" (email, name, password, role) insert into auth."user" (email, name, password, role)
values ('demo@numerus', 'Demo User', 'demo', 'invoicer') values ('demo@numerus', 'Demo User', 'demo', 'invoicer')
, ('admin@numerus', 'Demo Admin', 'admin', 'admin') , ('admin@numerus', 'Demo Admin', 'admin', 'admin')
@ -12,188 +10,65 @@ values ('demo@numerus', 'Demo User', 'demo', 'invoicer')
set constraints "company_default_payment_method_id_fkey" deferred; set constraints "company_default_payment_method_id_fkey" deferred;
alter sequence company_company_id_seq restart with 123; alter sequence company_company_id_seq restart;
insert into company (business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id, legal_disclaimer) insert into company (business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id, legal_disclaimer)
values ('Juli Verd', 'ES40404040D', 'Pessebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de lHort', 'Castelló dEmpúries', 'Girona', '17486', 'ES', 'EUR', 124, 'Juli Verd és responsable del tractament de les seves dades dacord amb el RGPD i la LOPDGDD, i les tracta per a mantenir una relació mercantil/comercial amb vostè. Les conservarà mentre es mantingui aquesta relació i no es comunicaran a tercers. Pot exercir els drets daccés, rectificació, portabilitat, supressió, limitació i oposició a Juli Verd, amb domicili Carrer de lHort 71, 17486 Castelló dEmpúries o enviant un correu electrònic a info@numerus.cat. Per a qualsevol reclamació pot acudir a agpd.es. Per a més informació pot consultar la nostra política de privacitat a numerus.cat.') values ('Juli Verd', 'ES40404040D', 'Pesebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de lHort', 'Castelló dEmpúries', 'Girona', '17486', 'ES', 'EUR', 2, 'Juli Verd és responsable del tractament de les seves dades dacord amb el RGPD i la LOPDGDD, i les tracta per a mantenir una relació mercantil/comercial amb vostè. Les conservarà mentre es mantingui aquesta relació i no es comunicaran a tercers. Pot exercir els drets daccés, rectificació, portabilitat, supressió, limitació i oposició a Juli Verd, amb domicili Carrer de lHort 71, 17486 Castelló dEmpúries o enviant un correu electrònic a info@numerus.cat. Per a qualsevol reclamació pot acudir a agpd.es. Per a més informació pot consultar la nostra política de privacitat a numerus.cat.');
, ('Pere Gil', 'ES41414141L', 'Betlem', parse_packed_phone_number('972 80 90 00', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de lHort', 'Castelló dEmpúries', 'Girona', '17486', 'ES', 'EUR', 126, 'Pere Gil és responsable del tractament de les seves dades dacord amb el RGPD i la LOPDGDD, i les tracta per a mantenir una relació mercantil/comercial amb vostè. Les conservarà mentre es mantingui aquesta relació i no es comunicaran a tercers. Pot exercir els drets daccés, rectificació, portabilitat, supressió, limitació i oposició a Pere Gil, amb domicili Carrer de lHort 71, 17486 Castelló dEmpúries o enviant un correu electrònic a info@numerus.cat. Per a qualsevol reclamació pot acudir a agpd.es. Per a més informació pot consultar la nostra política de privacitat a numerus.cat.')
;
alter sequence payment_method_payment_method_id_seq restart with 123; alter sequence payment_method_payment_method_id_seq restart;
insert into payment_method (company_id, name, instructions) insert into payment_method (company_id, name, instructions)
values (123, 'Efectiu', 'Pagament en efectiu al comptat.') values (1, 'Efectiu', 'Pagament en efectiu al comptat.')
, (123, 'Transferència', E'Pagament per transferència bancària al compte:\n\nES0123456789012345678901\n\nBIC AAAABBCCDD') , (1, 'Transferència', E'Pagament per transferència bancària al compte:\n\nES0123456789012345678901\n\nBIC AAAABBCCDD')
, (124, 'Efectiu', 'Pagament en efectiu al comptat.')
, (124, 'Transferència', E'Pagament per transferència bancària al compte:\n\nES0123456789012345678901\n\nBIC AAAABBCCDD')
; ;
set constraints "company_default_payment_method_id_fkey" immediate; set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id) insert into company_user (company_id, user_id)
values (123, 123) values (1, 1)
, (123, 124) , (1, 2)
, (124, 123)
, (124, 124)
; ;
alter sequence tax_class_tax_class_id_seq restart with 123; alter sequence tax_class_tax_class_id_seq restart;
insert into tax_class (company_id, name) insert into tax_class (company_id, name)
values (123, 'IRPF') values (1, 'IRPF')
, (123, 'IVA') , (1, 'IVA')
, (124, 'IRPF')
, (124, 'IVA')
; ;
alter sequence tax_tax_id_seq restart with 123; alter sequence tax_tax_id_seq restart;
insert into tax (company_id, tax_class_id, name, rate) insert into tax (company_id, tax_class_id, name, rate)
values (123, 123, 'Retenció 15 %', -0.15) values (1, 1, 'Retenció 15 %', -0.15)
, (123, 124, 'IVA 21 %', 0.21) , (1, 2, 'IVA 21 %', 0.21)
, (123, 124, 'IVA 10 %', 0.10) , (1, 2, 'IVA 10 %', 0.10)
, (123, 124, 'IVA 4 %', 0.04) , (1, 2, 'IVA 4 %', 0.04)
, (124, 123, 'Retenció 15 %', -0.15)
, (124, 124, 'IVA 21 %', 0.21)
, (124, 124, 'IVA 10 %', 0.10)
, (124, 124, 'IVA 4 %', 0.04)
; ;
alter sequence contact_contact_id_seq restart with 123; alter sequence contact_contact_id_seq restart;
-- customers select add_contact (1, 'Melcior', '1', 'Rei Blanc', '0732621', 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR', array['pesebre', 'mag']);
select add_contact (123, 'Melcior', '0732621', 'melcio@reismags.cat', '', '(Rei Blanc,1,"C/ Principal, 1",Shiraz,Fars,1,IR)', '', '', array['pesebre', 'mag', 'client']); select add_contact (1, 'Gaspar', '2', 'Rei Ros', '111', 'gaspar@reismags.cat', '', 'C/ Principal, 2', 'Nova Delhi', 'Delhi', '2', 'IN', array['pesebre', 'mag']);
select add_contact (123, 'Gaspar', '111', 'gaspar@reismags.cat', '', '(Rei Ros,2,"C/ Principal, 2",Nova Delhi,Delhi,2,IN)', '', '', array['pesebre', 'mag', 'client']); select add_contact (1, 'Baltasar', '3', 'Rei Negre', '1-111-111', 'baltasar@reismags.cat', '', 'C/ Principal, 3', 'Sanaa', 'Sanaa', '3', 'YE', array['pesebre', 'mag']);
select add_contact (123, 'Baltasar', '1-111-111', 'baltasar@reismags.cat', '', '(Rei Negre,3,"C/ Principal, 3",Sanaa,Sanaa,3,YE)', '', '', array['pesebre', 'mag', 'client']); select add_contact (1, 'Caganera', '41414141L', '', '222 222 222', 'caganera@pesebre.cat', '', 'C/ De lHort, 4', 'Olot', 'Girona', '17800', 'ES', array['pesebre', 'persona']);
select add_contact (123, 'Caganera', '222 222 222', 'caganera@pesebre.cat', '', '(Caganera,41414141L,"C/ De lHort, 4",Olot,Girona,17800,ES)', '', '', array['pesebre', 'persona', 'client']); select add_contact (1, 'Bou', '41414142C', '', '333 333 333', 'bou@pesebre.cat', '', 'C/ De la Palla, 5', 'Sant Climent Sescebes', 'Girona', '17751', 'ES', array['pesebre', 'bestia']);
select add_contact (123, 'Bou', '333 333 333', 'bou@pesebre.cat', '', '(Bou,41414142C,"C/ De la Palla, 5",Sant Climent Sescebes,Girona,17751,ES)', '', '', array['pesebre', 'bestia', 'client']); select add_contact (1, 'Rabadà', '41414143K', '', '444 444 444', 'rabada@pesebre.cat', '', 'C/ De les Ovelles, 6', 'Fornells de la Selva', 'Girona', '17458', 'ES', array['pesebre', 'persona']);
select add_contact (123, 'Rabadà', '444 444 444', 'rabada@pesebre.cat', '', '(Rabadà,41414143K,"C/ De les Ovelles, 6",Fornells de la Selva,Girona,17458,ES)', '', '', array['pesebre', 'persona', 'client']);
-- suppliers
select add_contact(123, 'TGSS', '', '', '', null, '', '', array['govern']);
select add_contact(123, 'Quadre Estable', '', '', '', null, '', '', array['proveidor']);
select add_contact(123, 'De tot i +', '', '', '', null, '', '', array['proveidor']);
select add_contact(123, 'Els números', '', '', '', null, '', '', array['gestoria']);
alter sequence product_product_id_seq restart with 123; alter sequence product_product_id_seq restart;
select add_product(123, 'Or', 'Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.', '55.92', array[124], array['metall']); select add_product(1, 'Or', 'Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.', '55.92', array[2], array['metall']);
select add_product(123, 'Encens', 'Goma resina fragrant que desprèn una olor característica quan es crema.', '2.15', array[124], array['resina']); select add_product(1, 'Encens', 'Goma resina fragrant que desprèn una olor característica quan es crema.', '2.15', array[2], array['resina']);
select add_product(123, 'Mirra', 'Goma resinosa aromàtica de color gris groguenc i gust amargant.', '6.90', array[124], array['resina']); select add_product(1, 'Mirra', 'Goma resinosa aromàtica de color gris groguenc i gust amargant.', '6.90', array[2], array['resina']);
select add_product(123, 'Paper higiènic (pack de 32 U)', 'Paper que susa per mantenir la higiene personal després de defecar o orinar.', '7.99', array[126], array['necessitat']); select add_product(1, 'Paper higiènic (pack de 32 U)', 'Paper que susa per mantenir la higiene personal després de defecar o orinar.', '7.99', array[4], array['necessitat']);
select add_product(123, 'Cavall Fort', 'Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.', '3.64', array[124], array['revista']); select add_product(1, 'Cavall Fort', 'Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.', '3.64', array[2], array['revista']);
select add_product(123, 'Palla', 'Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.', '25.00', array[125], array['necessitat']); select add_product(1, 'Palla', 'Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.', '25.00', array[3], array['necessitat']);
select add_product(123, 'Teia', 'Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.', '7.00', array[124], array['obsolet']); select add_product(1, 'Teia', 'Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.', '7.00', array[2], array['obsolet']);
alter table payment_account alter column payment_account_id restart with 123; alter sequence invoice_invoice_id_seq restart;
select add_payment_account_bank(123, 'Guardiola', 'ES2820958297603648596978'); alter sequence invoice_product_invoice_product_id_seq restart;
select add_payment_account_cash(123, 'Matalàs'); select add_invoice(1, (current_date - '28 days'::interval)::date, 6, 'Vol esmorzar!', 1, '{producte}','{"(1,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}');
select add_invoice(1, (current_date - '24 days'::interval)::date, 5, '', 1, '{producte,bestia}','{"(1,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{3})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}');
select add_invoice(1, (current_date - '17 days'::interval)::date, 4, '', 1, '{producte,higiene}','{"(1,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{4})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}');
select add_invoice(1, (current_date - '7 days'::interval)::date, 3, '', 1, '{producte,mag}','{"(1,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{2})"}');
select add_invoice(1, (current_date - '4 days'::interval)::date, 2, '', 1, '{producte,mag}','{"(1,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{2})"}');
select add_invoice(1, (current_date - '1 days'::interval)::date, 1, '', 1, '{producte,mag}','{"(1,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{2})"}');
alter sequence invoice_invoice_id_seq restart with 123; update invoice set invoice_status = 'paid' where invoice_id in (1, 5);
alter sequence invoice_product_invoice_product_id_seq restart with 123; update invoice set invoice_status = 'unpaid' where invoice_id = 3;
select add_invoice(123, (current_date - '338 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}'); update invoice set invoice_status = 'sent' where invoice_id = 4;
select add_collection(123, 123, (current_date - '337 days'::interval)::date, 123, 'Cobrament de FRA123', '1200.50', '{}');
select add_invoice(123, (current_date - '334 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}');
select add_collection(123, 124, (current_date - '330 days'::interval)::date, 123, 'Cobrament de FRA124', '1200.50', '{}');
select add_invoice(123, (current_date - '327 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
select add_collection(123, 125, (current_date - '317 days'::interval)::date, 123, 'Cobrament de FRA125', '1200.50', '{}');
select add_invoice(123, (current_date - '317 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 126, (current_date - '316 days'::interval)::date, 124, 'Cobrament de FRA126', '12.87', '{}');
select add_invoice(123, (current_date - '314 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
select add_collection(123, 127, (current_date - '310 days'::interval)::date, 123, 'Cobrament de FRA127', '1200.50', '{}');
select add_invoice(123, (current_date - '311 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 128, (current_date - '311 days'::interval)::date, 124, 'Cobrament de FRA128', '12.87', '{}');
select add_invoice(123, (current_date - '278 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}');
select add_collection(123, 129, (current_date - '270 days'::interval)::date, 123, 'Cobrament de FRA129', '1200.50', '{}');
select add_invoice(123, (current_date - '274 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 130, (current_date - '272 days'::interval)::date, 123, 'Cobrament de FRA130', '691.90', '{}');
select add_invoice(123, (current_date - '267 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 131, (current_date - '265 days'::interval)::date, 123, 'Cobrament de FRA131', '87.50', '{}');
select add_invoice(123, (current_date - '257 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 132, (current_date - '250 days'::interval)::date, 124, 'Cobrament de FRA132', '87.50', '{}');
select add_invoice(123, (current_date - '254 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 133, (current_date - '220 days'::interval)::date, 123, 'Cobrament de FRA133', '691.90', '{}');
select add_invoice(123, (current_date - '251 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}');
select add_collection(123, 134, (current_date - '245 days'::interval)::date, 123, 'Cobrament de FRA134', '1200.50', '{}');
select add_invoice(123, (current_date - '208 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 135, (current_date - '190 days'::interval)::date, 123, 'Cobrament de FRA135', '691.90', '{}');
select add_invoice(123, (current_date - '204 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 136, (current_date - '200 days'::interval)::date, 123, 'Cobrament de FRA136', '691.90', '{}');
select add_invoice(123, (current_date - '197 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}');
select add_collection(123, 137, (current_date - '190 days'::interval)::date, 123, 'Cobrament de FRA137', '1200.50', '{}');
select add_invoice(123, (current_date - '187 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 138, (current_date - '186 days'::interval)::date, 124, 'Cobrament de FRA138', '12.87', '{}');
select add_invoice(123, (current_date - '184 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
select add_collection(123, 139, (current_date - '181 days'::interval)::date, 123, 'Cobrament de FRA139', '1200.50', '{}');
select add_invoice(123, (current_date - '181 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}');
select add_collection(123, 140, (current_date - '177 days'::interval)::date, 123, 'Cobrament de FRA140', '1200.50', '{}');
select add_invoice(123, (current_date - '148 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 141, (current_date - '140 days'::interval)::date, 123, 'Cobrament de FRA141', '87.50', '{}');
select add_invoice(123, (current_date - '144 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 142, (current_date - '143 days'::interval)::date, 123, 'Cobrament de FRA142', '87.50', '{}');
select add_invoice(123, (current_date - '137 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
select add_collection(123, 143, (current_date - '137 days'::interval)::date, 123, 'Cobrament de FRA143', '1200.50', '{}');
select add_invoice(123, (current_date - '127 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}');
select add_collection(123, 144, (current_date - '120 days'::interval)::date, 123, 'Cobrament de FRA144', '1200.50', '{}');
select add_invoice(123, (current_date - '124 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}');
select add_collection(123, 145, (current_date - '123 days'::interval)::date, 123, 'Cobrament de FRA145', '1200.50', '{}');
select add_invoice(123, (current_date - '121 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 146, (current_date - '101 days'::interval)::date, 124, 'Cobrament de FRA146', '12.87', '{}');
select add_invoice(123, (current_date - '78 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 147, (current_date - '60 days'::interval)::date, 123, 'Cobrament de FRA147', '87.50', '{}');
select add_invoice(123, (current_date - '74 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}');
select add_collection(123, 148, (current_date - '61 days'::interval)::date, 123, 'Cobrament de FRA148', '1200.50', '{}');
select add_invoice(123, (current_date - '67 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 149, (current_date - '66 days'::interval)::date, 124, 'Cobrament de FRA149', '12.87', '{}');
select add_invoice(123, (current_date - '57 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
select add_collection(123, 150, (current_date - '55 days'::interval)::date, 123, 'Cobrament de FRA150', '1200.50', '{}');
select add_invoice(123, (current_date - '54 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 151, (current_date - '50 days'::interval)::date, 124, 'Cobrament de FRA151', '691.90', '{}');
select add_invoice(123, (current_date - '51 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}');
select add_collection(123, 152, (current_date - '44 days'::interval)::date, 123, 'Cobrament de FRA152', '1200.50', '{}');
select add_invoice(123, (current_date - '28 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 153, (current_date - '28 days'::interval)::date, 124, 'Cobrament de FRA153', '12.87', '{}');
select add_invoice(123, (current_date - '24 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_invoice(123, (current_date - '17 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_invoice(123, (current_date - '7 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}');
select add_invoice(123, (current_date - '4 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}');
select add_collection(123, 157, (current_date - '2 days'::interval)::date, 123, 'Primer cobrament de FRA157', '1000.00', '{}');
select add_invoice(123, (current_date - '1 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
alter sequence expense_expense_id_seq restart with 123;
select add_expense(123, (date_trunc('month', current_date) - '11 months + 14 day'::interval)::date, 130, 'ABC123', '256.12', '{124}', '{}');
select add_payment(123, 123, (date_trunc('month', current_date) - '11 months + 04 day'::interval)::date, 123, 'Pagament dABC123', '256.12', '{}');
select add_expense(123, (date_trunc('month', current_date) - '11 months + 8 day'::interval)::date, 131, '123ABC', '1023.17', '{124}', '{}');
select add_payment(123, 124, (date_trunc('month', current_date) - '11 months'::interval)::date, 123, 'Pagament d123ABC', '1023.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '11 months + 1 day'::interval)::date, 129, 'N CMDPGGNZG', '299.17', '{}', array['autonoms']);
select add_payment(123, 125, (date_trunc('month', current_date) - '10 months + 15 day'::interval)::date, 123, 'Pagament dN CMDPGGNZG', '299.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '10 months + 20 day'::interval)::date, 131, '123XYZ', '23.17', '{124}', '{}');
select add_payment(123, 126, (date_trunc('month', current_date) - '10 months + 15 day'::interval)::date, 124, 'Pagament d123XYZ', '23.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '10 months + 1 day'::interval)::date, 129, 'N QHVLDAN29', '299.17', '{}', array['autonoms']);
select add_payment(123, 127, (date_trunc('month', current_date) - '10 months'::interval)::date, 123, 'Pagament dN QHVLDAN29', '299.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '9 months + 2 day'::interval)::date, 130, 'XYZ123', '62.21', '{124}', '{}');
select add_payment(123, 128, (date_trunc('month', current_date) - '8 months + 28 day'::interval)::date, 123, 'Pagament dXYZ123', '62.21', '{}');
select add_expense(123, (date_trunc('month', current_date) - '9 months + 1 day'::interval)::date, 129, 'N WXMHH1R5Q', '299.17', '{}', array['autonoms']);
select add_payment(123, 129, (date_trunc('month', current_date) - '8 months + 28 day'::interval)::date, 123, 'Pagament dN WXMHH1R5Q', '299.17', '{}');
select add_expense(123, (current_date - '9 months'::interval)::date, 132, '00/0001', '117.74', '{124}', array['gestor']);
select add_payment(123, 130, (date_trunc('month', current_date) - '8 months + 15 day'::interval)::date, 124, 'Pagament de 00/0001', '117.74', '{}');
select add_expense(123, (date_trunc('month', current_date) - '8 months + 1 day'::interval)::date, 129, 'N NRP28PWY8', '299.17', '{}', array['autonoms']);
select add_payment(123, 131, (date_trunc('month', current_date) - '8 months'::interval)::date, 123, 'Pagament dN NRP28PWY8', '299.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '7 months + 1 day'::interval)::date, 129, 'N D256225DF', '299.17', '{}', array['autonoms']);
select add_payment(123, 132, (date_trunc('month', current_date) - '6 months + 15 day'::interval)::date, 123, 'Pagament dN D256225DF', '299.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '6 months + 15 day'::interval)::date, 130, 'ZZZ888', '162.21', '{124}', '{}');
select add_payment(123, 133, (date_trunc('month', current_date) - '6 months + 15 day'::interval)::date, 124, 'Pagament de ZZZ888', '80.00', '{}');
select add_expense(123, (date_trunc('month', current_date) - '6 months + 1 day'::interval)::date, 129, 'N K90XS7C3Q', '299.17', '{}', array['autonoms']);
select add_payment(123, 134, (date_trunc('month', current_date) - '5 months + 25 day'::interval)::date, 123, 'Pagament dN K90XS7C3Q', '299.17', '{}');
select add_expense(123, (current_date - '6 months'::interval)::date, 132, '00/0054', '117.74', '{124}', array['gestor']);
select add_payment(123, 135, (date_trunc('month', current_date) - '5 months + 29 day'::interval)::date, 123, 'Pagament dN ', '299.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '5 months + 1 day'::interval)::date, 129, 'N MCPDGGZNG', '299.17', '{}', array['autonoms']);
select add_payment(123, 136, (date_trunc('month', current_date) - '4 months + 29 day'::interval)::date, 123, 'Pagament dN MCPDGGZNG', '299.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '4 months + 1 day'::interval)::date, 129, 'N HQLVAD2N9', '299.17', '{}', array['autonoms']);
select add_payment(123, 137, (date_trunc('month', current_date) - '4 months + 1 day'::interval)::date, 123, 'Pagament dN HQLVAD2N9', '299.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '3 months + 1 day'::interval)::date, 129, 'N QXWHM1H5R', '299.17', '{}', array['autonoms']);
select add_payment(123, 138, (date_trunc('month', current_date) - '2 months + 15 day'::interval)::date, 123, 'Pagament dN QXWHM1H5R', '299.17', '{}');
select add_expense(123, (current_date - '3 months'::interval)::date, 132, '00/0331', '117.74', '{124}', array['gestor']);
select add_expense(123, (date_trunc('month', current_date) - '2 months + 1 day'::interval)::date, 129, 'N 8RN2PP8YW', '299.17', '{}', array['autonoms']);
select add_payment(123, 140, (date_trunc('month', current_date) - '1 months'::interval)::date, 123, 'Pagament dN 8RN2PP8YW', '299.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '1 months + 1 day'::interval)::date, 129, 'N F2D6522D5', '299.17', '{}', array['autonoms']);
select add_payment(123, 141, (date_trunc('month', current_date) - '1 months + 1 day'::interval)::date, 123, 'Pagament dN F2D6522D5', '299.17', '{}');
select add_expense(123, (date_trunc('month', current_date) - '1 day'::interval)::date, 129, 'N F2D6522D5', '299.17', '{}', array['autonoms']);
select add_payment(123, 142, (date_trunc('month', current_date) - '18 day'::interval)::date, 123, 'Pagament dN F2D6522D5', '299.17', '{}');
select add_expense(123, (current_date - '22 day'::interval)::date, 131, '321ABC', '1023.17', '{124}', '{}');
select add_expense(123, (current_date - '11 day'::interval)::date, 130, 'ABC321', '256.12', '{124}', '{}');
commit; commit;

View File

@ -1,45 +0,0 @@
-- Deploy numerus:_merge_collection_into_payment to pg
-- requires: collection
-- requires: collection_attachment
-- requires: invoice_collection
-- requires: payment
begin;
set search_path to numerus, public;
alter table payment
drop constraint if exists payment_amount_positive
, add constraint payment_amount_not_zero check (amount <> 0)
;
update payment
set amount = -amount
;
insert into payment (company_id, slug, description, payment_date, payment_account_id, amount, currency_code, tags, payment_status, created_at)
select company_id, slug, description, collection_date, payment_account_id, amount, currency_code, tags, payment_status, created_at
from collection
;
insert into payment_attachment (payment_id, original_filename, mime_type, content)
select payment_id, original_filename, mime_type, content
from collection_attachment
join collection using (collection_id)
join payment using (slug)
;
drop table collection_attachment;
insert into invoice_payment (invoice_id, payment_id)
select invoice_id, payment_id
from invoice_collection
join collection using (collection_id)
join payment using (slug)
;
drop table invoice_collection;
drop table collection;
commit;

View File

@ -1,68 +0,0 @@
-- Deploy numerus:add_collection to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment
-- requires: invoice_payment
-- requires: company
-- requires: currency
-- requires: parse_price
-- requires: tag_name
-- requires: update_invoice_collection_status
begin;
set search_path to numerus, public;
create or replace function add_collection(company integer, invoice_id integer, collection_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
$$
declare
cid integer;
cslug uuid;
amount_cents integer;
begin
insert into payment
( company_id
, payment_account_id
, description
, payment_date
, amount
, currency_code
, payment_status
, tags
)
select company_id
, payment_account_id
, description
, collection_date
, parse_price(amount, currency.decimal_digits)
, currency_code
, 'complete'
, tags
from company
join currency using (currency_code)
where company.company_id = add_collection.company
returning payment_id, slug, payment.amount
into cid, cslug, amount_cents
;
if invoice_id is not null then
-- must be inserted before updating statuses, so that it can see this
-- collections amount too.
insert into invoice_payment (invoice_id, payment_id)
values (invoice_id, cid)
;
perform update_invoice_collection_status(cid, invoice_id, amount_cents);
end if;
return cslug;
end
$$
language plpgsql
;
revoke execute on function add_collection(integer, integer, date, integer, text, text, tag_name[]) from public;
grant execute on function add_collection(integer, integer, date, integer, text, text, tag_name[]) to invoicer;
grant execute on function add_collection(integer, integer, date, integer, text, text, tag_name[]) to admin;
commit;

View File

@ -1,68 +0,0 @@
-- Deploy numerus:add_collection to pg
-- requires: roles
-- requires: schema_numerus
-- requires: collection
-- requires: invoice_collection
-- requires: company
-- requires: currency
-- requires: parse_price
-- requires: tag_name
-- requires: update_invoice_collection_status
begin;
set search_path to numerus, public;
create or replace function add_collection(company integer, invoice_id integer, collection_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
$$
declare
cid integer;
cslug uuid;
amount_cents integer;
begin
insert into collection
( company_id
, payment_account_id
, description
, collection_date
, amount
, currency_code
, payment_status
, tags
)
select company_id
, payment_account_id
, description
, collection_date
, parse_price(amount, currency.decimal_digits)
, currency_code
, 'complete'
, tags
from company
join currency using (currency_code)
where company.company_id = add_collection.company
returning collection_id, slug, collection.amount
into cid, cslug, amount_cents
;
if invoice_id is not null then
-- must be inserted before updating statuses, so that it can see this
-- collections amount too.
insert into invoice_collection (invoice_id, collection_id)
values (invoice_id, cid)
;
perform update_invoice_collection_status(cid, invoice_id, amount_cents);
end if;
return cslug;
end
$$
language plpgsql
;
revoke execute on function add_collection(integer, integer, date, integer, text, text, tag_name[]) from public;
grant execute on function add_collection(integer, integer, date, integer, text, text, tag_name[]) to invoicer;
grant execute on function add_collection(integer, integer, date, integer, text, text, tag_name[]) to admin;
commit;

View File

@ -7,65 +7,21 @@
-- requires: country_code -- requires: country_code
-- requires: contact -- requires: contact
-- requires: tag_name -- requires: tag_name
-- requires: tax_details
-- requires: contact_web
-- requires: contact_phone
-- requires: contact_tax_details
-- requires: contact_iban
-- requires: contact_bic
begin; begin;
set search_path to numerus, public; set search_path to numerus, public;
create or replace function add_contact(company_id integer, name text, phone text, email text, web text, tax_details tax_details, iban text, bic text, tags tag_name[]) returns uuid as create or replace function add_contact(company_id integer, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as
$$ $$
declare declare
cid integer; cid integer;
cslug uuid; cslug uuid;
begin begin
insert into contact (company_id, name, tags) insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, tags)
values (add_contact.company_id, add_contact.name, add_contact.tags) values (add_contact.company_id, add_contact.business_name, (add_contact.country_code || add_contact.vatin)::vatin, add_contact.trade_name, parse_packed_phone_number(add_contact.phone, add_contact.country_code), add_contact.email, add_contact.web, add_contact.address, add_contact.city, add_contact.province, add_contact.postal_code, add_contact.country_code, add_contact.tags)
returning contact_id, slug returning contact_id, slug
into cid, cslug into cid, cslug;
;
if tax_details is not null then
insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code)
values (cid, tax_details.business_name, (tax_details.country_code || tax_details.vatin)::vatin, tax_details.address, tax_details.city, tax_details.province, tax_details.postal_code, tax_details.country_code)
;
end if;
if phone is not null and trim(phone) <> '' then
insert into contact_phone (contact_id, phone)
values (cid, parse_packed_phone_number(add_contact.phone, coalesce(tax_details.country_code, 'ES')))
;
end if;
if email is not null and trim(email) <> '' then
insert into contact_email (contact_id, email)
values (cid, add_contact.email)
;
end if;
if web is not null and trim(web) <> '' then
insert into contact_web (contact_id, uri)
values (cid, add_contact.web)
;
end if;
if iban is not null and trim(iban) <> '' then
insert into contact_iban (contact_id, iban)
values (cid, add_contact.iban)
;
end if;
if bic is not null and trim(bic) <> '' then
insert into contact_swift (contact_id, bic)
values (cid, add_contact.bic)
;
end if;
return cslug; return cslug;
end end
@ -73,10 +29,8 @@ $$
language plpgsql language plpgsql
; ;
revoke execute on function add_contact(integer, text, text, text, text, tax_details, text, text, tag_name[]) from public; revoke execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public;
grant execute on function add_contact(integer, text, text, text, text, tax_details, text, text, tag_name[]) to invoicer; grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer;
grant execute on function add_contact(integer, text, text, text, text, tax_details, text, text, tag_name[]) to admin; grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin;
drop function if exists add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]);
commit; commit;

View File

@ -1,36 +0,0 @@
-- Deploy numerus:add_contact to pg
-- requires: schema_numerus
-- requires: extension_vat
-- requires: email
-- requires: extension_pg_libphonenumber
-- requires: extension_uri
-- requires: country_code
-- requires: contact
-- requires: tag_name
begin;
set search_path to numerus, public;
create or replace function add_contact(company_id integer, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as
$$
declare
cid integer;
cslug uuid;
begin
insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, tags)
values (add_contact.company_id, add_contact.business_name, (add_contact.country_code || add_contact.vatin)::vatin, add_contact.trade_name, parse_packed_phone_number(add_contact.phone, add_contact.country_code), add_contact.email, add_contact.web, add_contact.address, add_contact.city, add_contact.province, add_contact.postal_code, add_contact.country_code, add_contact.tags)
returning contact_id, slug
into cid, cslug;
return cslug;
end
$$
language plpgsql
;
revoke execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public;
grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer;
grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin;
commit;

View File

@ -8,15 +8,11 @@
-- requires: parse_price -- requires: parse_price
-- requires: tax -- requires: tax
-- requires: tag_name -- requires: tag_name
-- requires: expense_status
-- requires: expense_expense_status
begin; begin;
set search_path to numerus, public; set search_path to numerus, public;
drop function if exists add_expense(integer, text, date, integer, text, text, integer[], tag_name[]);
create or replace function add_expense(company integer, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as create or replace function add_expense(company integer, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$ $$
declare declare

View File

@ -1,50 +0,0 @@
-- Deploy numerus:add_expense to pg
-- requires: schema_numerus
-- requires: expense
-- requires: expense_tax
-- requires: tax
-- requires: company
-- requires: currency
-- requires: parse_price
-- requires: tax
-- requires: tag_name
begin;
set search_path to numerus, public;
create or replace function add_expense(company integer, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$
declare
eid integer;
eslug uuid;
begin
insert into expense (company_id, contact_id, invoice_number, invoice_date, amount, currency_code, tags)
select company_id
, contact_id
, invoice_number
, invoice_date
, parse_price(amount, currency.decimal_digits)
, currency_code
, tags
from company
join currency using (currency_code)
where company.company_id = add_expense.company
returning expense_id, slug
into eid, eslug;
insert into expense_tax (expense_id, tax_id, tax_rate)
select eid, tax_id, tax.rate
from tax
join unnest(taxes) as etax(tax_id) using (tax_id);
return eslug;
end;
$$
language plpgsql;
revoke execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) from public;
grant execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) to invoicer;
grant execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) to admin;
commit;

View File

@ -1,55 +0,0 @@
-- Deploy numerus:add_expense to pg
-- requires: schema_numerus
-- requires: expense
-- requires: expense_tax
-- requires: tax
-- requires: company
-- requires: currency
-- requires: parse_price
-- requires: tax
-- requires: tag_name
-- requires: expense_status
-- requires: expense_expense_status
begin;
set search_path to numerus, public;
create or replace function add_expense(company integer, status text, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$
declare
eid integer;
eslug uuid;
begin
insert into expense (company_id, contact_id, invoice_number, invoice_date, amount, currency_code, expense_status, tags)
select company_id
, contact_id
, invoice_number
, invoice_date
, parse_price(amount, currency.decimal_digits)
, currency_code
, status
, tags
from company
join currency using (currency_code)
where company.company_id = add_expense.company
returning expense_id, slug
into eid, eslug;
insert into expense_tax (expense_id, tax_id, tax_rate)
select eid, tax_id, tax.rate
from tax
join unnest(taxes) as etax(tax_id) using (tax_id);
return eslug;
end;
$$
language plpgsql;
revoke execute on function add_expense(integer, text, date, integer, text, text, integer[], tag_name[]) from public;
grant execute on function add_expense(integer, text, date, integer, text, text, integer[], tag_name[]) to invoicer;
grant execute on function add_expense(integer, text, date, integer, text, text, integer[], tag_name[]) to admin;
drop function if exists add_expense(integer, date, integer, text, text, integer[], tag_name[]);
commit;

View File

@ -1,67 +0,0 @@
-- Deploy numerus:add_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment
-- requires: expense_payment
-- requires: company
-- requires: currency
-- requires: parse_price
-- requires: tag_name
-- requires: update_expense_payment_status
begin;
set search_path to numerus, public;
create or replace function add_payment(company integer, expense_id integer, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
$$
declare
pslug uuid;
pid integer;
amount_cents integer;
begin
insert into payment
( company_id
, payment_account_id
, description
, payment_date
, amount
, currency_code
, payment_status
, tags
)
select company_id
, payment_account_id
, description
, payment_date
, -parse_price(amount, currency.decimal_digits)
, currency_code
, 'complete'
, tags
from company
join currency using (currency_code)
where company.company_id = add_payment.company
returning payment_id, slug, payment.amount
into pid, pslug, amount_cents
;
if expense_id is not null then
-- must be inserted before updating statuses, so that it can see this
-- payments amount too.
insert into expense_payment (expense_id, payment_id)
values (expense_id, pid);
perform update_expense_payment_status(pid, expense_id, amount_cents);
end if;
return pslug;
end
$$
language plpgsql
;
revoke execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) from public;
grant execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) to invoicer;
grant execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) to admin;
commit;

View File

@ -1,67 +0,0 @@
-- Deploy numerus:add_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment
-- requires: expense_payment
-- requires: company
-- requires: currency
-- requires: parse_price
-- requires: tag_name
-- requires: update_expense_payment_status
begin;
set search_path to numerus, public;
create or replace function add_payment(company integer, expense_id integer, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
$$
declare
pslug uuid;
pid integer;
amount_cents integer;
begin
insert into payment
( company_id
, payment_account_id
, description
, payment_date
, amount
, currency_code
, payment_status
, tags
)
select company_id
, payment_account_id
, description
, payment_date
, parse_price(amount, currency.decimal_digits)
, currency_code
, 'complete'
, tags
from company
join currency using (currency_code)
where company.company_id = add_payment.company
returning payment_id, slug, payment.amount
into pid, pslug, amount_cents
;
if expense_id is not null then
-- must be inserted before updating statuses, so that it can see this
-- payments amount too.
insert into expense_payment (expense_id, payment_id)
values (expense_id, pid);
perform update_expense_payment_status(pid, expense_id, amount_cents);
end if;
return pslug;
end
$$
language plpgsql
;
revoke execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) from public;
grant execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) to invoicer;
grant execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) to admin;
commit;

View File

@ -1,35 +0,0 @@
-- Deploy numerus:add_payment_account_bank to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: payment_account_bank
begin;
set search_path to numerus, public;
create or replace function add_payment_account_bank(company integer, name text, iban iban) returns uuid as
$$
declare
account_id integer;
account_slug uuid;
begin
insert into payment_account (company_id, payment_account_type, name)
select company, 'bank', add_payment_account_bank.name
returning payment_account_id, slug into account_id, account_slug;
insert into payment_account_bank (payment_account_id, iban)
values (account_id, iban)
;
return account_slug;
end;
$$
language plpgsql
;
revoke execute on function add_payment_account_bank(integer, text, iban) from public;
grant execute on function add_payment_account_bank(integer, text, iban) to invoicer;
grant execute on function add_payment_account_bank(integer, text, iban) to admin;
commit;

View File

@ -1,34 +0,0 @@
-- Deploy numerus:add_payment_account_card to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: payment_account_card
begin;
set search_path to numerus, public;
create or replace function add_payment_account_card(company integer, name text, four_digits text, exp_date date) returns uuid as
$$
declare
account_id integer;
account_slug uuid;
begin
insert into payment_account (company_id, payment_account_type, name)
select company, 'card', add_payment_account_card.name
returning payment_account_id, slug into account_id, account_slug;
insert into payment_account_card (payment_account_id, last_four_digits, expiration_date)
values (account_id, four_digits, exp_date);
return account_slug;
end
$$
language plpgsql
;
revoke execute on function add_payment_account_card(integer, text, text, date) from public;
grant execute on function add_payment_account_card(integer, text, text, date) to invoicer;
grant execute on function add_payment_account_card(integer, text, text, date) to admin;
commit;

View File

@ -1,23 +0,0 @@
-- Deploy numerus:add_payment_account_cash to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
begin;
set search_path to numerus, public;
create or replace function add_payment_account_cash(company integer, name text) returns uuid as
$$
insert into payment_account (company_id, payment_account_type, name)
values (company, 'cash', name)
returning slug;
$$
language sql
;
revoke execute on function add_payment_account_cash(integer, text) from public;
grant execute on function add_payment_account_cash(integer, text) to invoicer;
grant execute on function add_payment_account_cash(integer, text) to admin;
commit;

View File

@ -1,23 +0,0 @@
-- Deploy numerus:add_payment_account_other to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
begin;
set search_path to numerus, public;
create or replace function add_payment_account_other(company integer, name text) returns uuid as
$$
insert into payment_account (company_id, payment_account_type, name)
values (company, 'other', name)
returning slug;
$$
language sql
;
revoke execute on function add_payment_account_other(integer, text) from public;
grant execute on function add_payment_account_other(integer, text) to invoicer;
grant execute on function add_payment_account_other(integer, text) to admin;
commit;

View File

@ -1,20 +0,0 @@
-- Deploy numerus:attach_to_collection to pg
-- requires: roles
-- requires: attach_to_payment
begin;
set search_path to numerus, public;
create or replace function attach_to_collection(collection_slug uuid, original_filename text, mime_type text, content bytea) returns void as
$$
select attach_to_payment(collection_slug, original_filename, mime_type, content);
$$
language sql
;
revoke execute on function attach_to_collection(uuid, text, text, bytea) from public;
grant execute on function attach_to_collection(uuid, text, text, bytea) to invoicer;
grant execute on function attach_to_collection(uuid, text, text, bytea) to admin;
commit;

View File

@ -1,30 +0,0 @@
-- Deploy numerus:attach_to_collection to pg
-- requires: roles
-- requires: schema_numerus
-- requires: collection
-- requires: collection_attachment
begin;
set search_path to numerus, public;
create or replace function attach_to_collection(collection_slug uuid, original_filename text, mime_type text, content bytea) returns void as
$$
insert into collection_attachment (collection_id, original_filename, mime_type, content)
select collection_id, original_filename, mime_type, content
from collection
where slug = collection_slug
on conflict (collection_id) do update
set original_filename = excluded.original_filename
, mime_type = excluded.mime_type
, content = excluded.content
;
$$
language sql
;
revoke execute on function attach_to_collection(uuid, text, text, bytea) from public;
grant execute on function attach_to_collection(uuid, text, text, bytea) to invoicer;
grant execute on function attach_to_collection(uuid, text, text, bytea) to admin;
commit;

View File

@ -1,30 +0,0 @@
-- Deploy numerus:attach_to_invoice to pg
-- requires: schema_numerus
-- requires: roles
-- requires: invoice
-- requires: invoice_attachment
begin;
set search_path to numerus, public;
create or replace function attach_to_invoice(invoice_slug uuid, original_filename text, mime_type text, content bytea) returns void as
$$
insert into invoice_attachment (invoice_id, original_filename, mime_type, content)
select invoice_id, original_filename, mime_type, content
from invoice
where slug = invoice_slug
on conflict (invoice_id) do update
set original_filename = excluded.original_filename
, mime_type = excluded.mime_type
, content = excluded.content
;
$$
language sql
;
revoke execute on function attach_to_invoice(uuid, text, text, bytea) from public;
grant execute on function attach_to_invoice(uuid, text, text, bytea) to invoicer;
grant execute on function attach_to_invoice(uuid, text, text, bytea) to admin;
commit;

View File

@ -1,30 +0,0 @@
-- Deploy numerus:attach_to_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment
-- requires: payment_attachment
begin;
set search_path to numerus, public;
create or replace function attach_to_payment(payment_slug uuid, original_filename text, mime_type text, content bytea) returns void as
$$
insert into payment_attachment (payment_id, original_filename, mime_type, content)
select payment_id, original_filename, mime_type, content
from payment
where slug = payment_slug
on conflict (payment_id) do update
set original_filename = excluded.original_filename
, mime_type = excluded.mime_type
, content = excluded.content
;
$$
language sql
;
revoke execute on function attach_to_payment(uuid, text, text, bytea) from public;
grant execute on function attach_to_payment(uuid, text, text, bytea) to invoicer;
grant execute on function attach_to_payment(uuid, text, text, bytea) to admin;
commit;

View File

@ -1,27 +0,0 @@
-- Deploy numerus:available_expense_status to pg
-- requires: schema_numerus
-- requires: expense_status
-- requires: expense_status_i18n
begin;
set search_path to numerus;
insert into expense_status (expense_status, name)
values ('pending', 'Pending')
, ('partial', 'Partial')
, ('paid', 'Paid')
on conflict (expense_status) do nothing
;
insert into expense_status_i18n (expense_status, lang_tag, name)
values ('pending', 'ca', 'Pendent')
, ('partial', 'ca', 'Parcial')
, ('paid', 'ca', 'Pagada')
, ('pending', 'es', 'Pendiente')
, ('partial', 'es', 'Parcial')
, ('paid', 'es', 'Pagada')
on conflict (expense_status, lang_tag) do nothing
;
commit;

View File

@ -1,22 +0,0 @@
-- Deploy numerus:available_expense_status to pg
-- requires: schema_numerus
-- requires: expense_status
-- requires: expense_status_i18n
begin;
set search_path to numerus;
insert into expense_status (expense_status, name)
values ('pending', 'Pending')
, ('paid', 'Paid')
;
insert into expense_status_i18n (expense_status, lang_tag, name)
values ('pending', 'ca', 'Pendent')
, ('paid', 'ca', 'Pagada')
, ('pending', 'es', 'Pendiente')
, ('paid', 'es', 'Pagada')
;
commit;

View File

@ -10,28 +10,19 @@ set search_path to numerus;
insert into invoice_status (invoice_status, name) insert into invoice_status (invoice_status, name)
values ('created', 'Created') values ('created', 'Created')
, ('sent', 'Sent') , ('sent', 'Sent')
, ('partial', 'Partial')
, ('paid', 'Paid') , ('paid', 'Paid')
, ('unpaid', 'Unpaid') , ('unpaid', 'Unpaid')
on conflict (invoice_status) do nothing
; ;
insert into invoice_status_i18n (invoice_status, lang_tag, name) insert into invoice_status_i18n (invoice_status, lang_tag, name)
values ('created', 'ca', 'Creada') values ('created', 'ca', 'Creada')
, ('sent', 'ca', 'Enviada') , ('sent', 'ca', 'Enviada')
, ('partial', 'ca', 'Parcial')
, ('paid', 'ca', 'Cobrada') , ('paid', 'ca', 'Cobrada')
, ('unpaid', 'ca', 'No cobrada') , ('unpaid', 'ca', 'No cobrada')
, ('created', 'es', 'Creada') , ('created', 'es', 'Creada')
, ('sent', 'es', 'Enviada') , ('sent', 'es', 'Enviada')
, ('partial', 'es', 'Parcial')
, ('paid', 'es', 'Cobrada') , ('paid', 'es', 'Cobrada')
, ('unpaid', 'es', 'No cobrada') , ('unpaid', 'es', 'No cobrada')
on conflict (invoice_status, lang_tag) do nothing
; ;
update invoice set invoice_status = 'created' where invoice_status in ('sent', 'unpaid');
delete from invoice_status_i18n where invoice_status in ('sent', 'unpaid');
delete from invoice_status where invoice_status in ('sent', 'unpaid');
commit; commit;

View File

@ -1,28 +0,0 @@
-- Deploy numerus:available_invoice_status to pg
-- requires: schema_numerus
-- requires: invoice_status
-- requires: invoice_status_i18n
begin;
set search_path to numerus;
insert into invoice_status (invoice_status, name)
values ('created', 'Created')
, ('sent', 'Sent')
, ('paid', 'Paid')
, ('unpaid', 'Unpaid')
;
insert into invoice_status_i18n (invoice_status, lang_tag, name)
values ('created', 'ca', 'Creada')
, ('sent', 'ca', 'Enviada')
, ('paid', 'ca', 'Cobrada')
, ('unpaid', 'ca', 'No cobrada')
, ('created', 'es', 'Creada')
, ('sent', 'es', 'Enviada')
, ('paid', 'es', 'Cobrada')
, ('unpaid', 'es', 'No cobrada')
;
commit;

View File

@ -1,28 +0,0 @@
-- Deploy numerus:available_payment_account_types to pg
-- requires: schema_numerus
-- requires: payment_account_type
-- requires: payment_account_type_i18n
begin;
set search_path to numerus;
insert into payment_account_type (payment_account_type, name)
values ('bank', 'Bank')
, ('card', 'Credit Card')
, ('cash', 'Cash')
, ('other', 'Other')
;
insert into payment_account_type_i18n (payment_account_type, lang_tag, name)
values ('bank', 'ca', 'Banc')
, ('card', 'ca', 'Targeta de crèdit')
, ('cash', 'ca', 'Efectiu')
, ('other', 'ca', 'Altres')
, ('bank', 'es', 'Banco')
, ('card', 'es', 'Tarjeta de crédito')
, ('cash', 'es', 'Efectivo')
, ('other', 'es', 'Otros')
;
commit;

View File

@ -1,22 +0,0 @@
-- Deploy numerus:available_payment_status to pg
-- requires: schema_numerus
-- requires: payment_status
-- requires: payment_status_i18n
begin;
set search_path to numerus, public;
insert into payment_status (payment_status, name)
values ('partial', 'Partial')
, ('complete', 'Complete')
;
insert into payment_status_i18n (payment_status, lang_tag, name)
values ('partial', 'ca', 'Parcial')
, ('partial', 'es', 'Parcial')
, ('complete', 'ca', 'Complet')
, ('complete', 'es', 'Completo')
;
commit;

View File

@ -1,14 +0,0 @@
-- Deploy numerus:bic to pg
-- requires: schema_numerus
begin;
set search_path to numerus, public;
create domain bic as text
check ( value ~ '^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$' )
;
comment on domain bic is 'A valid, but not necessarily existent, Business Identifier Code (ISO 9362)';
commit;

View File

@ -1,45 +0,0 @@
-- Deploy numerus:collection to pg
-- requires: roles
-- requires: schema_numerus
-- requires: company
-- requires: payment_account
-- requires: currency
-- requires: tag_name
-- requires: payment_status
-- requires: extension_pgcrypto
begin;
set search_path to numerus, public;
create table collection (
collection_id integer generated by default as identity primary key,
company_id integer not null references company,
slug uuid not null unique default gen_random_uuid(),
description text not null,
collection_date date not null default current_date,
payment_account_id integer not null references payment_account,
amount integer not null constraint collection_amount_positive check (amount > 0),
currency_code text not null references currency,
tags tag_name[] not null default '{}',
payment_status text not null default 'complete' references payment_status,
created_at timestamptz not null default current_timestamp
);
grant select, insert, update, delete on table collection to invoicer;
grant select, insert, update, delete on table collection to admin;
alter table collection enable row level security;
create policy company_policy
on collection
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = collection.company_id
)
);
commit;

View File

@ -1,32 +0,0 @@
-- Deploy numerus:collection_attachment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: collection
begin;
set search_path to numerus, public;
create table collection_attachment (
collection_id integer primary key references collection,
original_filename text not null,
mime_type text not null,
content bytea not null
);
grant select, insert, update, delete on table collection_attachment to invoicer;
grant select, insert, update, delete on table collection_attachment to admin;
alter table collection_attachment enable row level security;
create policy company_policy
on collection_attachment
using (
exists(
select 1
from collection
where collection.collection_id = collection_attachment.collection_id
)
);
commit;

View File

@ -1,68 +0,0 @@
-- Deploy numerus:compute_new_expense_amount to pg
-- requires: schema_numerus
-- requires: roles
-- requires: company
-- requires: tax
-- requires: new_expense_amount
begin;
set search_path to numerus, public;
create or replace function compute_new_expense_amount(company_id integer, subtotal text, taxes integer[]) returns new_expense_amount as
$$
declare
result new_expense_amount;
begin
if trim(subtotal) = '' then
subtotal = '0';
end if;
if array_length(taxes, 1) > 0 then
with line as (
select round(parse_price(subtotal, currency.decimal_digits)) as price
, tax_id
, decimal_digits
from unnest (taxes) as tax(tax_id)
join company on company.company_id = compute_new_expense_amount.company_id
join currency using (currency_code)
)
, tax_amount as (
select tax_id
, sum(round(price * tax.rate)::integer)::integer as amount
, decimal_digits
from line
join tax using (tax_id)
group by tax_id, decimal_digits
)
, tax_total as (
select sum(amount)::integer as amount
, array_agg(array[name, to_price(amount, decimal_digits)]) as taxes
from tax_amount
join tax using (tax_id)
)
select coalesce(tax_total.taxes, array[]::text[][])
, to_price(price::integer + coalesce(tax_total.amount, 0), decimal_digits) as total
from line, tax_total
into result.taxes, result.total;
else
select array[]::text[][]
, to_price(coalesce(parse_price(subtotal, decimal_digits), 0), decimal_digits)
from company
join currency using (currency_code)
where company.company_id = compute_new_expense_amount.company_id
into result.taxes, result.total
;
end if;
return result;
end;
$$
language plpgsql
stable
;
revoke execute on function compute_new_expense_amount(integer, text, integer[]) from public;
grant execute on function compute_new_expense_amount(integer, text, integer[]) to invoicer;
grant execute on function compute_new_expense_amount(integer, text, integer[]) to admin;
commit;

View File

@ -1,39 +0,0 @@
-- Deploy numerus:contact_email to pg
-- requires: roles
-- requires: schema_numerus
-- requires: email
-- requires: contact
begin;
set search_path to numerus, public;
create table contact_email (
contact_id integer primary key references contact,
email email not null
);
grant select, insert, update, delete on table contact_email to invoicer;
grant select, insert, update, delete on table contact_email to admin;
alter table contact_email enable row level security;
create policy company_policy
on contact_email
using (
exists(
select 1
from contact
where contact.contact_id = contact_email.contact_id
)
);
insert into contact_email
select contact_id, email
from contact;
alter table contact
drop column email
;
commit;

View File

@ -1,31 +0,0 @@
-- Deploy numerus:contact_iban to pg
-- requires: schema_numerus
-- requires: roles
-- requires: contact
-- requires: extension_iban
begin;
set search_path to numerus, public;
create table contact_iban (
contact_id integer primary key references contact,
iban iban not null
);
grant select, insert, update, delete on table contact_iban to invoicer;
grant select, insert, update, delete on table contact_iban to admin;
alter table contact_iban enable row level security;
create policy company_policy
on contact_iban
using (
exists(
select 1
from contact
where contact.contact_id = contact_iban.contact_id
)
);
commit;

View File

@ -1,38 +0,0 @@
-- Deploy numerus:contact_phone to pg
-- requires: roles
-- requires: schema_numerus
-- requires: extension_pg_libphonenumber
begin;
set search_path to numerus, public;
create table contact_phone (
contact_id integer primary key references contact,
phone packed_phone_number not null
);
grant select, insert, update, delete on table contact_phone to invoicer;
grant select, insert, update, delete on table contact_phone to admin;
alter table contact_phone enable row level security;
create policy company_policy
on contact_phone
using (
exists(
select 1
from contact
where contact.contact_id = contact_phone.contact_id
)
);
insert into contact_phone
select contact_id, phone
from contact;
alter table contact
drop column phone
;
commit;

View File

@ -1,31 +0,0 @@
-- Deploy numerus:contact_swift to pg
-- requires: schema_numerus
-- requires: roles
-- requires: contact
-- requires: bic
begin;
set search_path to numerus, public;
create table contact_swift (
contact_id integer primary key references contact,
bic bic not null
);
grant select, insert, update, delete on table contact_swift to invoicer;
grant select, insert, update, delete on table contact_swift to admin;
alter table contact_swift enable row level security;
create policy company_policy
on contact_swift
using (
exists(
select 1
from contact
where contact.contact_id = contact_swift.contact_id
)
);
commit;

View File

@ -1,60 +0,0 @@
-- Deploy numerus:contact_tax_details to pg
-- requires: roles
-- requires: schema_numerus
-- requires: contact
-- requires: extension_vat
-- requires: country_code
-- requires: country
begin;
set search_path to numerus, public;
create table contact_tax_details (
contact_id integer primary key references contact,
business_name text not null constraint business_name_not_empty check(length(trim(business_name)) > 1),
vatin vatin not null,
address text not null,
city text not null,
province text not null,
postal_code text not null,
country_code country_code not null references country
);
alter table contact_tax_details enable row level security;
create policy company_policy
on contact_tax_details
using (
exists(
select 1
from contact
where contact.contact_id = contact_tax_details.contact_id
)
);
grant select, insert, update, delete on table contact_tax_details to invoicer;
grant select, insert, update, delete on table contact_tax_details to admin;
insert into contact_tax_details
select contact_id, business_name, vatin, address, city, province, postal_code, country_code
from contact;
update contact set trade_name = business_name where trade_name = '';
alter table contact
rename column trade_name to name
;
alter table contact
drop column business_name
, drop column vatin
, drop column address
, drop column city
, drop column province
, drop column postal_code
, drop column country_code
, add constraint name_not_empty check(length(trim(name)) > 1)
;
commit;

View File

@ -1,39 +0,0 @@
-- Deploy numerus:contact_web to pg
-- requires: roles
-- requires: schema_numerus
-- requires: extension_uri
-- requires: contact
begin;
set search_path to numerus, public;
create table contact_web (
contact_id integer primary key references contact,
uri uri not null
);
grant select, insert, update, delete on table contact_web to invoicer;
grant select, insert, update, delete on table contact_web to admin;
alter table contact_web enable row level security;
create policy company_policy
on contact_web
using (
exists(
select 1
from contact
where contact.contact_id = contact_web.contact_id
)
);
insert into contact_web
select contact_id, web
from contact;
alter table contact
drop column web
;
commit;

View File

@ -1,53 +0,0 @@
-- Deploy numerus:edit_collection to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment
-- requires: invoice_payment
-- requires: currency
-- requires: parse_price
-- requires: tag_name
-- requires: update_invoice_collection_status
begin;
set search_path to numerus, public;
create or replace function edit_collection(collection_slug uuid, collection_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
$$
declare
cid integer;
iid integer;
amount_cents integer;
begin
update payment
set payment_date = edit_collection.collection_date
, payment_account_id = edit_collection.payment_account_id
, description = edit_collection.description
, amount = parse_price(edit_collection.amount, decimal_digits)
, tags = edit_collection.tags
from currency
where slug = collection_slug
and currency.currency_code = payment.currency_code
returning payment_id, payment.amount
into cid, amount_cents
;
select invoice_id into iid
from invoice_payment
where payment_id = cid;
if iid is not null then
perform update_invoice_collection_status(cid, iid, amount_cents);
end if;
return collection_slug;
end
$$
language plpgsql
;
revoke execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) from public;
grant execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) to invoicer;
grant execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) to admin;
commit;

View File

@ -1,53 +0,0 @@
-- Deploy numerus:edit_collection to pg
-- requires: roles
-- requires: schema_numerus
-- requires: collection
-- requires: invoice_collection
-- requires: currency
-- requires: parse_price
-- requires: tag_name
-- requires: update_invoice_collection_status
begin;
set search_path to numerus, public;
create or replace function edit_collection(collection_slug uuid, collection_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
$$
declare
cid integer;
iid integer;
amount_cents integer;
begin
update collection
set collection_date = edit_collection.collection_date
, payment_account_id = edit_collection.payment_account_id
, description = edit_collection.description
, amount = parse_price(edit_collection.amount, decimal_digits)
, tags = edit_collection.tags
from currency
where slug = collection_slug
and currency.currency_code = collection.currency_code
returning collection_id, collection.amount
into cid, amount_cents
;
select invoice_id into iid
from invoice_collection
where collection_id = cid;
if iid is not null then
perform update_invoice_collection_status(cid, iid, amount_cents);
end if;
return collection_slug;
end
$$
language plpgsql
;
revoke execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) from public;
grant execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) to invoicer;
grant execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) to admin;
commit;

View File

@ -7,25 +7,29 @@
-- requires: contact -- requires: contact
-- requires: extension_vat -- requires: extension_vat
-- requires: extension_pg_libphonenumber -- requires: extension_pg_libphonenumber
-- requires: tax_details
-- requires: contact_web
-- requires: contact_phone
-- requires: contact_tax_details
-- requires: contact_iban
-- requires: contact_bic
begin; begin;
set search_path to numerus, public; set search_path to numerus, public;
create or replace function edit_contact(contact_slug uuid, name text, phone text, email text, web text, tax_details tax_details, iban text, bic text, tags tag_name[]) returns uuid as create or replace function edit_contact(contact_slug uuid, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as
$$ $$
declare declare
cid integer; cid integer;
company integer; company integer;
begin begin
update contact update contact
set name = edit_contact.name set business_name = edit_contact.business_name
, vatin = (edit_contact.country_code || edit_contact.vatin)::vatin
, trade_name = edit_contact.trade_name
, phone = parse_packed_phone_number( edit_contact.phone, edit_contact.country_code)
, email = edit_contact.email
, web = edit_contact.web
, address = edit_contact.address
, city = edit_contact.city
, province = edit_contact.province
, postal_code = edit_contact.postal_code
, country_code = edit_contact.country_code
, tags = edit_contact.tags , tags = edit_contact.tags
where slug = contact_slug where slug = contact_slug
returning contact_id, company_id returning contact_id, company_id
@ -36,96 +40,14 @@ begin
return null; return null;
end if; end if;
if tax_details is null then
delete
from contact_tax_details
where contact_id = cid
;
else
insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code)
values (cid, tax_details.business_name, (tax_details.country_code || tax_details.vatin)::vatin, tax_details.address, tax_details.city, tax_details.province, tax_details.postal_code, tax_details.country_code)
on conflict (contact_id) do update
set business_name = excluded.business_name
, vatin = excluded.vatin
, address = excluded.address
, city = excluded.city
, province = excluded.province
, postal_code = excluded.postal_code
, country_code = excluded.country_code
;
end if;
if phone is null or trim(phone) = '' then
delete from contact_phone
where contact_id = cid
;
else
insert into contact_phone (contact_id, phone)
values (cid, parse_packed_phone_number(edit_contact.phone, coalesce(tax_details.country_code, 'ES')))
on conflict (contact_id) do update
set phone = excluded.phone
;
end if;
if email is null or trim(email) = '' then
delete from contact_email
where contact_id = cid
;
else
insert into contact_email (contact_id, email)
values (cid, edit_contact.email)
on conflict (contact_id) do update
set email = excluded.email
;
end if;
if web is null or trim(web) = '' then
delete from contact_web
where contact_id = cid
;
else
insert into contact_web (contact_id, uri)
values (cid, edit_contact.web)
on conflict (contact_id) do update
set uri = excluded.uri
;
end if;
if iban is null or trim(iban) = '' then
delete from contact_iban
where contact_id = cid
;
else
insert into contact_iban (contact_id, iban)
values (cid, edit_contact.iban)
on conflict (contact_id) do update
set iban = excluded.iban
;
end if;
if bic is null or trim(bic) = '' then
delete from contact_swift
where contact_id = cid
;
else
insert into contact_swift (contact_id, bic)
values (cid, edit_contact.bic)
on conflict (contact_id) do update
set bic = excluded.bic
;
end if;
return contact_slug; return contact_slug;
end end
$$ $$
language plpgsql language plpgsql
; ;
revoke execute on function edit_contact(uuid, text, text, text, text, tax_details, text, text, tag_name[]) from public; revoke execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public;
grant execute on function edit_contact(uuid, text, text, text, text, tax_details, text, text, tag_name[]) to invoicer; grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer;
grant execute on function edit_contact(uuid, text, text, text, text, tax_details, text, text, tag_name[]) to admin; grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin;
drop function if exists edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]);
commit; commit;

View File

@ -1,53 +0,0 @@
-- Deploy numerus:edit_contact to pg
-- requires: schema_numerus
-- requires: email
-- requires: extension_uri
-- requires: country_code
-- requires: tag_name
-- requires: contact
-- requires: extension_vat
-- requires: extension_pg_libphonenumber
begin;
set search_path to numerus, public;
create or replace function edit_contact(contact_slug uuid, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as
$$
declare
cid integer;
company integer;
begin
update contact
set business_name = edit_contact.business_name
, vatin = (edit_contact.country_code || edit_contact.vatin)::vatin
, trade_name = edit_contact.trade_name
, phone = parse_packed_phone_number( edit_contact.phone, edit_contact.country_code)
, email = edit_contact.email
, web = edit_contact.web
, address = edit_contact.address
, city = edit_contact.city
, province = edit_contact.province
, postal_code = edit_contact.postal_code
, country_code = edit_contact.country_code
, tags = edit_contact.tags
where slug = contact_slug
returning contact_id, company_id
into cid, company
;
if cid is null then
return null;
end if;
return contact_slug;
end
$$
language plpgsql
;
revoke execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public;
grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer;
grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin;
commit;

View File

@ -5,15 +5,11 @@
-- requires: parse_price -- requires: parse_price
-- requires: tax -- requires: tax
-- requires: tag_name -- requires: tag_name
-- requires: expense_status
-- requires: expense_expense_status
begin; begin;
set search_path to numerus, public; set search_path to numerus, public;
drop function if exists edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]);
create or replace function edit_expense(expense_slug uuid, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as create or replace function edit_expense(expense_slug uuid, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$ $$
declare declare

View File

@ -1,50 +0,0 @@
-- Deploy numerus:edit_expense to pg
-- requires: schema_numerus
-- requires: expense
-- requires: currency
-- requires: parse_price
-- requires: tax
-- requires: tag_name
begin;
set search_path to numerus, public;
create or replace function edit_expense(expense_slug uuid, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$
declare
eid integer;
begin
update expense
set invoice_date = edit_expense.invoice_date
, contact_id = edit_expense.contact_id
, invoice_number = edit_expense.invoice_number
, amount = parse_price(edit_expense.amount, decimal_digits)
, tags = edit_expense.tags
from currency
where slug = expense_slug
and currency.currency_code = expense.currency_code
returning expense_id
into eid;
if eid is null then
return null;
end if;
delete from expense_tax where expense_id = eid;
insert into expense_tax (expense_id, tax_id, tax_rate)
select eid, tax_id, tax.rate
from tax
join unnest(taxes) as etax(tax_id) using (tax_id);
return expense_slug;
end;
$$
language plpgsql;
revoke execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) from public;
grant execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) to invoicer;
grant execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) to admin;
commit;

View File

@ -1,53 +0,0 @@
-- Deploy numerus:edit_expense to pg
-- requires: schema_numerus
-- requires: expense
-- requires: currency
-- requires: parse_price
-- requires: tax
-- requires: tag_name
-- requires: expense_status
-- requires: expense_expense_status
begin;
set search_path to numerus, public;
create or replace function edit_expense(expense_slug uuid, status text, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
$$
declare
eid integer;
begin
update expense
set invoice_date = edit_expense.invoice_date
, contact_id = edit_expense.contact_id
, invoice_number = edit_expense.invoice_number
, amount = parse_price(edit_expense.amount, decimal_digits)
, expense_status = status
, tags = edit_expense.tags
from currency
where slug = expense_slug
and currency.currency_code = expense.currency_code
returning expense_id
into eid;
if eid is null then
return null;
end if;
delete from expense_tax where expense_id = eid;
insert into expense_tax (expense_id, tax_id, tax_rate)
select eid, tax_id, tax.rate
from tax
join unnest(taxes) as etax(tax_id) using (tax_id);
return expense_slug;
end;
$$
language plpgsql;
revoke execute on function edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]) from public;
grant execute on function edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]) to invoicer;
grant execute on function edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]) to admin;
commit;

View File

@ -14,9 +14,7 @@ begin;
set search_path to numerus, public; set search_path to numerus, public;
drop function if exists edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]); create or replace function edit_invoice(invoice_slug uuid, invoice_status text, contact_id integer, notes text, payment_method_id integer, tags tag_name[], products edited_invoice_product[]) returns uuid as
create or replace function edit_invoice(invoice_slug uuid, contact_id integer, notes text, payment_method_id integer, tags tag_name[], products edited_invoice_product[]) returns uuid as
$$ $$
declare declare
iid integer; iid integer;
@ -29,6 +27,7 @@ declare
begin begin
update invoice update invoice
set contact_id = edit_invoice.contact_id set contact_id = edit_invoice.contact_id
, invoice_status = edit_invoice.invoice_status
, notes = edit_invoice.notes , notes = edit_invoice.notes
, payment_method_id = edit_invoice.payment_method_id , payment_method_id = edit_invoice.payment_method_id
, tags = edit_invoice.tags , tags = edit_invoice.tags
@ -104,9 +103,9 @@ end;
$$ $$
language plpgsql; language plpgsql;
revoke execute on function edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]) from public; revoke execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) from public;
grant execute on function edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]) to invoicer; grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to invoicer;
grant execute on function edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]) to admin; grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to admin;
commit; commit;

View File

@ -1,111 +0,0 @@
-- Deploy numerus:edit_invoice to pg
-- requires: schema_numerus
-- requires: invoice
-- requires: currency
-- requires: parse_price
-- requires: edited_invoice_product
-- requires: tax
-- requires: invoice_product
-- requires: invoice_product_product
-- requires: invoice_product_tax
-- requires: tag_name
begin;
set search_path to numerus, public;
create or replace function edit_invoice(invoice_slug uuid, invoice_status text, contact_id integer, notes text, payment_method_id integer, tags tag_name[], products edited_invoice_product[]) returns uuid as
$$
declare
iid integer;
products_to_keep integer[];
products_to_delete integer[];
company integer;
ccode text;
product edited_invoice_product;
ipid integer;
begin
update invoice
set contact_id = edit_invoice.contact_id
, invoice_status = edit_invoice.invoice_status
, notes = edit_invoice.notes
, payment_method_id = edit_invoice.payment_method_id
, tags = edit_invoice.tags
where slug = invoice_slug
returning invoice_id, company_id, currency_code
into iid, company, ccode
;
if iid is null then
return null;
end if;
foreach product in array products
loop
if product.invoice_product_id is null then
insert into invoice_product (invoice_id, name, description, price, quantity, discount_rate)
select iid
, product.name
, coalesce(product.description, '')
, parse_price(product.price, currency.decimal_digits)
, product.quantity
, product.discount_rate
from currency
where currency_code = ccode
returning invoice_product_id
into ipid;
else
ipid := product.invoice_product_id;
update invoice_product
set name = product.name
, description = coalesce(product.description, '')
, price = parse_price(product.price, currency.decimal_digits)
, quantity = product.quantity
, discount_rate = product.discount_rate
from currency
where invoice_product_id = ipid
and currency_code = ccode;
end if;
products_to_keep := array_append(products_to_keep, ipid);
if product.product_id is null then
delete from invoice_product_product where invoice_product_id = ipid;
else
insert into invoice_product_product (invoice_product_id, product_id)
values (ipid, product.product_id)
on conflict (invoice_product_id) do update
set product_id = product.product_id;
end if;
delete from invoice_product_tax where invoice_product_id = ipid;
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
select ipid, tax_id, tax.rate
from tax
join unnest(product.tax) as ptax(tax_id) using (tax_id);
end loop;
select array_agg(invoice_product_id)
into products_to_delete
from invoice_product
where invoice_id = iid
and not (invoice_product_id = any(products_to_keep));
if array_length(products_to_delete, 1) > 0 then
delete from invoice_product_tax where invoice_product_id = any(products_to_delete);
delete from invoice_product_product where invoice_product_id = any(products_to_delete);
delete from invoice_product where invoice_product_id = any(products_to_delete);
end if;
return invoice_slug;
end;
$$
language plpgsql;
revoke execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) from public;
grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to invoicer;
grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to admin;
commit;

View File

@ -1,53 +0,0 @@
-- Deploy numerus:edit_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment
-- requires: expense_payment
-- requires: currency
-- requires: parse_price
-- requires: tag_name
-- requires: update_expense_payment_status
begin;
set search_path to numerus, public;
create or replace function edit_payment(payment_slug uuid, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
$$
declare
pid integer;
eid integer;
amount_cents integer;
begin
update payment
set payment_date = edit_payment.payment_date
, payment_account_id = edit_payment.payment_account_id
, description = edit_payment.description
, amount = -parse_price(edit_payment.amount, decimal_digits)
, tags = edit_payment.tags
from currency
where slug = payment_slug
and currency.currency_code = payment.currency_code
returning payment_id, payment.amount
into pid, amount_cents
;
select expense_id into eid
from expense_payment
where payment_id = pid;
if eid is not null then
perform update_expense_payment_status(pid, eid, amount_cents);
end if;
return payment_slug;
end
$$
language plpgsql
;
revoke execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) from public;
grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to invoicer;
grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to admin;
commit;

View File

@ -1,53 +0,0 @@
-- Deploy numerus:edit_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment
-- requires: expense_payment
-- requires: currency
-- requires: parse_price
-- requires: tag_name
-- requires: update_expense_payment_status
begin;
set search_path to numerus, public;
create or replace function edit_payment(payment_slug uuid, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
$$
declare
pid integer;
eid integer;
amount_cents integer;
begin
update payment
set payment_date = edit_payment.payment_date
, payment_account_id = edit_payment.payment_account_id
, description = edit_payment.description
, amount = parse_price(edit_payment.amount, decimal_digits)
, tags = edit_payment.tags
from currency
where slug = payment_slug
and currency.currency_code = payment.currency_code
returning payment_id, payment.amount
into pid, amount_cents
;
select expense_id into eid
from expense_payment
where payment_id = pid;
if eid is not null then
perform update_expense_payment_status(pid, eid, amount_cents);
end if;
return payment_slug;
end
$$
language plpgsql
;
revoke execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) from public;
grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to invoicer;
grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to admin;
commit;

View File

@ -1,43 +0,0 @@
-- Deploy numerus:edit_payment_account_bank to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: payment_account_bank
-- requires: extension_iban
begin;
set search_path to numerus, public;
create or replace function edit_payment_account_bank(account_slug uuid, new_name text, new_iban iban) returns uuid as
$$
declare
account_id int;
begin
update payment_account
set name = new_name
where slug = account_slug
and payment_account_type = 'bank'
returning payment_account_id into account_id
;
if account_id is null then
return null;
end if;
update payment_account_bank
set iban = new_iban
where payment_account_id = account_id
;
return account_slug;
end
$$
language plpgsql
;
revoke execute on function edit_payment_account_bank(uuid, text, iban) from public;
grant execute on function edit_payment_account_bank(uuid, text, iban) to invoicer;
grant execute on function edit_payment_account_bank(uuid, text, iban) to admin;
commit;

View File

@ -1,43 +0,0 @@
-- Deploy numerus:edit_payment_account_card to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: payment_account_card
begin;
set search_path to numerus, public;
create or replace function edit_payment_account_card(account_slug uuid, new_name text, new_last_digits text, new_exp_date date) returns uuid as
$$
declare
account_id integer;
begin
update payment_account
set name = new_name
where slug = account_slug
and payment_account_type = 'card'
returning payment_account_id into account_id
;
if account_id is null then
return null;
end if;
update payment_account_card
set last_four_digits = new_last_digits
, expiration_date = new_exp_date
where payment_account_id = account_id
;
return account_slug;
end
$$
language plpgsql
;
revoke execute on function edit_payment_account_card(uuid, text, text, date) from public;
grant execute on function edit_payment_account_card(uuid, text, text, date) to invoicer;
grant execute on function edit_payment_account_card(uuid, text, text, date) to admin;
commit;

View File

@ -1,26 +0,0 @@
-- Deploy numerus:edit_payment_account_cash to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
begin;
set search_path to numerus, public;
create or replace function edit_payment_account_cash(account_slug uuid, new_name text) returns uuid as
$$
update payment_account
set name = new_name
where slug = account_slug
and payment_account_type = 'cash'
returning slug
;
$$
language sql
;
revoke execute on function edit_payment_account_cash(uuid, text) from public;
grant execute on function edit_payment_account_cash(uuid, text) to invoicer;
grant execute on function edit_payment_account_cash(uuid, text) to admin;
commit;

View File

@ -1,26 +0,0 @@
-- Deploy numerus:edit_payment_account_other to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
begin;
set search_path to numerus, public;
create or replace function edit_payment_account_other(account_slug uuid, new_name text) returns uuid as
$$
update payment_account
set name = new_name
where slug = account_slug
and payment_account_type = 'other'
returning slug
;
$$
language sql
;
revoke execute on function edit_payment_account_other(uuid, text) from public;
grant execute on function edit_payment_account_other(uuid, text) to invoicer;
grant execute on function edit_payment_account_other(uuid, text) to admin;
commit;

View File

@ -1,12 +0,0 @@
-- Deploy numerus:expense_expense_status to pg
-- requires: expense
begin;
set search_path to numerus, public;
alter table expense
add column expense_status text not null default 'pending' references expense_status
;
commit;

View File

@ -1,32 +0,0 @@
-- Deploy numerus:expense_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: expense
-- requires: payment
begin;
set search_path to numerus, public;
create table expense_payment (
expense_id integer not null references expense,
payment_id integer not null references payment,
primary key (expense_id, payment_id)
);
grant select, insert, update, delete on table expense_payment to invoicer;
grant select, insert, update, delete on table expense_payment to admin;
alter table expense_payment enable row level security;
create policy company_policy
on expense_payment
using (
exists(
select 1
from expense
where expense.expense_id = expense_payment.expense_id
)
);
commit;

View File

@ -1,17 +0,0 @@
-- Deploy numerus:expense_status to pg
-- requires: schema_numerus
-- requires: roles
begin;
set search_path to numerus, public;
create table expense_status (
expense_status text primary key,
name text not null
);
grant select on table expense_status to invoicer;
grant select on table expense_status to admin;
commit;

View File

@ -1,21 +0,0 @@
-- Deploy numerus:expense_status_i18n to pg
-- requires: schema_numerus
-- requires: roles
-- requires: expense_status
-- requires: language
begin;
set search_path to numerus, public;
create table expense_status_i18n (
expense_status text not null references expense_status,
lang_tag text not null references language,
name text not null,
primary key (expense_status, lang_tag)
);
grant select on table expense_status_i18n to invoicer;
grant select on table expense_status_i18n to admin;
commit;

View File

@ -1,8 +0,0 @@
-- Deploy numerus:extension_iban to pg
-- requires: schema_numerus
begin;
create extension if not exists iban;
commit;

View File

@ -1,165 +0,0 @@
-- Deploy numerus:import_contact to pg
-- requires: schema_numerus
-- requires: roles
-- requires: contact
-- requires: contact_web
-- requires: contact_phone
-- requires: contact_email
-- requires: contact_iban
-- requires: contact_swift
-- requires: contact_tax_details
-- requires: input_is_valid
-- requires: input_is_valid_phone
begin;
set search_path to numerus, public;
create or replace function begin_import_contacts() returns name as
$$
create temporary table imported_contact (
contact_id integer
, name text not null default ''
, vatin text not null default ''
, email text not null default ''
, phone text not null default ''
, web text not null default ''
, address text not null default ''
, city text not null default ''
, province text not null default ''
, postal_code text not null default ''
, country_code text not null default ''
, iban text not null default ''
, bic text not null default ''
, tags text not null default ''
);
select 'imported_contact'::name;
$$
language sql;
revoke execute on function begin_import_contacts() from public;
grant execute on function begin_import_contacts() to invoicer;
grant execute on function begin_import_contacts() to admin;
create or replace function end_import_contacts(company_id integer) returns integer as
$$
declare
imported integer;
begin
update imported_contact
set country_code = upper(trim(country_code))
, name = trim(name)
, vatin = trim(vatin)
, email = trim(email)
, phone = trim(phone)
, web = trim(web)
, address = trim(address)
, city = trim(city)
, province = trim(province)
, postal_code = trim(postal_code)
, iban = trim(iban)
, bic = trim(bic)
, tags = lower(trim(regexp_replace(regexp_replace(tags, '[^\sA-Za-z0-9-]', '', 'g'), '\s\s+', ' ', 'g')))
;
update imported_contact
set contact_id = tax_details.contact_id
from contact_tax_details as tax_details
join contact using (contact_id)
where contact.company_id = end_import_contacts.company_id
and tax_details.vatin::text = imported_contact.country_code || imported_contact.vatin
;
update imported_contact
set contact_id = nextval('contact_contact_id_seq'::regclass)
where length(trim(name)) > 1
and contact_id is null
;
insert into contact (contact_id, company_id, name, tags)
select contact_id, end_import_contacts.company_id, name, string_to_array(tags, ' ')::tag_name[]
from imported_contact
where contact_id is not null
on conflict (contact_id) do update
set tags = array_cat(contact.tags, excluded.tags)
;
insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code)
select contact_id, imported_contact.name, (country_code || vatin)::vatin, address, city, province, postal_code, country_code
from imported_contact
join country using (country_code)
where contact_id is not null
and length(address) > 1
and length(city) > 1
and length(province) > 1
and postal_code ~ postal_code_regex
and input_is_valid(country_code || vatin, 'vatin')
on conflict (contact_id) do update
set business_name = excluded.business_name
, vatin = excluded.vatin
, address = excluded.address
, city = excluded.city
, province = excluded.province
, postal_code = excluded.postal_code
, country_code = excluded.country_code
;
insert into contact_email (contact_id, email)
select contact_id, email::email
from imported_contact
where contact_id is not null
and input_is_valid(email, 'email')
on conflict (contact_id) do update
set email = excluded.email
;
insert into contact_web (contact_id, uri)
select contact_id, web::uri
from imported_contact
where contact_id is not null
and input_is_valid(web, 'uri')
and length(web) > 1
on conflict (contact_id) do update
set uri = excluded.uri
;
insert into contact_iban (contact_id, iban)
select contact_id, iban::iban
from imported_contact
where contact_id is not null
and input_is_valid(iban, 'iban')
on conflict (contact_id) do update
set iban = excluded.iban
;
insert into contact_swift (contact_id, bic)
select contact_id, bic::bic
from imported_contact
where contact_id is not null
and input_is_valid(bic, 'bic')
on conflict (contact_id) do update
set bic = excluded.bic
;
insert into contact_phone (contact_id, phone)
select contact_id, parse_packed_phone_number(phone, case when country_code = '' then 'ES' else country_code end)
from imported_contact
where contact_id is not null
and input_is_valid_phone(phone, case when country_code = '' then 'ES' else country_code end)
on conflict (contact_id) do update
set phone = excluded.phone
;
select count(*) from imported_contact where contact_id is not null into imported;
return imported;
drop table imported_contact;
end
$$
language plpgsql;
revoke execute on function end_import_contacts(integer) from public;
grant execute on function end_import_contacts(integer) to invoicer;
grant execute on function end_import_contacts(integer) to admin;
commit;

View File

@ -1,23 +0,0 @@
-- Deploy numerus:input_is_valid to pg
-- requires: schema_numerus
-- requires: roles
begin;
set search_path to public;
create or replace function input_is_valid(input text, domname text) returns boolean as
$$
begin
begin
execute format('select %L::%s', input, domname);
return true;
exception when others then
return false;
end;
end;
$$
language plpgsql
stable;
commit;

View File

@ -1,24 +0,0 @@
-- Deploy numerus:input_is_valid_phone to pg
-- requires: schema_numerus
-- requires: roles
-- requires: extension_pg_libphonenumber
begin;
set search_path to public;
create or replace function input_is_valid_phone(phone text, country text) returns boolean as
$$
begin
begin
perform parse_packed_phone_number(phone, country);
return true;
exception when others then
return false;
end;
end;
$$
language plpgsql
stable;
commit;

View File

@ -1,32 +0,0 @@
-- Deploy numerus:invoice_attachment to pg
-- requires: schema_numerus
-- requires: roles
-- requires: invoice
begin;
set search_path to numerus, public;
create table invoice_attachment (
invoice_id integer primary key references invoice,
original_filename text not null,
mime_type text not null,
content bytea not null
);
grant select, insert, update, delete on table invoice_attachment to invoicer;
grant select, insert, update, delete on table invoice_attachment to admin;
alter table invoice_attachment enable row level security;
create policy company_policy
on invoice_attachment
using (
exists(
select 1
from invoice
where invoice.invoice_id = invoice_attachment.invoice_id
)
);
commit;

View File

@ -1,32 +0,0 @@
-- Deploy numerus:invoice_collection to pg
-- requires: roles
-- requires: schema_numerus
-- requires: invoice
-- requires: collection
begin;
set search_path to numerus, public;
create table invoice_collection (
invoice_id integer not null references invoice,
collection_id integer not null references collection,
primary key (invoice_id, collection_id)
);
grant select, insert, update, delete on table invoice_collection to invoicer;
grant select, insert, update, delete on table invoice_collection to admin;
alter table invoice_collection enable row level security;
create policy company_policy
on invoice_collection
using (
exists(
select 1
from invoice
where invoice.invoice_id = invoice_collection.invoice_id
)
);
commit;

View File

@ -1,13 +0,0 @@
-- Deploy numerus:invoice_contact_id_fkey to pg
-- requires: schema_numerus
-- requires: invoice
-- requires: contact_tax_details
begin;
alter table numerus.invoice
drop constraint invoice_contact_id_fkey
, add foreign key (contact_id) references numerus.contact_tax_details (contact_id)
;
commit;

View File

@ -1,32 +0,0 @@
-- Deploy numerus:invoice_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: invoice
-- requires: payment
begin;
set search_path to numerus, public;
create table invoice_payment (
invoice_id integer not null references invoice,
payment_id integer not null references payment,
primary key (invoice_id, payment_id)
);
grant select, insert, update, delete on table invoice_payment to invoicer;
grant select, insert, update, delete on table invoice_payment to admin;
alter table invoice_payment enable row level security;
create policy company_policy
on invoice_payment
using (
exists(
select 1
from invoice
where invoice.invoice_id = invoice_payment.invoice_id
)
);
commit;

View File

@ -1,13 +0,0 @@
-- Deploy numerus:new_expense_amount to pg
-- requires: schema_numerus
begin;
set search_path to numerus, public;
create type new_expense_amount as (
taxes text[][],
total text
);
commit;

View File

@ -24,9 +24,6 @@ begin
end if; end if;
result := parts[1]::integer; result := parts[1]::integer;
if result is null then
raise invalid_parameter_value using message = price || ' is not a valid price representation.';
end if;
for d in 1..decimal_digits loop for d in 1..decimal_digits loop
result := result * 10; result := result * 10;
end loop; end loop;

View File

@ -1,53 +0,0 @@
-- Deploy numerus:parse_price to pg
-- requires: schema_public
begin;
set search_path to numerus, public;
create or replace function parse_price(price text, decimal_digits integer) returns integer as
$$
declare
parts text[];
result int;
frac_part text;
sign int := 1;
begin
if price like '-%' Then
sign := -1;
price := substring(price from 2);
end if;
parts := string_to_array(price, '.');
if array_length(parts, 1) > 2 then
raise invalid_parameter_value using message = price || ' is not a valid price representation.';
end if;
result := parts[1]::integer;
for d in 1..decimal_digits loop
result := result * 10;
end loop;
if array_length(parts, 1) = 2 then
frac_part := rtrim(parts[2], '0');
if length(frac_part) > decimal_digits then
raise invalid_parameter_value using message = price || ' has too many digits in the fraction part.';
end if;
frac_part := rpad(frac_part, decimal_digits, '0');
result := result + frac_part::integer;
end if;
return sign * result;
end;
$$
language plpgsql
immutable;
comment on function parse_price(text, integer) is
'Converts the string representation of a price in decimal form to cents, according to the number of decimal digits passed.';
revoke execute on function parse_price(text, integer) from public;
grant execute on function parse_price(text, integer) to invoicer;
grant execute on function parse_price(text, integer) to admin;
commit;

View File

@ -1,47 +0,0 @@
-- Deploy numerus:payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: company
-- requires: payment_account
-- requires: currency
-- requires: tag_name
-- requires: payment_status
-- requires: extension_pgcrypto
begin;
set search_path to numerus, public;
create table payment (
payment_id integer generated by default as identity primary key,
company_id integer not null references company,
slug uuid not null unique default gen_random_uuid(),
description text not null,
payment_date date not null default current_date,
payment_account_id integer not null references payment_account,
amount integer not null constraint payment_amount_positive check (amount > 0),
currency_code text not null references currency,
tags tag_name[] not null default '{}',
payment_status text not null default 'complete' references payment_status,
created_at timestamptz not null default current_timestamp
);
create index on payment using gin (tags);
grant select, insert, update, delete on table payment to invoicer;
grant select, insert, update, delete on table payment to admin;
alter table payment enable row level security;
create policy company_policy
on payment
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = payment.company_id
)
);
commit;

View File

@ -1,38 +0,0 @@
-- Deploy numerus:payment_account to pg
-- requires: roles
-- requires: schema_numerus
-- requires: company
-- requires: payment_account_type
-- requires: extension_pgcrypto
begin;
set search_path to numerus, public;
create table payment_account (
payment_account_id integer generated by default as identity primary key,
company_id integer not null references company,
slug uuid not null unique default gen_random_uuid(),
payment_account_type text not null references payment_account_type,
name text not null constraint payment_account_name_not_empty check(length(trim(name)) > 0),
unique (payment_account_id, payment_account_type)
);
grant select, insert, update, delete on table payment_account to invoicer;
grant select, insert, update, delete on table payment_account to admin;
alter table payment_account enable row level security;
create policy company_policy
on payment_account
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = payment_account.company_id
)
);
commit;

View File

@ -1,33 +0,0 @@
-- Deploy numerus:payment_account_bank to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
-- requires: extension_iban
begin;
set search_path to numerus, public;
create table payment_account_bank (
payment_account_id integer primary key,
payment_account_type text not null default 'bank' constraint payment_account_type_is_bank check (payment_account_type = 'bank'),
iban iban not null,
foreign key (payment_account_id, payment_account_type) references payment_account (payment_account_id, payment_account_type)
);
grant select, insert, update, delete on table payment_account_bank to invoicer;
grant select, insert, update, delete on table payment_account_bank to admin;
alter table payment_account_bank enable row level security;
create policy company_policy
on payment_account_bank
using (
exists(
select 1
from payment_account
where payment_account.payment_account_id = payment_account_bank.payment_account_id
)
);
commit;

View File

@ -1,33 +0,0 @@
-- Deploy numerus:payment_account_card to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account
begin;
set search_path to numerus, public;
create table payment_account_card (
payment_account_id integer primary key,
payment_account_type text not null default 'card' constraint payment_account_type_is_card check (payment_account_type = 'card'),
last_four_digits text not null constraint last_four_digits_are_digits check ( last_four_digits ~ '^\d{4}$'),
expiration_date date not null,
foreign key (payment_account_id, payment_account_type) references payment_account (payment_account_id, payment_account_type)
);
grant select, insert, update, delete on table payment_account_card to invoicer;
grant select, insert, update, delete on table payment_account_card to admin;
alter table payment_account_card enable row level security;
create policy company_policy
on payment_account_card
using (
exists(
select 1
from payment_account
where payment_account.payment_account_id = payment_account_card.payment_account_id
)
);
commit;

View File

@ -1,17 +0,0 @@
-- Deploy numerus:payment_account_type to pg
-- requires: roles
-- requires: schema_numerus
begin;
set search_path to numerus, public;
create table payment_account_type (
payment_account_type text primary key,
name text not null
);
grant select on table payment_account_type to invoicer;
grant select on table payment_account_type to admin;
commit;

View File

@ -1,21 +0,0 @@
-- Deploy numerus:payment_account_type_i18n to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_account_type
-- requires: language
begin;
set search_path to numerus, public;
create table payment_account_type_i18n (
payment_account_type text not null references payment_account_type,
lang_tag text not null references language,
name text not null,
primary key (payment_account_type, lang_tag)
);
grant select on table payment_account_type_i18n to invoicer;
grant select on table payment_account_type_i18n to admin;
commit;

View File

@ -1,33 +0,0 @@
-- Deploy numerus:payment_attachment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment
begin;
set search_path to numerus, public;
create table payment_attachment (
payment_id integer primary key references payment,
original_filename text not null,
mime_type text not null,
content bytea not null
);
grant select, insert, update, delete on table payment_attachment to invoicer;
grant select, insert, update, delete on table payment_attachment to admin;
alter table payment_attachment enable row level security;
create policy company_policy
on payment_attachment
using (
exists(
select 1
from payment
where payment.payment_id = payment_attachment.payment_id
)
);
commit;

View File

@ -1,17 +0,0 @@
-- Deploy numerus:payment_status to pg
-- requires: roles
-- requires: schema_numerus
begin;
set search_path to numerus, public;
create table payment_status (
payment_status text primary key,
name text not null
);
grant select on table payment_status to invoicer;
grant select on table payment_status to admin;
commit;

View File

@ -1,21 +0,0 @@
-- Deploy numerus:payment_status_i18n to pg
-- requires: roles
-- requires: schema_numerus
-- requires: payment_status
-- requires: language
begin;
set search_path to numerus, public;
create table payment_status_i18n (
payment_status text not null references payment_status,
lang_tag text not null references language,
name text not null,
primary key (payment_status, lang_tag)
);
grant select on table payment_status_i18n to invoicer;
grant select on table payment_status_i18n to admin;
commit;

View File

@ -1,40 +0,0 @@
-- Deploy numerus:remove_collection to pg
-- requires: roles
-- requires: schema_numerus
-- requires: invoice_payment
-- requires: payment
-- requires: payment_attachment
-- requires: update_invoice_collection_status
begin;
set search_path to numerus, public;
create or replace function remove_collection(collection_slug uuid) returns void as
$$
declare
cid integer;
iid integer;
begin
select payment_id into cid from payment where slug = collection_slug;
if not found then
return;
end if;
delete from invoice_payment where payment_id = cid returning invoice_id into iid;
if iid is not null then
perform update_invoice_collection_status(null, iid, 0);
end if;
delete from payment_attachment where payment_id = cid;
delete from payment where payment_id = cid;
end
$$
language plpgsql
;
revoke execute on function remove_collection(uuid) from public;
grant execute on function remove_collection(uuid) to invoicer;
grant execute on function remove_collection(uuid) to admin;
commit;

View File

@ -1,40 +0,0 @@
-- Deploy numerus:remove_collection to pg
-- requires: roles
-- requires: schema_numerus
-- requires: invoice_collection
-- requires: collection
-- requires: collection_attachment
-- requires: update_invoice_collection_status
begin;
set search_path to numerus, public;
create or replace function remove_collection(collection_slug uuid) returns void as
$$
declare
cid integer;
iid integer;
begin
select collection_id into cid from collection where slug = collection_slug;
if not found then
return;
end if;
delete from invoice_collection where collection_id = cid returning invoice_id into iid;
if iid is not null then
perform update_invoice_collection_status(null, iid, 0);
end if;
delete from collection_attachment where collection_id = cid;
delete from collection where collection_id = cid;
end
$$
language plpgsql
;
revoke execute on function remove_collection(uuid) from public;
grant execute on function remove_collection(uuid) to invoicer;
grant execute on function remove_collection(uuid) to admin;
commit;

View File

@ -1,34 +0,0 @@
-- Deploy numerus:remove_expense to pg
-- requires: roles
-- requires: schema_numerus
-- requires: expense_tax
-- requires: expense_attachment
-- requires: expense
begin;
set search_path to numerus, public;
create or replace function remove_expense(expense_slug uuid) returns void as
$$
declare
eid integer;
begin
select expense_id into eid from expense where slug = expense_slug;
if not found then
return;
end if;
delete from expense_tax where expense_id = eid;
delete from expense_attachment where expense_id = eid;
delete from expense where expense_id = eid;
end
$$
language plpgsql
;
revoke execute on function remove_expense(uuid) from public;
grant execute on function remove_expense(uuid) to invoicer;
grant execute on function remove_expense(uuid) to admin;
commit;

View File

@ -1,40 +0,0 @@
-- Deploy numerus:remove_payment to pg
-- requires: roles
-- requires: schema_numerus
-- requires: expense_payment
-- requires: payment
-- requires: payment_attachment
-- requires: update_expense_payment_status
begin;
set search_path to numerus, public;
create or replace function remove_payment(payment_slug uuid) returns void as
$$
declare
pid integer;
eid integer;
begin
select payment_id into pid from payment where slug = payment_slug;
if not found then
return;
end if;
delete from expense_payment where payment_id = pid returning expense_id into eid;
if eid is not null then
perform update_expense_payment_status(null, eid, 0);
end if;
delete from payment_attachment where payment_id = pid;
delete from payment where payment_id = pid;
end
$$
language plpgsql
;
revoke execute on function remove_payment(uuid) from public;
grant execute on function remove_payment(uuid) to invoicer;
grant execute on function remove_payment(uuid) to admin;
commit;

View File

@ -1,20 +0,0 @@
-- Deploy numerus:tax_details to pg
-- requires: schema_numerus
-- requires: extension_vat
-- requires: country_code
begin;
set search_path to numerus, public;
create type tax_details as (
business_name text,
vatin text,
address text,
city text,
province text,
postal_code text,
country_code country_code
);
commit;

View File

@ -1,53 +0,0 @@
-- Deploy numerus:update_expense_payment_status to pg
-- requires: roles
-- requires: schema_numerus
-- requires: expense
-- requires: payment
-- requires: expense_payment
-- requires: expense_tax_amount
-- requires: available_expense_status
-- requires: available_payment_status
begin;
set search_path to numerus, public;
create or replace function update_expense_payment_status(pid integer, eid integer, amount_cents integer) returns void as
$$
update payment
set payment_status = case
when (expense.amount > 0 and expense.amount + coalesce(tax.amount, 0) > -amount_cents) or (expense.amount < 0 and expense.amount + coalesce(tax.amount, 0) < -amount_cents) or exists (select 1 from expense_payment as ep where ep.expense_id = expense.expense_id and payment_id <> pid) then 'partial'
else 'complete' end
from expense
left join ( select expense_id, sum(amount) as amount from expense_tax_amount group by expense_id) as tax using (expense_id)
where expense.expense_id = eid
and payment_id = pid
;
update expense
set expense_status = case
when (expense.amount > 0 and -paid_amount >= expense.amount + tax_amount) or (expense.amount < 0 and -paid_amount <= expense.amount + tax_amount) then 'paid'
when paid_amount = 0 then 'pending'
else 'partial' end
from (
select coalesce (sum(payment.amount), 0) as paid_amount
from expense_payment
join payment using (payment_id)
where expense_payment.expense_id = eid
) as payment,
(
select coalesce (sum(amount), 0) as tax_amount
from expense_tax_amount
where expense_id = eid
) as tax
where expense.expense_id = eid
;
$$
language sql
;
revoke execute on function update_expense_payment_status(integer, integer, integer) from public;
grant execute on function update_expense_payment_status(integer, integer, integer) to invoicer;
grant execute on function update_expense_payment_status(integer, integer, integer) to admin;
commit;

View File

@ -1,51 +0,0 @@
-- Deploy numerus:update_expense_payment_status to pg
-- requires: roles
-- requires: schema_numerus
-- requires: expense
-- requires: payment
-- requires: expense_payment
-- requires: expense_tax_amount
-- requires: available_expense_status
-- requires: available_payment_status
begin;
set search_path to numerus, public;
create or replace function update_expense_payment_status(pid integer, eid integer, amount_cents integer) returns void as
$$
update payment
set payment_status = case when expense.amount + coalesce(tax.amount, 0) > amount_cents or exists (select 1 from expense_payment as ep where ep.expense_id = expense.expense_id and payment_id <> pid) then 'partial' else 'complete' end
from expense
left join ( select expense_id, sum(amount) as amount from expense_tax_amount group by expense_id) as tax using (expense_id)
where expense.expense_id = eid
and payment_id = pid
;
update expense
set expense_status = case
when paid_amount >= expense.amount + tax_amount then 'paid'
when paid_amount = 0 then 'pending'
else 'partial' end
from (
select coalesce (sum(payment.amount), 0) as paid_amount
from expense_payment
join payment using (payment_id)
where expense_payment.expense_id = eid
) as payment,
(
select coalesce (sum(amount), 0) as tax_amount
from expense_tax_amount
where expense_id = eid
) as tax
where expense.expense_id = eid
;
$$
language sql
;
revoke execute on function update_expense_payment_status(integer, integer, integer) from public;
grant execute on function update_expense_payment_status(integer, integer, integer) to invoicer;
grant execute on function update_expense_payment_status(integer, integer, integer) to admin;
commit;

View File

@ -1,51 +0,0 @@
-- Deploy numerus:update_invoice_collection_status to pg
-- requires: roles
-- requires: schema_numerus
-- requires: invoice
-- requires: payment
-- requires: invoice_payment
-- requires: invoice_amount
-- requires: available_invoice_status
-- requires: available_payment_status
begin;
set search_path to numerus, public;
create or replace function update_invoice_collection_status(cid integer, iid integer, amount_cents integer) returns void as
$$
update payment
set payment_status = case
when (invoice_amount.total > 0 and invoice_amount.total > amount_cents) or (invoice_amount.total < 0 and invoice_amount.total < amount_cents) or exists (select 1 from invoice_payment as ic where ic.invoice_id = invoice_amount.invoice_id and payment_id <> cid) then 'partial'
else 'complete' end
from invoice_amount
where invoice_id = iid
and payment_id = cid
;
update invoice
set invoice_status = case
when (total_amount > 0 and collected_amount >= total_amount) or (total_amount < 0 and collected_amount <= total_amount) then 'paid'
when collected_amount = 0 then 'created'
else 'partial' end
from (
select coalesce(sum(payment.amount), 0) as collected_amount
from invoice_payment
join payment using (payment_id)
where invoice_payment.invoice_id = iid
) as collection,
(
select total as total_amount
from invoice_amount
where invoice_id = iid
) as amount
where invoice.invoice_id = iid;
$$
language sql
;
revoke execute on function update_invoice_collection_status(integer, integer, integer) from public;
grant execute on function update_invoice_collection_status(integer, integer, integer) to invoicer;
grant execute on function update_invoice_collection_status(integer, integer, integer) to admin;
commit;

View File

@ -1,49 +0,0 @@
-- Deploy numerus:update_invoice_collection_status to pg
-- requires: roles
-- requires: schema_numerus
-- requires: invoice
-- requires: collection
-- requires: invoice_collection
-- requires: invoice_amount
-- requires: available_invoice_status
-- requires: available_payment_status
begin;
set search_path to numerus, public;
create or replace function update_invoice_collection_status(cid integer, iid integer, amount_cents integer) returns void as
$$
update collection
set payment_status = case when invoice_amount.total > amount_cents or exists (select 1 from invoice_collection as ic where ic.collection_id = invoice_amount.invoice_id and collection_id <> cid) then 'partial' else 'complete' end
from invoice_amount
where invoice_id = iid
and collection_id = cid
;
update invoice
set invoice_status = case
when collected_amount >= total_amount then 'paid'
when collected_amount = 0 then 'created'
else 'partial' end
from (
select coalesce(sum(collection.amount), 0) as collected_amount
from invoice_collection
join collection using (collection_id)
where invoice_collection.invoice_id = iid
) as collection,
(
select total as total_amount
from invoice_amount
where invoice_id = iid
) as amount
where invoice.invoice_id = iid;
$$
language sql
;
revoke execute on function update_invoice_collection_status(integer, integer, integer) from public;
grant execute on function update_invoice_collection_status(integer, integer, integer) to invoicer;
grant execute on function update_invoice_collection_status(integer, integer, integer) to admin;
commit;

19
go.mod
View File

@ -1,24 +1,15 @@
module dev.tandem.ws/tandem/numerus module dev.tandem.ws/tandem/numerus
go 1.19 go 1.16
require ( require (
github.com/jackc/pgtype v1.10.0 github.com/jackc/pgx/v4 v4.17.2
github.com/jackc/pgx/v4 v4.15.0
github.com/julienschmidt/httprouter v1.3.0 github.com/julienschmidt/httprouter v1.3.0
github.com/leonelquinteros/gotext v1.5.0 github.com/leonelquinteros/gotext v1.5.1
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8 golang.org/x/text v0.3.8
github.com/tealeg/xlsx v0.0.0-20181024002044-dbf71b6a931e
golang.org/x/text v0.7.0
) )
require ( require (
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgtype v1.12.0
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/puddle v1.3.0 // indirect
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
) )

45
go.sum
View File

@ -14,6 +14,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@ -24,7 +25,6 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
@ -35,6 +35,7 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
@ -42,7 +43,6 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
@ -51,18 +51,17 @@ github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01C
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
@ -70,14 +69,12 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leonelquinteros/gotext v1.5.0 h1:ODY7LzLpZWWSJdAHnzhreOr6cwLXTAmc914FOauSkBM= github.com/leonelquinteros/gotext v1.5.1 h1:vmddRn3gHp67YFjZLZE2AZsgYMT4IBTJhua4yfe7/4Q=
github.com/leonelquinteros/gotext v1.5.0/go.mod h1:OCiUVHuhP9LGFBQ1oAmdtNCHJCiHiQA8lf4nAifHkr0= github.com/leonelquinteros/gotext v1.5.1/go.mod h1:/A4Y7BvIsf5JHO60E43ZQDVkV3qO+7eP8HjeqD6ChIA=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -92,8 +89,6 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8 h1:iZTHFqK/oFrjyFDkiw5U/RjQxkMlkpq6tHQIO407i+s=
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
@ -116,8 +111,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/tealeg/xlsx v0.0.0-20181024002044-dbf71b6a931e h1:0AoAjM/7iqEZwTsWhk3nm9+H5mocFnh6dCGUaIOSTDQ= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/tealeg/xlsx v0.0.0-20181024002044-dbf71b6a931e/go.mod h1:uxu5UY2ovkuRPWKQ8Q7JG0JbSivrISjdPzZQKeo74mA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@ -139,19 +134,26 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -165,16 +167,20 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@ -182,15 +188,16 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=

257
guix.scm
View File

@ -1,257 +0,0 @@
(use-modules (gnu packages)
(gnu packages base)
(gnu packages compression)
(gnu packages databases)
(gnu packages geo)
(gnu packages messaging)
(gnu packages perl)
(gnu packages perl-check)
(gnu packages pkg-config)
(gnu packages protobuf)
(gnu packages web)
(gnu packages xml)
(guix build-system gnu)
(guix build-system perl)
(guix download)
(guix git-download)
(guix licenses)
(guix packages))
(define vat
(package
(name "vat")
(version "0.3")
(source (origin
(method url-fetch)
(uri (string-append
"https://dev.tandem.ws/tandem/" name "/archive/v" version ".tar.gz"))
(sha256
(base32
"0jbgakz7ip09hrnbbg1dm02n5zx7sv0magvw7s6g7rbbpy6wpqwh"))))
(build-system gnu-build-system)
(arguments
`(#:tests? #f
#:make-flags
(list (string-append "datadir=" (assoc-ref %outputs "out") "/share")
(string-append "docdir="(assoc-ref %outputs "out") "/share/doc")
(string-append "pkglibdir="(assoc-ref %outputs "out") "/lib")
(string-append "bindir=" (assoc-ref %outputs "out") "/bin"))
#:phases
(modify-phases %standard-phases
(delete 'configure))))
(inputs
`(("postgresql" ,postgresql-15)))
(home-page "https://dev.tandem.ws/tandem/vat/")
(synopsis "VAT identification numbers for PostgreSQL")
(description "VAT identification numbers library for PostgreSQL")
(license (x11-style "https://www.postgresql.org/about/licence/"))))
(define pg-libphonenumber
(let ((commit "753e2fa4be452620455a099aeda917648f2da70a")
(revision "1"))
(package
(name "pg-libphonenumber")
(version (git-version "0.1.0" revision commit))
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/blm768/pg-libphonenumber")
(commit commit)))
(file-name (git-file-name name version))
(sha256
(base32
"01syw93giq0pz80qzrdr79cr4p6w8lx129y1gsn2avc97r7pqanj"))))
(build-system gnu-build-system)
(arguments
`(#:tests? #f
#:make-flags
(list (string-append "datadir=" (assoc-ref %outputs "out") "/share")
(string-append "docdir="(assoc-ref %outputs "out") "/share/doc")
(string-append "pkglibdir="(assoc-ref %outputs "out") "/lib")
(string-append "bindir=" (assoc-ref %outputs "out") "/bin"))
#:phases
(modify-phases %standard-phases
(delete 'configure))))
(inputs
`(("postgresql" ,postgresql-15)
("libphonenumber" ,libphonenumber)
("protobuf" ,protobuf)))
(home-page "https://github.com/blm768/pg-libphonenumber")
(synopsis "PostgreSQL extension for libphonenumber")
(description "pg_libphonenumber is a (partially implemented!) PostgreSQL extension that provides access to Googles libphonenumber.")
(license asl2.0))))
(define pguri
(let ((commit "00241b96b8a285aa7ec0a81b5a4c0a664a044192")
(revision "1"))
(package
(name "pguri")
(version (git-version "1.20151224" revision commit))
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/petere/pguri")
(commit commit)))
(file-name (git-file-name name version))
(sha256
(base32
"0lz5nlqix60mxcjkqn3zn7q62xx0qbmybng3v0h049mf68l80ch9"))))
(build-system gnu-build-system)
(arguments
`(#:tests? #f
#:make-flags
(list (string-append "datadir=" (assoc-ref %outputs "out") "/share")
(string-append "docdir="(assoc-ref %outputs "out") "/share/doc")
(string-append "pkglibdir="(assoc-ref %outputs "out") "/lib")
(string-append "bindir=" (assoc-ref %outputs "out") "/bin"))
#:phases
(modify-phases %standard-phases
(delete 'configure))))
(inputs
`(("postgresql" ,postgresql-15)
("pkg-config" ,pkg-config)
("uriparser" ,uriparser)))
(home-page "https://github.com/petere/pguri")
(synopsis "uri type for PostgreSQL ")
(description "This is an extension for PostgreSQL that provides a uri data type. Advantages over using plain text for storing URIs include: URI syntax checking, functions for extracting URI components, and human-friendly sorting.")
(license asl2.0))))
(define postgresql-iban
(let ((commit "0e533afb4d6bdb5af615d71ee16db9528e501ba6")
(revision "1"))
(package
(name "PostgreSQL-IBAN")
(version (git-version "1.0.0" revision commit))
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/yorickdewid/PostgreSQL-IBAN.git")
(commit commit)))
(file-name (git-file-name name version))
(sha256
(base32
"1fqjk0amdr3mvhq6n7ig6lxs8xckm6vh5nxm8m1rlar82081agh2"))
(patches (search-patches "postgresql-iban-enable-nls.patch"))))
(build-system gnu-build-system)
(arguments
`(#:tests? #f
#:make-flags
(list (string-append "datadir=" (assoc-ref %outputs "out") "/share")
(string-append "docdir="(assoc-ref %outputs "out") "/share/doc")
(string-append "pkglibdir="(assoc-ref %outputs "out") "/lib")
(string-append "bindir=" (assoc-ref %outputs "out") "/bin"))
#:phases
(modify-phases %standard-phases
(delete 'configure))))
(inputs
`(("postgresql" ,postgresql-15)))
(home-page "https://github.com/yorickdewid/PostgreSQL-IBAN")
(synopsis "PostgreSQL extension that can verify International Bank Account Numbers")
(description "PostgreSQL IBAN extension that can verify International Bank Account Numbers. This ensures that only valid bank account numbers are stored.")
(license gpl3+))))
(define postgresql-15/xml
(package
(inherit postgresql-15)
(arguments
`(#:configure-flags '("--with-uuid=e2fs" "--with-openssl" "--with-libxml")
#:phases
(modify-phases %standard-phases
(add-before 'configure 'patch-/bin/sh
(lambda _
;; Refer to the actual shell.
(substitute* '("src/bin/pg_ctl/pg_ctl.c"
"src/bin/psql/command.c")
(("/bin/sh") (which "sh")))
#t))
(add-after 'build 'build-contrib
(lambda _
(invoke "make" "-C" "contrib")))
(add-after 'install 'install-contrib
(lambda _
(invoke "make" "-C" "contrib" "install"))))))
(inputs
`(("libxml2" ,libxml2)
,@(package-inputs postgresql-15)))))
(define perl-tap-parser-sourcehandler-pgtap
(package
(name "perl-tap-parser-sourcehandler-pgtap")
(version "3.36")
(source
(origin
(method url-fetch)
(uri (string-append "mirror://cpan/authors/id/D/DW/DWHEELER/"
"TAP-Parser-SourceHandler-pgTAP-" version ".tar.gz"))
(sha256
(base32 "0rwcx6z0xg1jrwnsyhb4a3aq3g7ff1a510g5v1paqgh65r9m3gh7"))))
(build-system perl-build-system)
(inputs
`(("perl-module-build" ,perl-module-build)
("perl-test-pod" ,perl-test-pod)
("perl-test-pod-coverage" ,perl-test-pod-coverage)))
(home-page "https://metacpan.org/pod/TAP::Parser::SourceHandler::pgTAP")
(synopsis "Stream TAP from pgTAP test scripts")
(description "This source handler executes pgTAP tests. It does two things: 1) Looks at the TAP::Parser::Source passed to it to determine whether or not the source in question is in fact a pgTAP test (\"can_handle\"). And, 2) Creates an iterator that will call psql to run the pgTAP tests (\"make_iterator\"). Unless you're writing a plugin or subclassing TAP::Parser, you probably won't need to use this module directly.")
(license perl-license)))
(define pgtap
(package
(name "pgtap")
(version "1.2.0")
(source (origin
(method url-fetch)
(uri (string-append
"https://api.pgxn.org/dist/pgtap/" version
"/pgtap-" version ".zip"))
(sha256
(base32
"106p24wslq39h9hrscf415x7s1nb6l21xjdzpg3dh73gawslfmqv"))))
(build-system gnu-build-system)
(arguments
`(#:tests? #f
#:make-flags
(list (string-append "datadir=" (assoc-ref %outputs "out") "/share")
(string-append "docdir="(assoc-ref %outputs "out") "/share/doc")
(string-append "pkglibdir="(assoc-ref %outputs "out") "/lib")
(string-append "bindir=" (assoc-ref %outputs "out") "/bin"))
#:phases
(modify-phases %standard-phases
(delete 'configure))))
(inputs
`(("postgresql" ,postgresql)))
(native-inputs
`(("perl" ,perl)
("which" ,which)
("unzip" ,unzip)))
(home-page "https://pgtap.org")
(synopsis "Unit testining for PostgreSQL")
(description "pgTAP is a suite of database functions that make it easy to write TAP-emitting unit tests in psql scripts or xUnit-style test functions.")
(license (x11-style "https://www.postgresql.org/about/licence/"))))
(package
(name "numerus")
(version "0.1.0")
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://dev.tandem.ws/tandem/numerus.git")
(commit "666935b54c8f2d735d8fe5591407ca231bba7e4c")))
(sha256
(base32
"0p31j7qing7nhnpngsnnfvx6wz70ryj4q5k7l81anh2z19nzwbk8"))))
(build-system gnu-build-system)
(inputs
`(("sqitch" ,sqitch)
("pgtap" ,pgtap)
("vat" ,vat)
("perl-tap-parser-sourcehandler-pgtap" ,perl-tap-parser-sourcehandler-pgtap)
("perl" ,perl)
("pg-libphonenumber" ,pg-libphonenumber)
("pguri" ,pguri)
("PostgreSQL-IBAN" ,postgresql-iban)
("postgresql" ,postgresql-15/xml)))
(synopsis "Simple invoicing and accounting web application")
(description "A simple web application to keep invoice and accouting records, intended for freelancers working in Spain.")
(home-page "https://dev.tandem.ws/tandem/numerus")
(license agpl3+))

View File

@ -1,338 +0,0 @@
package pkg
import (
"context"
"github.com/jackc/pgtype"
"github.com/julienschmidt/httprouter"
"html/template"
"net/http"
"time"
)
const (
ExpirationDateFormat = "01/06"
AccountTypeBank = "bank"
AccountTypeCard = "card"
AccountTypeCash = "cash"
AccountTypeOther = "other"
)
func servePaymentAccountIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
company := mustGetCompany(r)
locale := getLocale(r)
page := NewPaymentAccountIndexPage(r.Context(), conn, company, locale)
page.MustRender(w, r)
}
type PaymentAccountIndexPage struct {
Accounts []*PaymentAccountEntry
}
func NewPaymentAccountIndexPage(ctx context.Context, conn *Conn, company *Company, locale *Locale) *PaymentAccountIndexPage {
return &PaymentAccountIndexPage{
Accounts: mustCollectPaymentAccountEntries(ctx, conn, company, locale),
}
}
func (page *PaymentAccountIndexPage) MustRender(w http.ResponseWriter, r *http.Request) {
mustRenderMainTemplate(w, r, "payments/accounts/index.gohtml", page)
}
type PaymentAccountEntry struct {
ID int
Slug string
Type string
TypeLabel string
Name string
IBAN string
LastFourDigits string
ExpirationDate string
}
func mustCollectPaymentAccountEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale) []*PaymentAccountEntry {
rows := conn.MustQuery(ctx, `
select payment_account_id
, slug
, payment_account.payment_account_type
, coalesce(i18n.name, payment_account_type.name)
, payment_account.name
, coalesce(iban::text, '') as iban
, coalesce(last_four_digits, '') as last_four_digits
, expiration_date
from payment_account
left join payment_account_bank using (payment_account_id, payment_account_type)
left join payment_account_card using (payment_account_id, payment_account_type)
join payment_account_type using (payment_account_type)
left join payment_account_type_i18n as i18n on payment_account_type.payment_account_type = i18n.payment_account_type and i18n.lang_tag = $1
where company_id = $2
order by payment_account_id
`, locale.Language.String(), company.Id)
defer rows.Close()
var entries []*PaymentAccountEntry
for rows.Next() {
entry := &PaymentAccountEntry{}
var expirationDate pgtype.Date
if err := rows.Scan(&entry.ID, &entry.Slug, &entry.Type, &entry.TypeLabel, &entry.Name, &entry.IBAN, &entry.LastFourDigits, &expirationDate); err != nil {
panic(err)
}
if expirationDate.Status == pgtype.Present {
entry.ExpirationDate = expirationDate.Time.Format(ExpirationDateFormat)
}
entries = append(entries, entry)
}
if rows.Err() != nil {
panic(rows.Err())
}
return entries
}
func servePaymentAccountForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
form := newPaymentAccountForm(r.Context(), conn, locale, company)
slug := params[0].Value
if slug == "new" {
form.MustRender(w, r)
return
}
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
http.NotFound(w, r)
return
}
form.MustRender(w, r)
}
type PaymentAccountForm struct {
locale *Locale
company *Company
Slug string
Type *RadioField
Name *InputField
IBAN *InputField
LastFourDigits *InputField
ExpirationMonthYear *InputField
}
func newPaymentAccountForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *PaymentAccountForm {
return &PaymentAccountForm{
locale: locale,
company: company,
Type: &RadioField{
Name: "payment_account_type",
Label: pgettext("input", "Type", locale),
Required: true,
Options: MustGetRadioOptions(ctx, conn, "select payment_account_type, i18n.name from payment_account_type join payment_account_type_i18n as i18n using (payment_account_type) where i18n.lang_tag = $1 order by payment_account_type", locale.Language.String()),
Attributes: []template.HTMLAttr{
`x-model="type"`,
},
},
Name: &InputField{
Name: "name",
Label: pgettext("input", "Name", locale),
Required: true,
Type: "text",
},
IBAN: &InputField{
Name: "iban",
Label: pgettext("input", "IBAN", locale),
Required: true,
Type: "text",
},
LastFourDigits: &InputField{
Name: "last_four_digits",
Label: pgettext("input", "Cards last four digits", locale),
Required: true,
Type: "text",
Attributes: []template.HTMLAttr{
`maxlength="4"`,
`minlength="4"`,
`pattern="[0-9]{4}"`,
},
},
ExpirationMonthYear: &InputField{
Name: "expiration_date",
Label: pgettext("input", "Expiration date", locale),
Required: true,
Type: "text",
Attributes: []template.HTMLAttr{
`maxlength="5"`,
`minlength="5"`,
`pattern="[0-9]{2}/[0-9]{2}"`,
},
},
}
}
func (f *PaymentAccountForm) MustRender(w http.ResponseWriter, r *http.Request) {
if f.Slug == "" {
mustRenderMainTemplate(w, r, "payments/accounts/new.gohtml", f)
} else {
mustRenderMainTemplate(w, r, "payments/accounts/edit.gohtml", f)
}
}
func (f *PaymentAccountForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
selectedType := f.Type.Selected
var expirationDate pgtype.Date
if notFoundErrorOrPanic(conn.QueryRow(ctx, `
select payment_account_type
, name
, coalesce(iban::text, '') as iban
, coalesce(last_four_digits, '') as last_four_digits
, expiration_date
from payment_account
left join payment_account_bank using (payment_account_id, payment_account_type)
left join payment_account_card using (payment_account_id, payment_account_type)
where slug = $1
`, slug).Scan(
f.Type,
f.Name,
f.IBAN,
f.LastFourDigits,
&expirationDate)) {
f.Type.Selected = selectedType
return false
}
f.Slug = slug
if expirationDate.Status == pgtype.Present {
f.ExpirationMonthYear.Val = expirationDate.Time.Format(ExpirationDateFormat)
} else {
f.ExpirationMonthYear.Val = ""
}
return true
}
func (f *PaymentAccountForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
f.Type.FillValue(r)
f.Name.FillValue(r)
f.IBAN.FillValue(r)
f.LastFourDigits.FillValue(r)
f.ExpirationMonthYear.FillValue(r)
return nil
}
func (f *PaymentAccountForm) Validate(ctx context.Context, conn *Conn) bool {
validator := newFormValidator()
if validator.CheckValidRadioOption(f.Type, gettext("Selected payment account type is not valid.", f.locale)) {
switch f.Type.Selected {
case AccountTypeBank:
if validator.CheckRequiredInput(f.IBAN, gettext("IBAN can not be empty.", f.locale)) {
validator.CheckValidIBANInput(ctx, conn, f.IBAN, gettext("This value is not a valid IBAN.", f.locale))
}
case AccountTypeCard:
if validator.CheckRequiredInput(f.LastFourDigits, gettext("Last four digits can not be empty.", f.locale)) {
if validator.CheckInputLength(f.LastFourDigits, 4, gettext("You must enter the cards last four digits", f.locale)) {
validator.CheckValidInteger(f.LastFourDigits, 0, 9999, gettext("Last four digits must be a number.", f.locale))
}
}
if validator.CheckRequiredInput(f.ExpirationMonthYear, gettext("Expiration date can not be empty.", f.locale)) {
_, err := time.Parse(ExpirationDateFormat, f.ExpirationMonthYear.Val)
validator.checkInput(f.ExpirationMonthYear, err == nil, gettext("Expiration date should be a valid date in format MM/YY (e.g., 08/24).", f.locale))
}
}
}
validator.CheckRequiredInput(f.Name, gettext("Payment account name can not be empty.", f.locale))
return validator.AllOK()
}
func (f *PaymentAccountForm) ExpirationDate() (time.Time, error) {
date, err := time.Parse(ExpirationDateFormat, f.ExpirationMonthYear.Val)
if err != nil {
return date, err
}
return date.AddDate(0, 1, -1), nil
}
func handleAddPaymentAccount(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
form := newPaymentAccountForm(r.Context(), conn, locale, company)
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(r.Context(), conn) {
w.WriteHeader(http.StatusUnprocessableEntity)
form.MustRender(w, r)
return
}
switch form.Type.Selected {
case AccountTypeBank:
conn.MustExec(r.Context(), "select add_payment_account_bank($1, $2, $3)", company.Id, form.Name, form.IBAN)
case AccountTypeCard:
date, err := form.ExpirationDate()
if err != nil {
panic(err)
}
conn.MustExec(r.Context(), "select add_payment_account_card($1, $2, $3, $4)", company.Id, form.Name, form.LastFourDigits, date)
case AccountTypeCash:
conn.MustExec(r.Context(), "select add_payment_account_cash($1, $2)", company.Id, form.Name)
case AccountTypeOther:
conn.MustExec(r.Context(), "select add_payment_account_other($1, $2)", company.Id, form.Name)
}
htmxRedirect(w, r, companyURI(company, "/payment-accounts"))
}
func handleEditPaymentAccount(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
company := mustGetCompany(r)
form := newPaymentAccountForm(r.Context(), conn, locale, company)
form.Slug = params[0].Value
if !ValidUuid(form.Slug) {
http.NotFound(w, r)
return
}
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(r.Context(), conn) {
w.WriteHeader(http.StatusUnprocessableEntity)
form.MustRender(w, r)
return
}
var found string
switch form.Type.Selected {
case AccountTypeBank:
found = conn.MustGetText(r.Context(), "", "select edit_payment_account_bank($1, $2, $3)", form.Slug, form.Name, form.IBAN)
case AccountTypeCard:
date, err := form.ExpirationDate()
if err != nil {
panic(err)
}
found = conn.MustGetText(r.Context(), "", "select edit_payment_account_card($1, $2, $3, $4)", form.Slug, form.Name, form.LastFourDigits, date)
case AccountTypeCash:
found = conn.MustGetText(r.Context(), "", "select edit_payment_account_cash($1, $2)", form.Slug, form.Name)
case AccountTypeOther:
found = conn.MustGetText(r.Context(), "", "select edit_payment_account_other($1, $2)", form.Slug, form.Name)
}
if found == "" {
http.NotFound(w, r)
return
}
htmxRedirect(w, r, companyURI(company, "/payment-accounts"))
}

View File

@ -1,25 +0,0 @@
package pkg
import (
"github.com/julienschmidt/httprouter"
"net/http"
"strconv"
)
func serveAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params, sql string) {
slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
conn := getConn(r)
var contentType string
var content []byte
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), sql, slug).Scan(&contentType, &content)) {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(content)), 10))
_, _ = w.Write(content)
}

View File

@ -1,3 +0,0 @@
package pkg
const Version = "1.4~git"

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/tealeg/xlsx"
"html/template" "html/template"
"net/http" "net/http"
"strings" "strings"
@ -42,21 +41,13 @@ func IndexContacts(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
func GetContactForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func GetContactForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r) locale := getLocale(r)
conn := getConn(r) conn := getConn(r)
slug := params[0].Value
if slug == "import" {
ServeImportPage(w, r, params)
return
}
form := newContactForm(r.Context(), conn, locale) form := newContactForm(r.Context(), conn, locale)
slug := params[0].Value
if slug == "new" { if slug == "new" {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
mustRenderNewContactForm(w, r, form) mustRenderNewContactForm(w, r, form)
return return
} }
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
if !form.MustFillFromDatabase(r.Context(), conn, slug) { if !form.MustFillFromDatabase(r.Context(), conn, slug) {
http.NotFound(w, r) http.NotFound(w, r)
return return
@ -78,7 +69,7 @@ type editContactPage struct {
func mustRenderEditContactForm(w http.ResponseWriter, r *http.Request, slug string, form *contactForm) { func mustRenderEditContactForm(w http.ResponseWriter, r *http.Request, slug string, form *contactForm) {
page := &editContactPage{ page := &editContactPage{
Slug: slug, Slug: slug,
ContactName: form.Name.String(), ContactName: form.BusinessName.Val,
Form: form, Form: form,
} }
mustRenderMainTemplate(w, r, "contacts/edit.gohtml", page) mustRenderMainTemplate(w, r, "contacts/edit.gohtml", page)
@ -97,12 +88,14 @@ func HandleAddContact(w http.ResponseWriter, r *http.Request, _ httprouter.Param
return return
} }
if !form.Validate(r.Context(), conn) { if !form.Validate(r.Context(), conn) {
if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity) w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderNewContactForm(w, r, form) mustRenderNewContactForm(w, r, form)
return return
} }
company := mustGetCompany(r) company := mustGetCompany(r)
conn.MustExec(r.Context(), "select add_contact($1, $2, $3, $4, $5, $6, $7, $8, $9)", company.Id, form.Name, form.Phone, form.Email, form.Web, form.TaxDetails(), form.IBAN, form.BIC, form.Tags) conn.MustExec(r.Context(), "select add_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", company.Id, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Tags)
htmxRedirect(w, r, companyURI(company, "/contacts")) htmxRedirect(w, r, companyURI(company, "/contacts"))
} }
@ -122,12 +115,7 @@ func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprout
mustRenderEditContactForm(w, r, params[0].Value, form) mustRenderEditContactForm(w, r, params[0].Value, form)
return return
} }
slug := params[0].Value slug := conn.MustGetText(r.Context(), "", "select edit_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", params[0].Value, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Tags)
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
slug = conn.MustGetText(r.Context(), "", "select edit_contact($1, $2, $3, $4, $5, $6, $7, $8, $9)", slug, form.Name, form.Phone, form.Email, form.Web, form.TaxDetails(), form.IBAN, form.BIC, form.Tags)
if slug == "" { if slug == "" {
http.NotFound(w, r) http.NotFound(w, r)
} }
@ -179,11 +167,6 @@ func (form *contactFilterForm) Parse(r *http.Request) error {
return nil return nil
} }
func (form *contactFilterForm) HasValue() bool {
return form.Name.HasValue() ||
form.Tags.HasValue()
}
func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company, filters *contactFilterForm) []*ContactEntry { func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company, filters *contactFilterForm) []*ContactEntry {
args := []interface{}{company.Id} args := []interface{}{company.Id}
where := []string{"contact.company_id = $1"} where := []string{"contact.company_id = $1"}
@ -194,7 +177,7 @@ func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company
if filters != nil { if filters != nil {
name := strings.TrimSpace(filters.Name.String()) name := strings.TrimSpace(filters.Name.String())
if name != "" { if name != "" {
appendWhere("contact.name ilike $%d", "%"+name+"%") appendWhere("contact.business_name ilike $%d", "%"+name+"%")
} }
if len(filters.Tags.Tags) > 0 { if len(filters.Tags.Tags) > 0 {
if filters.TagsCondition.Selected == "and" { if filters.TagsCondition.Selected == "and" {
@ -206,15 +189,13 @@ func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company
} }
rows := conn.MustQuery(ctx, fmt.Sprintf(` rows := conn.MustQuery(ctx, fmt.Sprintf(`
select slug select slug
, name , business_name
, coalesce(email::text, '') , email
, coalesce(phone::text, '') , phone
, tags , tags
from contact from contact
left join contact_email using (contact_id)
left join contact_phone using (contact_id)
where (%s) where (%s)
order by name order by business_name
`, strings.Join(where, ") AND (")), args...) `, strings.Join(where, ") AND (")), args...)
defer rows.Close() defer rows.Close()
@ -235,10 +216,9 @@ func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company
type contactForm struct { type contactForm struct {
locale *Locale locale *Locale
Name *InputField
HasTaxDetails *CheckField
BusinessName *InputField BusinessName *InputField
VATIN *InputField VATIN *InputField
TradeName *InputField
Phone *InputField Phone *InputField
Email *InputField Email *InputField
Web *InputField Web *InputField
@ -247,48 +227,12 @@ type contactForm struct {
Province *InputField Province *InputField
PostalCode *InputField PostalCode *InputField
Country *SelectField Country *SelectField
IBAN *InputField
BIC *InputField
Tags *TagsField Tags *TagsField
} }
func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactForm { func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactForm {
return &contactForm{ return &contactForm{
locale: locale, locale: locale,
Name: &InputField{
Name: "name",
Label: pgettext("input", "Name", locale),
Type: "text",
Required: true,
},
Phone: &InputField{
Name: "phone",
Label: pgettext("input", "Phone", locale),
Type: "tel",
Attributes: []template.HTMLAttr{
`autocomplete="tel"`,
},
},
Email: &InputField{
Name: "email",
Label: pgettext("input", "Email", locale),
Type: "email",
Attributes: []template.HTMLAttr{
`autocomplete="email"`,
},
},
Web: &InputField{
Name: "web",
Label: pgettext("input", "Web", locale),
Type: "url",
Attributes: []template.HTMLAttr{
`autocomplete="url"`,
},
},
HasTaxDetails: &CheckField{
Name: "has_tax_details",
Label: pgettext("input", "Need to input tax details", locale),
},
BusinessName: &InputField{ BusinessName: &InputField{
Name: "business_name", Name: "business_name",
Label: pgettext("input", "Business name", locale), Label: pgettext("input", "Business name", locale),
@ -305,6 +249,37 @@ func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactFor
Type: "text", Type: "text",
Required: true, Required: true,
}, },
TradeName: &InputField{
Name: "trade_name",
Label: pgettext("input", "Trade name", locale),
Type: "text",
},
Phone: &InputField{
Name: "phone",
Label: pgettext("input", "Phone", locale),
Type: "tel",
Required: true,
Attributes: []template.HTMLAttr{
`autocomplete="tel"`,
},
},
Email: &InputField{
Name: "email",
Label: pgettext("input", "Email", locale),
Type: "email",
Required: true,
Attributes: []template.HTMLAttr{
`autocomplete="email"`,
},
},
Web: &InputField{
Name: "web",
Label: pgettext("input", "Web", locale),
Type: "url",
Attributes: []template.HTMLAttr{
`autocomplete="url"`,
},
},
Address: &InputField{ Address: &InputField{
Name: "address", Name: "address",
Label: pgettext("input", "Address", locale), Label: pgettext("input", "Address", locale),
@ -345,16 +320,6 @@ func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactFor
`autocomplete="country"`, `autocomplete="country"`,
}, },
}, },
IBAN: &InputField{
Name: "iban",
Label: pgettext("input", "IBAN", locale),
Type: "text",
},
BIC: &InputField{
Name: "bic",
Label: pgettext("bic", "BIC", locale),
Type: "text",
},
Tags: &TagsField{ Tags: &TagsField{
Name: "tags", Name: "tags",
Label: pgettext("input", "Tags", locale), Label: pgettext("input", "Tags", locale),
@ -366,10 +331,9 @@ func (form *contactForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
return err return err
} }
form.Name.FillValue(r)
form.HasTaxDetails.FillValue(r)
form.BusinessName.FillValue(r) form.BusinessName.FillValue(r)
form.VATIN.FillValue(r) form.VATIN.FillValue(r)
form.TradeName.FillValue(r)
form.Phone.FillValue(r) form.Phone.FillValue(r)
form.Email.FillValue(r) form.Email.FillValue(r)
form.Web.FillValue(r) form.Web.FillValue(r)
@ -378,8 +342,6 @@ func (form *contactForm) Parse(r *http.Request) error {
form.Province.FillValue(r) form.Province.FillValue(r)
form.PostalCode.FillValue(r) form.PostalCode.FillValue(r)
form.Country.FillValue(r) form.Country.FillValue(r)
form.IBAN.FillValue(r)
form.BIC.FillValue(r)
form.Tags.FillValue(r) form.Tags.FillValue(r)
return nil return nil
} }
@ -387,79 +349,54 @@ func (form *contactForm) Parse(r *http.Request) error {
func (form *contactForm) Validate(ctx context.Context, conn *Conn) bool { func (form *contactForm) Validate(ctx context.Context, conn *Conn) bool {
validator := newFormValidator() validator := newFormValidator()
country := "ES" country := ""
if form.HasTaxDetails.Checked {
if validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale)) { if validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale)) {
country = form.Country.Selected[0] country = form.Country.Selected[0]
} }
if validator.CheckRequiredInput(form.BusinessName, gettext("Business name can not be empty.", form.locale)) {
validator.CheckRequiredInput(form.BusinessName, gettext("Business name can not be empty.", form.locale))
validator.CheckInputMinLength(form.BusinessName, 2, gettext("Business name must have at least two letters.", form.locale)) validator.CheckInputMinLength(form.BusinessName, 2, gettext("Business name must have at least two letters.", form.locale))
}
if validator.CheckRequiredInput(form.VATIN, gettext("VAT number can not be empty.", form.locale)) { if validator.CheckRequiredInput(form.VATIN, gettext("VAT number can not be empty.", form.locale)) {
validator.CheckValidVATINInput(ctx, conn, form.VATIN, country, gettext("This value is not a valid VAT number.", form.locale)) validator.CheckValidVATINInput(form.VATIN, country, gettext("This value is not a valid VAT number.", form.locale))
} }
validator.CheckRequiredInput(form.Address, gettext("Address can not be empty.", form.locale)) if validator.CheckRequiredInput(form.Phone, gettext("Phone can not be empty.", form.locale)) {
validator.CheckRequiredInput(form.City, gettext("City can not be empty.", form.locale)) validator.CheckValidPhoneInput(form.Phone, country, gettext("This value is not a valid phone number.", form.locale))
validator.CheckRequiredInput(form.Province, gettext("Province can not be empty.", form.locale))
if validator.CheckRequiredInput(form.PostalCode, gettext("Postal code can not be empty.", form.locale)) {
validator.CheckValidPostalCode(ctx, conn, form.PostalCode, country, gettext("This value is not a valid postal code.", form.locale))
} }
} if validator.CheckRequiredInput(form.Email, gettext("Email can not be empty.", form.locale)) {
if validator.CheckRequiredInput(form.Name, gettext("Name can not be empty.", form.locale)) {
validator.CheckInputMinLength(form.Name, 2, gettext("Name must have at least two letters.", form.locale))
}
if form.Phone.Val != "" {
validator.CheckValidPhoneInput(ctx, conn, form.Phone, country, gettext("This value is not a valid phone number.", form.locale))
}
if form.Email.Val != "" {
validator.CheckValidEmailInput(form.Email, gettext("This value is not a valid email. It should be like name@domain.com.", form.locale)) validator.CheckValidEmailInput(form.Email, gettext("This value is not a valid email. It should be like name@domain.com.", form.locale))
} }
if form.Web.Val != "" { if form.Web.Val != "" {
validator.CheckValidURL(form.Web, gettext("This value is not a valid web address. It should be like https://domain.com/.", form.locale)) validator.CheckValidURL(form.Web, gettext("This value is not a valid web address. It should be like https://domain.com/.", form.locale))
} }
if form.IBAN.Val != "" { validator.CheckRequiredInput(form.Address, gettext("Address can not be empty.", form.locale))
validator.CheckValidIBANInput(ctx, conn, form.IBAN, gettext("This values is not a valid IBAN.", form.locale)) validator.CheckRequiredInput(form.City, gettext("City can not be empty.", form.locale))
validator.CheckRequiredInput(form.Province, gettext("Province can not be empty.", form.locale))
if validator.CheckRequiredInput(form.PostalCode, gettext("Postal code can not be empty.", form.locale)) {
validator.CheckValidPostalCode(ctx, conn, form.PostalCode, country, gettext("This value is not a valid postal code.", form.locale))
} }
if form.BIC.Val != "" {
validator.CheckValidBICInput(ctx, conn, form.BIC, gettext("This values is not a valid BIC.", form.locale))
}
return validator.AllOK() return validator.AllOK()
} }
func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool { func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
return !notFoundErrorOrPanic(conn.QueryRow(ctx, ` return !notFoundErrorOrPanic(conn.QueryRow(ctx, `
select name select business_name
, vatin is not null
, business_name
, substr(vatin::text, 3) , substr(vatin::text, 3)
, trade_name
, phone , phone
, email , email
, uri , web
, address , address
, city , city
, province , province
, postal_code , postal_code
, country_code , country_code
, iban
, bic
, tags , tags
from contact from contact
left join contact_email using (contact_id)
left join contact_phone using (contact_id)
left join contact_web using (contact_id)
left join contact_iban using (contact_id)
left join contact_swift using (contact_id)
left join contact_tax_details using (contact_id)
where slug = $1 where slug = $1
`, slug).Scan( `, slug).Scan(
form.Name,
form.HasTaxDetails,
form.BusinessName, form.BusinessName,
form.VATIN, form.VATIN,
form.TradeName,
form.Phone, form.Phone,
form.Email, form.Email,
form.Web, form.Web,
@ -468,72 +405,28 @@ func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
form.Province, form.Province,
form.PostalCode, form.PostalCode,
form.Country, form.Country,
form.IBAN,
form.BIC,
form.Tags)) form.Tags))
} }
func (form *contactForm) TaxDetails() *CustomerTaxDetails {
if !form.HasTaxDetails.Checked {
return nil
}
return &CustomerTaxDetails{
BusinessName: form.BusinessName.String(),
VATIN: form.VATIN.String(),
Address: form.Address.String(),
City: form.City.String(),
Province: form.Province.String(),
PostalCode: form.PostalCode.String(),
CountryCode: form.Country.String(),
}
}
func ServeEditContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeEditContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
serveTagsEditForm(w, r, params, "/contacts/", "select tags from contact where slug = $1") conn := getConn(r)
locale := getLocale(r)
company := getCompany(r)
slug := params[0].Value
form := newTagsForm(companyURI(company, "/contacts/"+slug+"/tags"), slug, locale)
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from contact where slug = $1`, form.Slug).Scan(form.Tags)) {
http.NotFound(w, r)
return
}
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
} }
func HandleUpdateContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func HandleUpdateContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
handleUpdateTags(w, r, params, "/contacts/", "update contact set tags = $1 where slug = $2 returning slug")
}
func ServeImportPage(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
form := newContactImportForm(getLocale(r), getCompany(r))
mustRenderMainTemplate(w, r, "contacts/import.gohtml", form)
}
type contactImportForm struct {
locale *Locale
company *Company
File *FileField
}
func newContactImportForm(locale *Locale, company *Company) *contactImportForm {
return &contactImportForm{
locale: locale,
company: company,
File: &FileField{
Name: "file",
Label: pgettext("input", "Holded Excel file", locale),
MaxSize: 1 << 20,
Required: true,
},
}
}
func (form *contactImportForm) Parse(r *http.Request) error {
if err := r.ParseMultipartForm(form.File.MaxSize); err != nil {
return err
}
if err := form.File.FillValue(r); err != nil {
return err
}
return nil
}
func HandleImportContacts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
locale := getLocale(r) locale := getLocale(r)
company := mustGetCompany(r) conn := getConn(r)
form := newContactImportForm(locale, company) company := getCompany(r)
slug := params[0].Value
form := newTagsForm(companyURI(company, "/contacts/"+slug+"/tags/edit"), slug, locale)
if err := form.Parse(r); err != nil { if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
@ -542,45 +435,8 @@ func HandleImportContacts(w http.ResponseWriter, r *http.Request, _ httprouter.P
http.Error(w, err.Error(), http.StatusForbidden) http.Error(w, err.Error(), http.StatusForbidden)
return return
} }
workbook, err := xlsx.OpenBinary(form.File.Content) if conn.MustGetText(r.Context(), "", "update contact set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
if err != nil { http.NotFound(w, r)
panic(err)
} }
conn := getConn(r) mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
tx := conn.MustBegin(r.Context())
defer tx.MustRollback(r.Context())
relation := tx.MustGetText(r.Context(), "select begin_import_contacts()")
columns := []string{"name", "vatin", "email", "phone", "address", "city", "postal_code", "province", "country_code", "iban", "bic", "tags"}
for _, sheet := range workbook.Sheets {
tx.MustCopyFrom(r.Context(), relation, columns, len(sheet.Rows)-4, func(idx int) ([]interface{}, error) {
row := sheet.Rows[idx+4]
var values []interface{}
if len(row.Cells) < 23 {
values = []interface{}{"", "", "", "", "", "", "", "", "", "", "", ""}
} else {
phone := row.Cells[5].String() // mobile
if phone == "" {
phone = row.Cells[4].String() // landline
}
values = []interface{}{
row.Cells[1].String(),
row.Cells[2].String(),
row.Cells[3].String(),
phone,
row.Cells[6].String(),
row.Cells[7].String(),
row.Cells[8].String(),
row.Cells[9].String(),
row.Cells[11].String(),
row.Cells[19].String(),
row.Cells[20].String(),
row.Cells[22].String(),
}
}
return values, nil
})
}
tx.MustExec(r.Context(), "select end_import_contacts($1)", company.Id)
tx.MustCommit(r.Context())
htmxRedirect(w, r, companyURI(company, "/contacts"))
} }

View File

@ -66,7 +66,7 @@ func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
rows := conn.MustQuery(r.Context(), fmt.Sprintf(` rows := conn.MustQuery(r.Context(), fmt.Sprintf(`
select to_price(0, decimal_digits) as sales select to_price(0, decimal_digits) as sales
, to_price(coalesce(invoice.total, 0), decimal_digits) as income , to_price(coalesce(invoice.total, 0), decimal_digits) as income
, to_price(coalesce(expense.total, 0) + coalesce(expense_tax.vat, 0) + coalesce(expense_tax.irpf, 0), decimal_digits) as expenses , to_price(coalesce(expense.total, 0), decimal_digits) as expenses
, to_price(coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0), decimal_digits) as vat , to_price(coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0), decimal_digits) as vat
, to_price(coalesce(invoice_tax.irpf, 0) + coalesce(expense_tax.irpf, 0), decimal_digits) as irpf , to_price(coalesce(invoice_tax.irpf, 0) + coalesce(expense_tax.irpf, 0), decimal_digits) as irpf
, to_price(coalesce(invoice.total, 0) - coalesce(expense.total, 0) - (coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0)) + coalesce(expense_tax.irpf, 0), decimal_digits) as net_income , to_price(coalesce(invoice.total, 0) - coalesce(expense.total, 0) - (coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0)) + coalesce(expense_tax.irpf, 0), decimal_digits) as net_income
@ -201,21 +201,9 @@ func buildDashboardChart(ctx context.Context, conn *Conn, locale *Locale, compan
) as invoice ) as invoice
left join ( left join (
select to_char(date.invoice_date, '%[3]s')::integer as date select to_char(date.invoice_date, '%[3]s')::integer as date
, sum(subtotal + taxes)::integer as total , sum(amount)::integer as total
from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date) from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date)
left join ( left join expense on expense.invoice_date = date.invoice_date and company_id = 1
select expense_id
, invoice_date
, expense.amount as subtotal
, coalesce(sum(tax.amount)::integer, 0) as taxes
from expense
left join expense_tax_amount as tax using (expense_id)
where company_id = $1
group by expense_id
, invoice_date
, expense.amount
) as expense
on expense.invoice_date = date.invoice_date
group by date group by date
) as expense using (date) ) as expense using (date)
order by date order by date

View File

@ -143,14 +143,6 @@ func (tx *Tx) MustExec(ctx context.Context, sql string, args ...interface{}) {
} }
} }
func (tx *Tx) MustGetText(ctx context.Context, sql string, args ...interface{}) string {
var result string
if err := tx.QueryRow(ctx, sql, args...).Scan(&result); err != nil {
panic(err)
}
return result
}
func (tx *Tx) MustGetInteger(ctx context.Context, sql string, args ...interface{}) int { func (tx *Tx) MustGetInteger(ctx context.Context, sql string, args ...interface{}) int {
var result int var result int
if err := tx.QueryRow(ctx, sql, args...).Scan(&result); err != nil { if err := tx.QueryRow(ctx, sql, args...).Scan(&result); err != nil {
@ -167,8 +159,8 @@ func (tx *Tx) MustGetIntegerOrDefault(ctx context.Context, def int, sql string,
return result return result
} }
func (tx *Tx) MustCopyFrom(ctx context.Context, tableName string, columns []string, length int, next func(int) ([]interface{}, error)) int64 { func (tx *Tx) MustCopyFrom(ctx context.Context, tableName string, columns []string, rows [][]interface{}) int64 {
copied, err := tx.CopyFrom(ctx, pgx.Identifier{tableName}, columns, pgx.CopyFromSlice(length, next)) copied, err := tx.CopyFrom(ctx, pgx.Identifier{tableName}, columns, pgx.CopyFromRows(rows))
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -13,28 +13,18 @@ import (
) )
type ExpenseEntry struct { type ExpenseEntry struct {
ID int
Slug string Slug string
InvoiceDate time.Time InvoiceDate time.Time
InvoiceNumber string InvoiceNumber string
Amount string Amount string
Taxes map[string]string
Total string
InvoicerName string InvoicerName string
OriginalFileName string OriginalFileName string
Tags []string Tags []string
Status string
StatusLabel string
} }
type expensesIndexPage struct { type expensesIndexPage struct {
Expenses []*ExpenseEntry Expenses []*ExpenseEntry
SumAmount string
SumTaxes map[string]string
SumTotal string
Filters *expenseFilterForm Filters *expenseFilterForm
TaxClasses []string
ExpenseStatuses map[string]string
} }
func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@ -47,66 +37,65 @@ func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
return return
} }
page := &expensesIndexPage{ page := &expensesIndexPage{
Expenses: mustCollectExpenseEntries(r.Context(), conn, locale, filters), Expenses: mustCollectExpenseEntries(r.Context(), conn, company, filters),
ExpenseStatuses: mustCollectExpenseStatuses(r.Context(), conn, locale),
TaxClasses: mustCollectTaxClasses(r.Context(), conn, company),
Filters: filters, Filters: filters,
} }
page.mustComputeExpensesTotalAmount(r.Context(), conn, filters)
mustRenderMainTemplate(w, r, "expenses/index.gohtml", page) mustRenderMainTemplate(w, r, "expenses/index.gohtml", page)
} }
func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale, filters *expenseFilterForm) []*ExpenseEntry { func mustCollectExpenseEntries(ctx context.Context, conn *Conn, company *Company, filters *expenseFilterForm) []*ExpenseEntry {
where, args := filters.BuildQuery([]interface{}{locale.Language.String()}) args := []interface{}{company.Id}
where := []string{"expense.company_id = $1"}
appendWhere := func(expression string, value interface{}) {
args = append(args, value)
where = append(where, fmt.Sprintf(expression, len(args)))
}
maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) {
if value != "" {
if conv == nil {
appendWhere(expression, value)
} else {
appendWhere(expression, conv(value))
}
}
}
maybeAppendWhere("contact_id = $%d", filters.Customer.String(), func(v string) interface{} {
customerId, _ := strconv.Atoi(filters.Customer.Selected[0])
return customerId
})
maybeAppendWhere("invoice_number = $%d", filters.InvoiceNumber.String(), nil)
maybeAppendWhere("invoice_date >= $%d", filters.FromDate.String(), nil)
maybeAppendWhere("invoice_date <= $%d", filters.ToDate.String(), nil)
if len(filters.Tags.Tags) > 0 {
if filters.TagsCondition.Selected == "and" {
appendWhere("expense.tags @> $%d", filters.Tags)
} else {
appendWhere("expense.tags && $%d", filters.Tags)
}
}
rows := conn.MustQuery(ctx, fmt.Sprintf(` rows := conn.MustQuery(ctx, fmt.Sprintf(`
select expense_id select expense.slug
, expense.slug
, invoice_date , invoice_date
, invoice_number , invoice_number
, to_price(expense.amount, decimal_digits) as amount , to_price(amount, decimal_digits)
, array_agg(array[tax_class.name, to_price(coalesce(expense_tax.amount, 0), decimal_digits)]) filter (where tax_class.name is not null) , contact.business_name
, to_price(expense.amount + coalesce(sum(expense_tax.amount)::integer, 0), decimal_digits) as total
, contact.name
, coalesce(attachment.original_filename, '') , coalesce(attachment.original_filename, '')
, expense.tags , expense.tags
, expense.expense_status
, esi18n.name
from expense from expense
left join expense_attachment as attachment using (expense_id) left join expense_attachment as attachment using (expense_id)
left join expense_tax_amount as expense_tax using (expense_id)
left join tax using (tax_id)
left join tax_class using (tax_class_id)
join contact using (contact_id) join contact using (contact_id)
join expense_status_i18n esi18n on expense.expense_status = esi18n.expense_status and esi18n.lang_tag = $1
join currency using (currency_code) join currency using (currency_code)
where (%s) where (%s)
group by expense_id order by invoice_date
, expense.slug `, strings.Join(where, ") AND (")), args...)
, invoice_date
, invoice_number
, expense.amount
, decimal_digits
, contact.name
, attachment.original_filename
, expense.tags
, expense.expense_status
, esi18n.name
order by invoice_date desc, contact.name, total desc
`, where), args...)
defer rows.Close() defer rows.Close()
var entries []*ExpenseEntry var entries []*ExpenseEntry
for rows.Next() { for rows.Next() {
entry := &ExpenseEntry{ entry := &ExpenseEntry{}
Taxes: make(map[string]string), if err := rows.Scan(&entry.Slug, &entry.InvoiceDate, &entry.InvoiceNumber, &entry.Amount, &entry.InvoicerName, &entry.OriginalFileName, &entry.Tags); err != nil {
}
var taxes [][]string
if err := rows.Scan(&entry.ID, &entry.Slug, &entry.InvoiceDate, &entry.InvoiceNumber, &entry.Amount, &taxes, &entry.Total, &entry.InvoicerName, &entry.OriginalFileName, &entry.Tags, &entry.Status, &entry.StatusLabel); err != nil {
panic(err) panic(err)
} }
for _, tax := range taxes {
entry.Taxes[tax[0]] = tax[1]
}
entries = append(entries, entry) entries = append(entries, entry)
} }
if rows.Err() != nil { if rows.Err() != nil {
@ -115,99 +104,6 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale,
return entries return entries
} }
func mustCollectExpenseStatuses(ctx context.Context, conn *Conn, locale *Locale) map[string]string {
rows := conn.MustQuery(ctx, `
select expense_status.expense_status
, esi18n.name
from expense_status
join expense_status_i18n esi18n using(expense_status)
where esi18n.lang_tag = $1
order by expense_status`, locale.Language.String())
defer rows.Close()
statuses := map[string]string{}
for rows.Next() {
var key, name string
if err := rows.Scan(&key, &name); err != nil {
panic(err)
}
statuses[key] = name
}
if rows.Err() != nil {
panic(rows.Err())
}
return statuses
}
func (page *expensesIndexPage) mustComputeExpensesTotalAmount(ctx context.Context, conn *Conn, filters *expenseFilterForm) {
where, args := filters.BuildQuery(nil)
row := conn.QueryRow(ctx, fmt.Sprintf(`
select to_price(sum(subtotal)::integer, decimal_digits)
, to_price(sum(subtotal + taxes)::integer, decimal_digits)
from (
select expense_id
, expense.amount as subtotal
, coalesce(sum(tax.amount)::integer, 0) as taxes
, currency_code
from expense
left join expense_tax_amount as tax using (expense_id)
where (%s)
group by expense_id
, expense.amount
, currency_code
) as expense
join currency using (currency_code)
group by decimal_digits
`, where), args...)
if notFoundErrorOrPanic(row.Scan(&page.SumAmount, &page.SumTotal)) {
page.SumAmount = "0.0"
page.SumTotal = "0.0"
}
row = conn.QueryRow(ctx, fmt.Sprintf(`
select array_agg(array[tax_class_name, to_price(coalesce(tax_amount, 0), decimal_digits)]) filter (where tax_class_name is not null)
from (
select tax_class.name as tax_class_name
, coalesce(sum(expense_tax.amount)::integer, 0) as tax_amount
, currency_code
from expense
left join expense_tax_amount as expense_tax using (expense_id)
left join tax using (tax_id)
left join tax_class using (tax_class_id)
where (%s)
group by tax_class.name
, currency_code
) as tax
join currency using (currency_code)
group by decimal_digits
`, where), args...)
var taxes [][]string
if notFoundErrorOrPanic(row.Scan(&taxes)) {
// well, nothing to do
}
page.SumTaxes = make(map[string]string)
for _, tax := range taxes {
page.SumTaxes[tax[0]] = tax[1]
}
}
func mustCollectTaxClasses(ctx context.Context, conn *Conn, company *Company) []string {
rows := conn.MustQuery(ctx, "select name from tax_class where company_id = $1", company.Id)
defer rows.Close()
var taxClasses []string
for rows.Next() {
var taxClass string
if err := rows.Scan(&taxClass); err != nil {
panic(err)
}
taxClasses = append(taxClasses, taxClass)
}
return taxClasses
}
func ServeExpenseForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeExpenseForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r) locale := getLocale(r)
conn := getConn(r) conn := getConn(r)
@ -220,10 +116,6 @@ func ServeExpenseForm(w http.ResponseWriter, r *http.Request, params httprouter.
mustRenderNewExpenseForm(w, r, form) mustRenderNewExpenseForm(w, r, form)
return return
} }
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
if !form.MustFillFromDatabase(r.Context(), conn, slug) { if !form.MustFillFromDatabase(r.Context(), conn, slug) {
http.NotFound(w, r) http.NotFound(w, r)
return return
@ -235,40 +127,20 @@ func ServeExpenseForm(w http.ResponseWriter, r *http.Request, params httprouter.
func mustRenderNewExpenseForm(w http.ResponseWriter, r *http.Request, form *expenseForm) { func mustRenderNewExpenseForm(w http.ResponseWriter, r *http.Request, form *expenseForm) {
locale := getLocale(r) locale := getLocale(r)
form.Invoicer.EmptyLabel = gettext("Select a contact.", locale) form.Invoicer.EmptyLabel = gettext("Select a contact.", locale)
page := newNewExpensePage(form, r) mustRenderMainTemplate(w, r, "expenses/new.gohtml", form)
mustRenderMainTemplate(w, r, "expenses/new.gohtml", page)
}
type newExpensePage struct {
Form *expenseForm
Taxes [][]string
Total string
}
func newNewExpensePage(form *expenseForm, r *http.Request) *newExpensePage {
page := &newExpensePage{
Form: form,
}
conn := getConn(r)
company := mustGetCompany(r)
err := conn.QueryRow(r.Context(), "select taxes, total from compute_new_expense_amount($1, $2, $3)", company.Id, form.Amount, form.Tax.Selected).Scan(&page.Taxes, &page.Total)
if err != nil {
panic(err)
}
return page
} }
func mustRenderEditExpenseForm(w http.ResponseWriter, r *http.Request, slug string, form *expenseForm) { func mustRenderEditExpenseForm(w http.ResponseWriter, r *http.Request, slug string, form *expenseForm) {
page := &editExpensePage{ page := &editExpensePage{
newNewExpensePage(form, r), Slug: slug,
slug, Form: form,
} }
mustRenderMainTemplate(w, r, "expenses/edit.gohtml", page) mustRenderMainTemplate(w, r, "expenses/edit.gohtml", page)
} }
type editExpensePage struct { type editExpensePage struct {
*newExpensePage
Slug string Slug string
Form *expenseForm
} }
type expenseForm struct { type expenseForm struct {
@ -284,7 +156,6 @@ type expenseForm struct {
} }
func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *expenseForm { func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *expenseForm {
triggerRecompute := template.HTMLAttr(`data-hx-on="change: this.dispatchEvent(new CustomEvent('recompute', {bubbles: true}))"`)
return &expenseForm{ return &expenseForm{
locale: locale, locale: locale,
company: company, company: company,
@ -310,9 +181,6 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Label: pgettext("input", "Taxes", locale), Label: pgettext("input", "Taxes", locale),
Multiple: true, Multiple: true,
Options: mustGetTaxOptions(ctx, conn, company), Options: mustGetTaxOptions(ctx, conn, company),
Attributes: []template.HTMLAttr{
triggerRecompute,
},
}, },
Amount: &InputField{ Amount: &InputField{
Name: "amount", Name: "amount",
@ -320,7 +188,7 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Type: "number", Type: "number",
Required: true, Required: true,
Attributes: []template.HTMLAttr{ Attributes: []template.HTMLAttr{
triggerRecompute, `min="0"`,
template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())), template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
}, },
}, },
@ -336,16 +204,6 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
} }
} }
func mustGetExpenseStatusOptions(ctx context.Context, conn *Conn, locale *Locale) []*SelectOption {
return MustGetOptions(ctx, conn, `
select expense_status.expense_status
, esi18n.name
from expense_status
join expense_status_i18n esi18n using(expense_status)
where esi18n.lang_tag = $1
order by expense_status`, locale.Language.String())
}
func (form *expenseForm) Parse(r *http.Request) error { func (form *expenseForm) Parse(r *http.Request) error {
if err := r.ParseMultipartForm(form.File.MaxSize); err != nil { if err := r.ParseMultipartForm(form.File.MaxSize); err != nil {
return err return err
@ -369,18 +227,20 @@ func (form *expenseForm) Validate() bool {
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale)) validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale)) validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
if validator.CheckRequiredInput(form.Amount, gettext("Amount can not be empty.", form.locale)) { if validator.CheckRequiredInput(form.Amount, gettext("Amount can not be empty.", form.locale)) {
validator.CheckValidDecimal(form.Amount, -math.MaxFloat64, math.MaxFloat64, gettext("Amount must be a decimal number.", form.locale)) validator.CheckValidDecimal(form.Amount, form.company.MinCents(), math.MaxFloat64, gettext("Amount must be a number greater than zero.", form.locale))
} }
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
return validator.AllOK() return validator.AllOK()
} }
func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool { func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
if notFoundErrorOrPanic(conn.QueryRow(ctx, ` return !notFoundErrorOrPanic(conn.QueryRow(ctx, `
select contact_id select contact_id
, invoice_number , invoice_number
, invoice_date , invoice_date
, to_price(amount, decimal_digits) , to_price(amount, decimal_digits)
, array_agg(tax_id) filter ( where tax_id is not null ) , array_agg(tax_id)
, tags , tags
from expense from expense
left join expense_tax using (expense_id) left join expense_tax using (expense_id)
@ -398,14 +258,36 @@ func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
form.InvoiceDate, form.InvoiceDate,
form.Amount, form.Amount,
form.Tax, form.Tax,
form.Tags)) { form.Tags))
return false
}
if len(form.Tax.Selected) == 1 && form.Tax.Selected[0] == "" {
form.Tax.Selected = nil
}
return true
} }
func HandleAddExpense(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
company := mustGetCompany(r)
form := newExpenseForm(r.Context(), conn, locale, company)
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() {
if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderNewExpenseForm(w, r, form)
return
}
taxes := mustSliceAtoi(form.Tax.Selected)
slug := conn.MustGetText(r.Context(), "", "select add_expense($1, $2, $3, $4, $5, $6, $7)", company.Id, form.InvoiceDate, form.Invoicer, form.InvoiceNumber, form.Amount, taxes, form.Tags)
if len(form.File.Content) > 0 {
conn.MustQuery(r.Context(), "select attach_to_expense($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
}
htmxRedirect(w, r, companyURI(company, "/expenses"))
}
func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
conn := getConn(r) conn := getConn(r)
locale := getLocale(r) locale := getLocale(r)
@ -420,12 +302,10 @@ func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprout
return return
} }
slug := params[0].Value slug := params[0].Value
if !ValidUuid(slug) {
http.NotFound(w, r)
return
}
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity) w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderEditExpenseForm(w, r, slug, form) mustRenderEditExpenseForm(w, r, slug, form)
return return
} }
@ -443,11 +323,10 @@ func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprout
type expenseFilterForm struct { type expenseFilterForm struct {
locale *Locale locale *Locale
company *Company company *Company
Contact *SelectField Customer *SelectField
InvoiceNumber *InputField InvoiceNumber *InputField
FromDate *InputField FromDate *InputField
ToDate *InputField ToDate *InputField
ExpenseStatus *SelectField
Tags *TagsField Tags *TagsField
TagsCondition *ToggleField TagsCondition *ToggleField
} }
@ -456,10 +335,10 @@ func newExpenseFilterForm(ctx context.Context, conn *Conn, locale *Locale, compa
return &expenseFilterForm{ return &expenseFilterForm{
locale: locale, locale: locale,
company: company, company: company,
Contact: &SelectField{ Customer: &SelectField{
Name: "contact", Name: "customer",
Label: pgettext("input", "Contact", locale), Label: pgettext("input", "Customer", locale),
EmptyLabel: gettext("All contacts", locale), EmptyLabel: gettext("All customers", locale),
Options: mustGetContactOptions(ctx, conn, company), Options: mustGetContactOptions(ctx, conn, company),
}, },
InvoiceNumber: &InputField{ InvoiceNumber: &InputField{
@ -481,12 +360,6 @@ func newExpenseFilterForm(ctx context.Context, conn *Conn, locale *Locale, compa
Name: "tags", Name: "tags",
Label: pgettext("input", "Tags", locale), Label: pgettext("input", "Tags", locale),
}, },
ExpenseStatus: &SelectField{
Name: "expense_status",
Label: pgettext("input", "Expense Status", locale),
EmptyLabel: gettext("All status", locale),
Options: mustGetExpenseStatusOptions(ctx, conn, locale),
},
TagsCondition: &ToggleField{ TagsCondition: &ToggleField{
Name: "tags_condition", Name: "tags_condition",
Label: pgettext("input", "Tags Condition", locale), Label: pgettext("input", "Tags Condition", locale),
@ -509,107 +382,34 @@ func (form *expenseFilterForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
return err return err
} }
form.Contact.FillValue(r) form.Customer.FillValue(r)
form.InvoiceNumber.FillValue(r) form.InvoiceNumber.FillValue(r)
form.FromDate.FillValue(r) form.FromDate.FillValue(r)
form.ToDate.FillValue(r) form.ToDate.FillValue(r)
form.ExpenseStatus.FillValue(r)
form.Tags.FillValue(r) form.Tags.FillValue(r)
form.TagsCondition.FillValue(r) form.TagsCondition.FillValue(r)
return nil return nil
} }
func (form *expenseFilterForm) HasValue() bool {
return form.Contact.HasValue() ||
form.InvoiceNumber.HasValue() ||
form.FromDate.HasValue() ||
form.ToDate.HasValue() ||
form.ExpenseStatus.HasValue() ||
form.Tags.HasValue()
}
func (form *expenseFilterForm) BuildQuery(args []interface{}) (string, []interface{}) {
var where []string
appendWhere := func(expression string, value interface{}) {
args = append(args, value)
where = append(where, fmt.Sprintf(expression, len(args)))
}
maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) {
if value != "" {
if conv == nil {
appendWhere(expression, value)
} else {
appendWhere(expression, conv(value))
}
}
}
appendWhere("expense.company_id = $%d", form.company.Id)
maybeAppendWhere("contact_id = $%d", form.Contact.String(), func(v string) interface{} {
customerId, _ := strconv.Atoi(form.Contact.Selected[0])
return customerId
})
maybeAppendWhere("expense.expense_status = $%d", form.ExpenseStatus.String(), nil)
maybeAppendWhere("invoice_number = $%d", form.InvoiceNumber.String(), nil)
maybeAppendWhere("invoice_date >= $%d", form.FromDate.String(), nil)
maybeAppendWhere("invoice_date <= $%d", form.ToDate.String(), nil)
if len(form.Tags.Tags) > 0 {
if form.TagsCondition.Selected == "and" {
appendWhere("expense.tags @> $%d", form.Tags)
} else {
appendWhere("expense.tags && $%d", form.Tags)
}
}
return strings.Join(where, ") AND ("), args
}
func ServeEditExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func ServeEditExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
serveTagsEditForm(w, r, params, "/expenses/", "select tags from expense where slug = $1") conn := getConn(r)
} locale := getLocale(r)
company := getCompany(r)
func HandleUpdateExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
handleUpdateTags(w, r, params, "/expenses/", "update expense set tags = $1 where slug = $2 returning slug")
}
func ServeExpenseAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
serveAttachment(w, r, params, `
select mime_type
, content
from expense
join expense_attachment using (expense_id)
where slug = $1
`)
}
func HandleEditExpenseAction(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
slug := params[0].Value slug := params[0].Value
switch slug { form := newTagsForm(companyURI(company, "/expenses/"+slug+"/tags"), slug, locale)
case "batch": if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from expense where slug = $1`, form.Slug).Scan(form.Tags)) {
HandleBatchExpenseAction(w, r, params)
default:
if !ValidUuid(slug) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
actionUri := fmt.Sprintf("/invoices/%s/edit", slug) mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
handleExpenseAction(w, r, actionUri, func(w http.ResponseWriter, r *http.Request, form *expenseForm) {
mustRenderEditExpenseForm(w, r, slug, form)
})
}
} }
func HandleNewExpenseAction(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func HandleUpdateExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
handleExpenseAction(w, r, "/expenses", mustRenderNewExpenseForm)
}
type renderExpenseFormFunc func(w http.ResponseWriter, r *http.Request, form *expenseForm)
func handleExpenseAction(w http.ResponseWriter, r *http.Request, action string, renderForm renderExpenseFormFunc) {
locale := getLocale(r) locale := getLocale(r)
conn := getConn(r) conn := getConn(r)
company := mustGetCompany(r) company := getCompany(r)
form := newExpenseForm(r.Context(), conn, locale, company) slug := params[0].Value
form := newTagsForm(companyURI(company, "/expenses/"+slug+"/tags/edit"), slug, locale)
if err := form.Parse(r); err != nil { if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
@ -618,117 +418,29 @@ func handleExpenseAction(w http.ResponseWriter, r *http.Request, action string,
http.Error(w, err.Error(), http.StatusForbidden) http.Error(w, err.Error(), http.StatusForbidden)
return return
} }
actionField := r.Form.Get("action") if conn.MustGetText(r.Context(), "", "update expense set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
switch actionField { http.NotFound(w, r)
case "update":
// Nothing else to do
w.WriteHeader(http.StatusOK)
renderForm(w, r, form)
case "add":
if !form.Validate() {
w.WriteHeader(http.StatusUnprocessableEntity)
renderForm(w, r, form)
return
}
taxes := mustSliceAtoi(form.Tax.Selected)
slug := conn.MustGetText(r.Context(), "", "select add_expense($1, $2, $3, $4, $5, $6, $7)", company.Id, form.InvoiceDate, form.Invoicer, form.InvoiceNumber, form.Amount, taxes, form.Tags)
if len(form.File.Content) > 0 {
conn.MustQuery(r.Context(), "select attach_to_expense($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
}
htmxRedirect(w, r, companyURI(company, action))
default:
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
} }
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
} }
func HandleBatchExpenseAction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func ServeExpenseAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
locale := getLocale(r)
switch r.Form.Get("action") {
case "export":
conn := getConn(r)
company := getCompany(r)
filters := newExpenseFilterForm(r.Context(), conn, locale, company)
if err := filters.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
entries := mustCollectExpenseEntries(r.Context(), conn, locale, filters)
vatin := mustCollectExpenseEntriesVATIN(r.Context(), conn, entries)
lastPaymentDate := mustCollectExpenseEntriesLastPaymentDate(r.Context(), conn, entries)
taxes := mustCollectExpenseEntriesTaxes(r.Context(), conn, entries)
taxColumns := mustCollectTaxColumns(r.Context(), conn, company)
ods := mustWriteExpensesOds(entries, vatin, lastPaymentDate, taxes, taxColumns, locale, company)
writeOdsResponse(w, ods, gettext("expenses.ods", locale))
default:
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
}
}
func mustCollectExpenseEntriesTaxes(ctx context.Context, conn *Conn, entries []*ExpenseEntry) map[int]taxMap {
ids := mustMakeIDArray(entries, func(entry *ExpenseEntry) int {
return entry.ID
})
return mustMakeTaxMap(ctx, conn, ids, `
select expense_id
, tax_id
, to_price(tax.amount, decimal_digits)
from expense_tax_amount as tax
join expense using (expense_id)
join currency using (currency_code)
where expense_id = any ($1)
`)
}
func mustCollectExpenseEntriesVATIN(ctx context.Context, conn *Conn, entries []*ExpenseEntry) map[int]string {
ids := mustMakeIDArray(entries, func(entry *ExpenseEntry) int {
return entry.ID
})
return mustMakeVATINMap(ctx, conn, ids, `
select expense_id
, vatin::text
from contact_tax_details as tax
join expense using (contact_id)
where expense_id = any ($1)
`)
}
func mustCollectExpenseEntriesLastPaymentDate(ctx context.Context, conn *Conn, entries []*ExpenseEntry) map[int]time.Time {
ids := mustMakeIDArray(entries, func(entry *ExpenseEntry) int {
return entry.ID
})
return mustMakeDateMap(ctx, conn, ids, `
select expense_id
, max(payment_date)
from expense_payment
join payment using (payment_id)
where expense_id = any ($1)
group by expense_id
`)
}
func handleRemoveExpense(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
slug := params[0].Value slug := params[0].Value
if !ValidUuid(slug) { conn := getConn(r)
var contentType string
var content []byte
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `
select mime_type
, content
from expense
join expense_attachment using (expense_id)
where slug = $1
`, slug).Scan(&contentType, &content)) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
w.Header().Set("Content-Type", contentType)
if err := verifyCsrfTokenValid(r); err != nil { w.Header().Set("Content-Length", strconv.FormatInt(int64(len(content)), 10))
http.Error(w, err.Error(), http.StatusForbidden) w.WriteHeader(http.StatusOK)
return w.Write(content)
}
conn := getConn(r)
conn.MustExec(r.Context(), "select remove_expense($1)", slug)
company := mustGetCompany(r)
htmxRedirect(w, r, companyURI(company, "/expenses"))
} }

View File

@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"github.com/jackc/pgtype" "github.com/jackc/pgtype"
"html/template" "html/template"
"io" "io/ioutil"
"net/http" "net/http"
"net/mail" "net/mail"
"net/url" "net/url"
@ -59,10 +59,6 @@ func (field *InputField) Value() (driver.Value, error) {
return field.Val, nil return field.Val, nil
} }
func (field *InputField) HasValue() bool {
return field.Val != ""
}
func (field *InputField) FillValue(r *http.Request) { func (field *InputField) FillValue(r *http.Request) {
field.Val = strings.TrimSpace(r.FormValue(field.Name)) field.Val = strings.TrimSpace(r.FormValue(field.Name))
} }
@ -188,10 +184,6 @@ func (field *SelectField) Clear() {
field.Selected = []string{} field.Selected = []string{}
} }
func (field *SelectField) HasValue() bool {
return len(field.Selected) > 0 && field.Selected[0] != ""
}
func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption { func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption {
rows, err := conn.Query(ctx, sql, args...) rows, err := conn.Query(ctx, sql, args...)
if err != nil { if err != nil {
@ -295,64 +287,6 @@ func (field *RadioField) isValidOption(selected string) bool {
return field.FindOption(selected) != nil return field.FindOption(selected) != nil
} }
func (field *RadioField) HasValidOption() bool {
return field.isValidOption(field.Selected)
}
func MustGetRadioOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*RadioOption {
rows, err := conn.Query(ctx, sql, args...)
if err != nil {
panic(err)
}
defer rows.Close()
var options []*RadioOption
for rows.Next() {
option := &RadioOption{}
err = rows.Scan(&option.Value, &option.Label)
if err != nil {
panic(err)
}
options = append(options, option)
}
if rows.Err() != nil {
panic(rows.Err())
}
return options
}
type CheckField struct {
Name string
Label string
Checked bool
Attributes []template.HTMLAttr
Required bool
Errors []error
}
func (field *CheckField) FillValue(r *http.Request) {
field.Checked = len(r.Form[field.Name]) > 0
}
func (field *CheckField) Scan(value interface{}) error {
if value == nil {
field.Checked = false
return nil
}
switch v := value.(type) {
case bool:
field.Checked = v
default:
field.Checked, _ = strconv.ParseBool(fmt.Sprintf("%v", v))
}
return nil
}
func (field *CheckField) Value() (driver.Value, error) {
return field.Checked, nil
}
type FileField struct { type FileField struct {
Name string Name string
Label string Label string
@ -360,7 +294,6 @@ type FileField struct {
OriginalFileName string OriginalFileName string
ContentType string ContentType string
Content []byte Content []byte
Required bool
Errors []error Errors []error
} }
@ -373,7 +306,7 @@ func (field *FileField) FillValue(r *http.Request) error {
return err return err
} }
defer file.Close() defer file.Close()
field.Content, err = io.ReadAll(file) field.Content, err = ioutil.ReadAll(file)
if err != nil { if err != nil {
return err return err
} }
@ -401,10 +334,6 @@ func (field *TagsField) Value() (driver.Value, error) {
return field.Tags, nil return field.Tags, nil
} }
func (field *TagsField) HasValue() bool {
return len(field.Tags) > 0 && field.Tags[0] != ""
}
func (field *TagsField) Scan(value interface{}) error { func (field *TagsField) Scan(value interface{}) error {
if value == nil { if value == nil {
return nil return nil
@ -455,10 +384,6 @@ func (field *ToggleField) FillValue(r *http.Request) {
} }
} }
func (field *ToggleField) String() string {
return field.Selected
}
type FormValidator struct { type FormValidator struct {
Valid bool Valid bool
} }
@ -479,29 +404,19 @@ func (v *FormValidator) CheckInputMinLength(field *InputField, min int, message
return v.checkInput(field, len(field.Val) >= min, message) return v.checkInput(field, len(field.Val) >= min, message)
} }
func (v *FormValidator) CheckInputLength(field *InputField, length int, message string) bool {
return v.checkInput(field, len(field.Val) == length, message)
}
func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool { func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool {
_, err := mail.ParseAddress(field.Val) _, err := mail.ParseAddress(field.Val)
return v.checkInput(field, err == nil, message) return v.checkInput(field, err == nil, message)
} }
func (v *FormValidator) CheckValidVATINInput(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool { func (v *FormValidator) CheckValidVATINInput(field *InputField, country string, message string) bool {
return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid($1 || $2, 'vatin')", country, field.Val), message) // TODO: actual VATIN validation
return v.checkInput(field, true, message)
} }
func (v *FormValidator) CheckValidPhoneInput(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool { func (v *FormValidator) CheckValidPhoneInput(field *InputField, country string, message string) bool {
return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid_phone($1, $2)", field.Val, country), message) // TODO: actual phone validation
} return v.checkInput(field, true, message)
func (v *FormValidator) CheckValidIBANInput(ctx context.Context, conn *Conn, field *InputField, message string) bool {
return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid($1, 'iban')", field.Val), message)
}
func (v *FormValidator) CheckValidBICInput(ctx context.Context, conn *Conn, field *InputField, message string) bool {
return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid($1, 'bic')", field.Val), message)
} }
func (v *FormValidator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool { func (v *FormValidator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool {
@ -512,10 +427,6 @@ func (v *FormValidator) CheckValidSelectOption(field *SelectField, message strin
return v.checkSelect(field, field.HasValidOptions(), message) return v.checkSelect(field, field.HasValidOptions(), message)
} }
func (v *FormValidator) CheckValidRadioOption(field *RadioField, message string) bool {
return v.checkRadio(field, field.HasValidOption(), message)
}
func (v *FormValidator) CheckAtMostOneOfEachGroup(field *SelectField, message string) bool { func (v *FormValidator) CheckAtMostOneOfEachGroup(field *SelectField, message string) bool {
repeated := false repeated := false
groups := map[string]bool{} groups := map[string]bool{}
@ -574,11 +485,3 @@ func (v *FormValidator) checkSelect(field *SelectField, ok bool, message string)
} }
return ok return ok
} }
func (v *FormValidator) checkRadio(field *RadioField, ok bool, message string) bool {
if !ok {
field.Errors = append(field.Errors, errors.New(message))
v.Valid = false
}
return ok
}

Some files were not shown because too many files have changed in this diff Show More