Compare commits
141 Commits
Author | SHA1 | Date |
---|---|---|
|
ebb073166a | |
|
69076903e8 | |
|
913612a179 | |
|
a0961f9da9 | |
|
0ee64f1905 | |
|
d4dc8e00e5 | |
|
12b3b4ad12 | |
|
f1e876d4fb | |
|
01807de5ca | |
|
f1534e6cd2 | |
|
cfd7a0c701 | |
|
4ae9fc5cfa | |
|
ce4d29b83b | |
|
193409eed8 | |
|
f5a9e819eb | |
|
82da7f4697 | |
|
2eecdcfc3e | |
|
16e80b5ae0 | |
|
36423c8636 | |
|
6ef551a846 | |
|
292720de28 | |
|
e8a44e480e | |
|
0b74c7a91c | |
|
790417e12c | |
|
b815a18967 | |
|
7b1220c9f6 | |
|
e3d1e1fd1d | |
|
93a95d77d0 | |
|
d4ef6c3254 | |
|
7f31b10cce | |
|
c6c550a036 | |
|
4f646e35d6 | |
|
eb880fed36 | |
|
268ab9989a | |
|
dda32db683 | |
|
a30e015639 | |
|
fa57c4b191 | |
|
dca8b3a719 | |
|
9ab08deaa1 | |
|
7f21a2131e | |
|
f95936c523 | |
|
e626c7b4bd | |
|
ac0143b2b0 | |
|
71a0a82a3f | |
|
c95f172499 | |
|
58cef8c00b | |
|
4deb698265 | |
|
778f9c1555 | |
|
ad5bc271b6 | |
|
f546632a89 | |
|
c3fa23727f | |
|
505fa0f154 | |
|
64be350677 | |
|
4363073682 | |
|
3e6f44f778 | |
|
faf7ee8ed5 | |
|
a689e2f734 | |
|
405c833490 | |
|
65413637ac | |
|
6fcc19bebf | |
|
5b0ca28b97 | |
|
662ba59be3 | |
|
24a4bf2583 | |
|
2ec88eddae | |
|
5f7b798eb4 | |
|
2bd7b2e952 | |
|
843379a908 | |
|
61fc8ee255 | |
|
b28f29eb24 | |
|
4d2af368d2 | |
|
b4b049aab9 | |
|
e0bdb89472 | |
|
f15294c042 | |
|
0937cfcf33 | |
|
18b38f593c | |
|
22ee6343e2 | |
|
7e377f550c | |
|
d3afde9e21 | |
|
a5fdeb9ab4 | |
|
45a45d7cc9 | |
|
b62f86950e | |
|
2d0572e1d6 | |
|
6de4135fa6 | |
|
f3fdc0d743 | |
|
e34ef4f458 | |
|
31a655ae7f | |
|
c3e1597972 | |
|
e322ddd168 | |
|
998159d1d7 | |
|
4e831d94db | |
|
ef215f1e6e | |
|
2501b7d226 | |
|
0fd0cf5a38 | |
|
80a6a802a2 | |
|
831becf6fd | |
|
60ec335769 | |
|
52256c3cb9 | |
|
1c6375b51d | |
|
0c4ef97dff | |
|
835e52dbcb | |
|
5e8bed8452 | |
|
51c789ca13 | |
|
ae1e294144 | |
|
a7c1df20f0 | |
|
7d55e949fc | |
|
bb7af20a17 | |
|
66ab3b4bf7 | |
|
b48a974086 | |
|
b7578a56df | |
|
fa97f53dd7 | |
|
1164210d84 | |
|
c174fb447c | |
|
1bb6870f26 | |
|
58dd69773a | |
|
d697b340e9 | |
|
596120d84a | |
|
ef8f40e734 | |
|
2320cae3f4 | |
|
183b8d3ed9 | |
|
a068784a22 | |
|
f917ce84dd | |
|
eb845edf0a | |
|
2299ec9f8c | |
|
20827b2cfb | |
|
1c0f126c58 | |
|
30cd15ee89 | |
|
f40e4fdb2e | |
|
ee0b5d0bdc | |
|
de2a2f5912 | |
|
07c1071975 | |
|
8a4f80783d | |
|
1ad771b771 | |
|
055e92fb23 | |
|
826741a381 | |
|
3af40cc7bc | |
|
010e174de7 | |
|
73682462da | |
|
6732d654a4 | |
|
eb207a01fc | |
|
3c14447ef9 | |
|
d79ddc6731 |
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -19,9 +20,12 @@ 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),
|
Handler: numerus.NewRouter(db, demo),
|
||||||
ReadTimeout: 5 * time.Second,
|
ReadTimeout: 5 * time.Second,
|
||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 10 * time.Second,
|
||||||
IdleTimeout: 2 * time.Minute,
|
IdleTimeout: 2 * time.Minute,
|
||||||
|
@ -29,7 +33,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(); err != http.ErrServerClosed {
|
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Fatalf("http server: %v", err)
|
log.Fatalf("http server: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -10,14 +10,17 @@ 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-13-pg-libphonenumber,
|
postgresql-15-pg-libphonenumber,
|
||||||
postgresql-13-pgtap,
|
postgresql-15-pgtap,
|
||||||
postgresql-13-pguri,
|
postgresql-15-pguri,
|
||||||
postgresql-13-vat
|
postgresql-15-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
|
||||||
|
@ -52,9 +55,10 @@ Package: numerus-sqitch
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Depends:
|
Depends:
|
||||||
${misc:Depends},
|
${misc:Depends},
|
||||||
postgresql-13-pg-libphonenumber,
|
postgresql-15-pg-libphonenumber,
|
||||||
postgresql-13-pguri,
|
postgresql-15-pguri,
|
||||||
postgresql-13-vat,
|
postgresql-15-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
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/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
|
|
@ -0,0 +1,15 @@
|
||||||
|
[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
|
|
@ -1,10 +1,14 @@
|
||||||
#!/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 13 make test-deploy
|
pg_virtualenv -v 15 make test-deploy
|
||||||
|
|
209
demo/demo.sql
209
demo/demo.sql
|
@ -2,7 +2,9 @@ begin;
|
||||||
|
|
||||||
set search_path to auth, numerus, public;
|
set search_path to auth, numerus, public;
|
||||||
|
|
||||||
alter sequence user_user_id_seq restart;
|
create or replace function public.database_is_numerus_demo() returns bool as $$ select true $$ language sql;
|
||||||
|
|
||||||
|
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')
|
||||||
|
@ -10,65 +12,188 @@ 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;
|
alter sequence company_company_id_seq restart with 123;
|
||||||
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', 'Pesebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de l’Hort', 'Castelló d’Empúries', 'Girona', '17486', 'ES', 'EUR', 2, 'Juli Verd és responsable del tractament de les seves dades d’acord 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 d’accés, rectificació, portabilitat, supressió, limitació i oposició a Juli Verd, amb domicili Carrer de l’Hort 71, 17486 Castelló d’Empú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', 'Pessebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de l’Hort', 'Castelló d’Empúries', 'Girona', '17486', 'ES', 'EUR', 124, 'Juli Verd és responsable del tractament de les seves dades d’acord 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 d’accés, rectificació, portabilitat, supressió, limitació i oposició a Juli Verd, amb domicili Carrer de l’Hort 71, 17486 Castelló d’Empú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 l’Hort', 'Castelló d’Empúries', 'Girona', '17486', 'ES', 'EUR', 126, 'Pere Gil és responsable del tractament de les seves dades d’acord 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 d’accés, rectificació, portabilitat, supressió, limitació i oposició a Pere Gil, amb domicili Carrer de l’Hort 71, 17486 Castelló d’Empú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;
|
alter sequence payment_method_payment_method_id_seq restart with 123;
|
||||||
insert into payment_method (company_id, name, instructions)
|
insert into payment_method (company_id, name, instructions)
|
||||||
values (1, 'Efectiu', 'Pagament en efectiu al comptat.')
|
values (123, 'Efectiu', 'Pagament en efectiu al comptat.')
|
||||||
, (1, 'Transferència', E'Pagament per transferència bancària al compte:\n\nES0123456789012345678901\n\nBIC AAAABBCCDD')
|
, (123, '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 (1, 1)
|
values (123, 123)
|
||||||
, (1, 2)
|
, (123, 124)
|
||||||
|
, (124, 123)
|
||||||
|
, (124, 124)
|
||||||
;
|
;
|
||||||
|
|
||||||
alter sequence tax_class_tax_class_id_seq restart;
|
alter sequence tax_class_tax_class_id_seq restart with 123;
|
||||||
insert into tax_class (company_id, name)
|
insert into tax_class (company_id, name)
|
||||||
values (1, 'IRPF')
|
values (123, 'IRPF')
|
||||||
, (1, 'IVA')
|
, (123, 'IVA')
|
||||||
|
, (124, 'IRPF')
|
||||||
|
, (124, 'IVA')
|
||||||
;
|
;
|
||||||
|
|
||||||
alter sequence tax_tax_id_seq restart;
|
alter sequence tax_tax_id_seq restart with 123;
|
||||||
insert into tax (company_id, tax_class_id, name, rate)
|
insert into tax (company_id, tax_class_id, name, rate)
|
||||||
values (1, 1, 'Retenció 15 %', -0.15)
|
values (123, 123, 'Retenció 15 %', -0.15)
|
||||||
, (1, 2, 'IVA 21 %', 0.21)
|
, (123, 124, 'IVA 21 %', 0.21)
|
||||||
, (1, 2, 'IVA 10 %', 0.10)
|
, (123, 124, 'IVA 10 %', 0.10)
|
||||||
, (1, 2, 'IVA 4 %', 0.04)
|
, (123, 124, '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;
|
alter sequence contact_contact_id_seq restart with 123;
|
||||||
select add_contact (1, 'Melcior', '1', 'Rei Blanc', '0732621', 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR', array['pesebre', 'mag']);
|
-- customers
|
||||||
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, 'Melcior', '0732621', 'melcio@reismags.cat', '', '(Rei Blanc,1,"C/ Principal, 1",Shiraz,Fars,1,IR)', '', '', 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, 'Gaspar', '111', 'gaspar@reismags.cat', '', '(Rei Ros,2,"C/ Principal, 2",Nova Delhi,Delhi,2,IN)', '', '', array['pesebre', 'mag', 'client']);
|
||||||
select add_contact (1, 'Caganera', '41414141L', '', '222 222 222', 'caganera@pesebre.cat', '', 'C/ De l’Hort, 4', 'Olot', 'Girona', '17800', 'ES', array['pesebre', 'persona']);
|
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, '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, 'Caganera', '222 222 222', 'caganera@pesebre.cat', '', '(Caganera,41414141L,"C/ De l’Hort, 4",Olot,Girona,17800,ES)', '', '', array['pesebre', 'persona', '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, '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 (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;
|
alter sequence product_product_id_seq restart with 123;
|
||||||
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 l’aigua règia.', '55.92', array[2], array['metall']);
|
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 l’aigua règia.', '55.92', array[124], array['metall']);
|
||||||
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, 'Encens', 'Goma resina fragrant que desprèn una olor característica quan es crema.', '2.15', 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, 'Mirra', 'Goma resinosa aromàtica de color gris groguenc i gust amargant.', '6.90', array[124], array['resina']);
|
||||||
select add_product(1, 'Paper higiènic (pack de 32 U)', 'Paper que s’usa per mantenir la higiene personal després de defecar o orinar.', '7.99', array[4], array['necessitat']);
|
select add_product(123, 'Paper higiènic (pack de 32 U)', 'Paper que s’usa per mantenir la higiene personal després de defecar o orinar.', '7.99', array[126], array['necessitat']);
|
||||||
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, '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, '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, '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, 'Teia', 'Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.', '7.00', array[2], array['obsolet']);
|
select add_product(123, 'Teia', 'Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.', '7.00', array[124], array['obsolet']);
|
||||||
|
|
||||||
alter sequence invoice_invoice_id_seq restart;
|
alter table payment_account alter column payment_account_id restart with 123;
|
||||||
alter sequence invoice_product_invoice_product_id_seq restart;
|
select add_payment_account_bank(123, 'Guardiola', 'ES2820958297603648596978');
|
||||||
select add_invoice(1, (current_date - '28 days'::interval)::date, 6, 'Vol esmorzar!', 1, '{producte}','{"(1,Teia,\"Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, 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_payment_account_cash(123, 'Matalàs');
|
||||||
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 s’usa 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 l’aigua 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})"}');
|
|
||||||
|
|
||||||
update invoice set invoice_status = 'paid' where invoice_id in (1, 5);
|
alter sequence invoice_invoice_id_seq restart with 123;
|
||||||
update invoice set invoice_status = 'unpaid' where invoice_id = 3;
|
alter sequence invoice_product_invoice_product_id_seq restart with 123;
|
||||||
update invoice set invoice_status = 'sent' where invoice_id = 4;
|
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})"}');
|
||||||
|
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 l’aigua 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 d’altres arbres, provinent sobretot del cor de l’arbre, 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 l’aigua 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 d’altres arbres, provinent sobretot del cor de l’arbre, 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 s’usa 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 s’usa 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 d’altres arbres, provinent sobretot del cor de l’arbre, 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 l’aigua 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 s’usa 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 s’usa 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 l’aigua 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 d’altres arbres, provinent sobretot del cor de l’arbre, 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 s’usa 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 d’altres arbres, provinent sobretot del cor de l’arbre, 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 l’aigua 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 d’altres arbres, provinent sobretot del cor de l’arbre, 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 s’usa 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 l’aigua 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 d’ABC123', '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 d’123ABC', '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 d’N 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 d’123XYZ', '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 d’N 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 d’XYZ123', '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 d’N 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 d’N 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 d’N 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 d’N 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 d’N ', '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 d’N 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 d’N 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 d’N 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 d’N 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 d’N 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 d’N 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;
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,68 @@
|
||||||
|
-- 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
|
||||||
|
-- collection’s 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;
|
|
@ -0,0 +1,68 @@
|
||||||
|
-- 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
|
||||||
|
-- collection’s 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;
|
|
@ -7,21 +7,65 @@
|
||||||
-- 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, 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
|
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
|
||||||
$$
|
$$
|
||||||
declare
|
declare
|
||||||
cid integer;
|
cid integer;
|
||||||
cslug uuid;
|
cslug uuid;
|
||||||
begin
|
begin
|
||||||
insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, tags)
|
insert into contact (company_id, name, 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)
|
values (add_contact.company_id, add_contact.name, 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
|
||||||
|
@ -29,8 +73,10 @@ $$
|
||||||
language plpgsql
|
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;
|
revoke execute on function add_contact(integer, text, text, text, text, tax_details, text, text, 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, 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 admin;
|
grant execute on function add_contact(integer, text, text, text, text, tax_details, text, text, 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;
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- 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;
|
|
@ -8,11 +8,15 @@
|
||||||
-- 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
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,55 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,67 @@
|
||||||
|
-- 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
|
||||||
|
-- payment’s 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;
|
|
@ -0,0 +1,67 @@
|
||||||
|
-- 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
|
||||||
|
-- payment’s 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;
|
|
@ -0,0 +1,35 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,34 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,23 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,23 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,20 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,30 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,30 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,30 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,27 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,22 @@
|
||||||
|
-- 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;
|
|
@ -10,19 +10,28 @@ 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;
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,28 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,22 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,45 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,32 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,68 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,39 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,31 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,38 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,31 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,60 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,39 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,53 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,53 @@
|
||||||
|
-- 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;
|
|
@ -7,29 +7,25 @@
|
||||||
-- 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, 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
|
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
|
||||||
$$
|
$$
|
||||||
declare
|
declare
|
||||||
cid integer;
|
cid integer;
|
||||||
company integer;
|
company integer;
|
||||||
begin
|
begin
|
||||||
update contact
|
update contact
|
||||||
set business_name = edit_contact.business_name
|
set name = edit_contact.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
|
||||||
|
@ -40,14 +36,96 @@ 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, email, uri, text, text, text, text, country_code, tag_name[]) from public;
|
revoke execute on function edit_contact(uuid, text, text, text, text, tax_details, text, text, 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, 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 admin;
|
grant execute on function edit_contact(uuid, text, text, text, text, tax_details, text, text, 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;
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
-- 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;
|
|
@ -5,11 +5,15 @@
|
||||||
-- 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
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,53 @@
|
||||||
|
-- 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;
|
|
@ -14,7 +14,9 @@ begin;
|
||||||
|
|
||||||
set search_path to numerus, public;
|
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
|
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, 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;
|
||||||
|
@ -27,7 +29,6 @@ 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
|
||||||
|
@ -103,9 +104,9 @@ end;
|
||||||
$$
|
$$
|
||||||
language plpgsql;
|
language plpgsql;
|
||||||
|
|
||||||
revoke execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) from public;
|
revoke execute on function edit_invoice(uuid, 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, 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;
|
grant execute on function edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]) to admin;
|
||||||
|
|
||||||
|
|
||||||
commit;
|
commit;
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,53 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,53 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,43 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,43 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,26 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,26 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,32 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Deploy numerus:extension_iban to pg
|
||||||
|
-- requires: schema_numerus
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
create extension if not exists iban;
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,165 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,23 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,24 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,32 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,32 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,32 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- 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;
|
|
@ -24,6 +24,9 @@ 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;
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,47 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,38 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,33 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,33 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,33 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,40 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,40 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,34 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,40 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,20 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,53 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,51 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,51 @@
|
||||||
|
-- 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;
|
|
@ -0,0 +1,49 @@
|
||||||
|
-- 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
19
go.mod
|
@ -1,15 +1,24 @@
|
||||||
module dev.tandem.ws/tandem/numerus
|
module dev.tandem.ws/tandem/numerus
|
||||||
|
|
||||||
go 1.16
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jackc/pgx/v4 v4.17.2
|
github.com/jackc/pgtype v1.10.0
|
||||||
|
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.1
|
github.com/leonelquinteros/gotext v1.5.0
|
||||||
golang.org/x/text v0.3.8
|
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8
|
||||||
|
github.com/tealeg/xlsx v0.0.0-20181024002044-dbf71b6a931e
|
||||||
|
golang.org/x/text v0.7.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jackc/pgtype v1.12.0
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
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
45
go.sum
|
@ -14,7 +14,6 @@ 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=
|
||||||
|
@ -25,6 +24,7 @@ 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,7 +35,6 @@ 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=
|
||||||
|
@ -43,6 +42,7 @@ 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,17 +51,18 @@ 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.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
|
github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
|
||||||
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
github.com/jackc/pgtype v1.10.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.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
|
github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
|
||||||
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
|
github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
|
||||||
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=
|
||||||
|
@ -69,12 +70,14 @@ 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.1 h1:vmddRn3gHp67YFjZLZE2AZsgYMT4IBTJhua4yfe7/4Q=
|
github.com/leonelquinteros/gotext v1.5.0 h1:ODY7LzLpZWWSJdAHnzhreOr6cwLXTAmc914FOauSkBM=
|
||||||
github.com/leonelquinteros/gotext v1.5.1/go.mod h1:/A4Y7BvIsf5JHO60E43ZQDVkV3qO+7eP8HjeqD6ChIA=
|
github.com/leonelquinteros/gotext v1.5.0/go.mod h1:OCiUVHuhP9LGFBQ1oAmdtNCHJCiHiQA8lf4nAifHkr0=
|
||||||
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=
|
||||||
|
@ -89,6 +92,8 @@ 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=
|
||||||
|
@ -111,8 +116,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/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/tealeg/xlsx v0.0.0-20181024002044-dbf71b6a931e h1:0AoAjM/7iqEZwTsWhk3nm9+H5mocFnh6dCGUaIOSTDQ=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/tealeg/xlsx v0.0.0-20181024002044-dbf71b6a931e/go.mod h1:uxu5UY2ovkuRPWKQ8Q7JG0JbSivrISjdPzZQKeo74mA=
|
||||||
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=
|
||||||
|
@ -134,26 +139,19 @@ 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=
|
||||||
|
@ -167,20 +165,16 @@ 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.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
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=
|
||||||
|
@ -188,16 +182,15 @@ 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.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
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=
|
||||||
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
(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 Google’s 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+))
|
|
@ -0,0 +1,338 @@
|
||||||
|
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", "Card’s 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 card’s 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"))
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
const Version = "1.4~git"
|
869
pkg/company.go
869
pkg/company.go
File diff suppressed because it is too large
Load Diff
330
pkg/contacts.go
330
pkg/contacts.go
|
@ -4,6 +4,7 @@ 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"
|
||||||
|
@ -41,13 +42,21 @@ 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)
|
||||||
form := newContactForm(r.Context(), conn, locale)
|
|
||||||
slug := params[0].Value
|
slug := params[0].Value
|
||||||
|
if slug == "import" {
|
||||||
|
ServeImportPage(w, r, params)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form := newContactForm(r.Context(), conn, locale)
|
||||||
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
|
||||||
|
@ -69,7 +78,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.BusinessName.Val,
|
ContactName: form.Name.String(),
|
||||||
Form: form,
|
Form: form,
|
||||||
}
|
}
|
||||||
mustRenderMainTemplate(w, r, "contacts/edit.gohtml", page)
|
mustRenderMainTemplate(w, r, "contacts/edit.gohtml", page)
|
||||||
|
@ -88,14 +97,12 @@ 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, $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)
|
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)
|
||||||
htmxRedirect(w, r, companyURI(company, "/contacts"))
|
htmxRedirect(w, r, companyURI(company, "/contacts"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +122,12 @@ 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 := 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)
|
slug := params[0].Value
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
@ -167,6 +179,11 @@ 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"}
|
||||||
|
@ -177,7 +194,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.business_name ilike $%d", "%"+name+"%")
|
appendWhere("contact.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" {
|
||||||
|
@ -189,13 +206,15 @@ 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
|
||||||
, business_name
|
, name
|
||||||
, email
|
, coalesce(email::text, '')
|
||||||
, phone
|
, coalesce(phone::text, '')
|
||||||
, 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 business_name
|
order by name
|
||||||
`, strings.Join(where, ") AND (")), args...)
|
`, strings.Join(where, ") AND (")), args...)
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
|
@ -215,24 +234,61 @@ func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company
|
||||||
}
|
}
|
||||||
|
|
||||||
type contactForm struct {
|
type contactForm struct {
|
||||||
locale *Locale
|
locale *Locale
|
||||||
BusinessName *InputField
|
Name *InputField
|
||||||
VATIN *InputField
|
HasTaxDetails *CheckField
|
||||||
TradeName *InputField
|
BusinessName *InputField
|
||||||
Phone *InputField
|
VATIN *InputField
|
||||||
Email *InputField
|
Phone *InputField
|
||||||
Web *InputField
|
Email *InputField
|
||||||
Address *InputField
|
Web *InputField
|
||||||
City *InputField
|
Address *InputField
|
||||||
Province *InputField
|
City *InputField
|
||||||
PostalCode *InputField
|
Province *InputField
|
||||||
Country *SelectField
|
PostalCode *InputField
|
||||||
Tags *TagsField
|
Country *SelectField
|
||||||
|
IBAN *InputField
|
||||||
|
BIC *InputField
|
||||||
|
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),
|
||||||
|
@ -249,37 +305,6 @@ 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),
|
||||||
|
@ -320,6 +345,16 @@ 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),
|
||||||
|
@ -331,9 +366,10 @@ 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)
|
||||||
|
@ -342,6 +378,8 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -349,54 +387,79 @@ 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 := ""
|
country := "ES"
|
||||||
if validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale)) {
|
if form.HasTaxDetails.Checked {
|
||||||
country = form.Country.Selected[0]
|
if validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale)) {
|
||||||
|
country = form.Country.Selected[0]
|
||||||
|
}
|
||||||
|
if 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))
|
||||||
|
}
|
||||||
|
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.CheckRequiredInput(form.Address, gettext("Address can not be empty.", 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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validator.CheckRequiredInput(form.BusinessName, gettext("Business name can not be empty.", form.locale))
|
if validator.CheckRequiredInput(form.Name, gettext("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.Name, 2, gettext("Name must have at least two letters.", form.locale))
|
||||||
if validator.CheckRequiredInput(form.VATIN, gettext("VAT number can not be empty.", form.locale)) {
|
|
||||||
validator.CheckValidVATINInput(form.VATIN, country, gettext("This value is not a valid VAT number.", form.locale))
|
|
||||||
}
|
}
|
||||||
if validator.CheckRequiredInput(form.Phone, gettext("Phone can not be empty.", form.locale)) {
|
|
||||||
validator.CheckValidPhoneInput(form.Phone, country, gettext("This value is not a valid phone number.", 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 validator.CheckRequiredInput(form.Email, gettext("Email can not be empty.", 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))
|
||||||
}
|
}
|
||||||
validator.CheckRequiredInput(form.Address, gettext("Address can not be empty.", form.locale))
|
if form.IBAN.Val != "" {
|
||||||
validator.CheckRequiredInput(form.City, gettext("City 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.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 business_name
|
select name
|
||||||
|
, vatin is not null
|
||||||
|
, business_name
|
||||||
, substr(vatin::text, 3)
|
, substr(vatin::text, 3)
|
||||||
, trade_name
|
|
||||||
, phone
|
, phone
|
||||||
, email
|
, email
|
||||||
, web
|
, uri
|
||||||
, 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,
|
||||||
|
@ -405,28 +468,72 @@ 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 ServeEditContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func (form *contactForm) TaxDetails() *CustomerTaxDetails {
|
||||||
conn := getConn(r)
|
if !form.HasTaxDetails.Checked {
|
||||||
locale := getLocale(r)
|
return nil
|
||||||
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)
|
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) {
|
||||||
|
serveTagsEditForm(w, r, params, "/contacts/", "select tags from contact where slug = $1")
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
conn := getConn(r)
|
company := mustGetCompany(r)
|
||||||
company := getCompany(r)
|
form := newContactImportForm(locale, company)
|
||||||
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
|
||||||
|
@ -435,8 +542,45 @@ func HandleUpdateContactTags(w http.ResponseWriter, r *http.Request, params http
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if conn.MustGetText(r.Context(), "", "update contact set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
|
workbook, err := xlsx.OpenBinary(form.File.Content)
|
||||||
http.NotFound(w, r)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
|
conn := getConn(r)
|
||||||
|
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"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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), decimal_digits) as expenses
|
, to_price(coalesce(expense.total, 0) + coalesce(expense_tax.vat, 0) + coalesce(expense_tax.irpf, 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,9 +201,21 @@ 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(amount)::integer as total
|
, sum(subtotal + taxes)::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 expense on expense.invoice_date = date.invoice_date and company_id = 1
|
left join (
|
||||||
|
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
|
||||||
|
|
12
pkg/db.go
12
pkg/db.go
|
@ -143,6 +143,14 @@ 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 {
|
||||||
|
@ -159,8 +167,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, rows [][]interface{}) int64 {
|
func (tx *Tx) MustCopyFrom(ctx context.Context, tableName string, columns []string, length int, next func(int) ([]interface{}, error)) int64 {
|
||||||
copied, err := tx.CopyFrom(ctx, pgx.Identifier{tableName}, columns, pgx.CopyFromRows(rows))
|
copied, err := tx.CopyFrom(ctx, pgx.Identifier{tableName}, columns, pgx.CopyFromSlice(length, next))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
528
pkg/expenses.go
528
pkg/expenses.go
|
@ -13,18 +13,28 @@ 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
|
||||||
Filters *expenseFilterForm
|
SumAmount string
|
||||||
|
SumTaxes map[string]string
|
||||||
|
SumTotal string
|
||||||
|
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) {
|
||||||
|
@ -37,65 +47,66 @@ func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page := &expensesIndexPage{
|
page := &expensesIndexPage{
|
||||||
Expenses: mustCollectExpenseEntries(r.Context(), conn, company, filters),
|
Expenses: mustCollectExpenseEntries(r.Context(), conn, locale, filters),
|
||||||
Filters: filters,
|
ExpenseStatuses: mustCollectExpenseStatuses(r.Context(), conn, locale),
|
||||||
|
TaxClasses: mustCollectTaxClasses(r.Context(), conn, company),
|
||||||
|
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, company *Company, filters *expenseFilterForm) []*ExpenseEntry {
|
func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale, filters *expenseFilterForm) []*ExpenseEntry {
|
||||||
args := []interface{}{company.Id}
|
where, args := filters.BuildQuery([]interface{}{locale.Language.String()})
|
||||||
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.slug
|
select expense_id
|
||||||
|
, expense.slug
|
||||||
, invoice_date
|
, invoice_date
|
||||||
, invoice_number
|
, invoice_number
|
||||||
, to_price(amount, decimal_digits)
|
, to_price(expense.amount, decimal_digits) as amount
|
||||||
, contact.business_name
|
, array_agg(array[tax_class.name, to_price(coalesce(expense_tax.amount, 0), decimal_digits)]) filter (where tax_class.name is not null)
|
||||||
|
, 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)
|
||||||
order by invoice_date
|
group by expense_id
|
||||||
`, strings.Join(where, ") AND (")), args...)
|
, expense.slug
|
||||||
|
, 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{
|
||||||
if err := rows.Scan(&entry.Slug, &entry.InvoiceDate, &entry.InvoiceNumber, &entry.Amount, &entry.InvoicerName, &entry.OriginalFileName, &entry.Tags); err != nil {
|
Taxes: make(map[string]string),
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
@ -104,6 +115,99 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, company *Company
|
||||||
|
|
||||||
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)
|
||||||
|
@ -116,6 +220,10 @@ 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
|
||||||
|
@ -127,20 +235,40 @@ 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)
|
||||||
mustRenderMainTemplate(w, r, "expenses/new.gohtml", form)
|
page := newNewExpensePage(form, r)
|
||||||
|
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{
|
||||||
Slug: slug,
|
newNewExpensePage(form, r),
|
||||||
Form: form,
|
slug,
|
||||||
}
|
}
|
||||||
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 {
|
||||||
|
@ -156,6 +284,7 @@ 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,
|
||||||
|
@ -181,6 +310,9 @@ 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",
|
||||||
|
@ -188,7 +320,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{
|
||||||
`min="0"`,
|
triggerRecompute,
|
||||||
template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
|
template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -204,6 +336,16 @@ 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
|
||||||
|
@ -227,20 +369,18 @@ 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, form.company.MinCents(), math.MaxFloat64, gettext("Amount must be a number greater than zero.", form.locale))
|
validator.CheckValidDecimal(form.Amount, -math.MaxFloat64, math.MaxFloat64, gettext("Amount must be a decimal number.", 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 {
|
||||||
return !notFoundErrorOrPanic(conn.QueryRow(ctx, `
|
if 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)
|
, array_agg(tax_id) filter ( where tax_id is not null )
|
||||||
, tags
|
, tags
|
||||||
from expense
|
from expense
|
||||||
left join expense_tax using (expense_id)
|
left join expense_tax using (expense_id)
|
||||||
|
@ -258,36 +398,14 @@ 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)
|
||||||
|
@ -302,10 +420,12 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -323,10 +443,11 @@ func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprout
|
||||||
type expenseFilterForm struct {
|
type expenseFilterForm struct {
|
||||||
locale *Locale
|
locale *Locale
|
||||||
company *Company
|
company *Company
|
||||||
Customer *SelectField
|
Contact *SelectField
|
||||||
InvoiceNumber *InputField
|
InvoiceNumber *InputField
|
||||||
FromDate *InputField
|
FromDate *InputField
|
||||||
ToDate *InputField
|
ToDate *InputField
|
||||||
|
ExpenseStatus *SelectField
|
||||||
Tags *TagsField
|
Tags *TagsField
|
||||||
TagsCondition *ToggleField
|
TagsCondition *ToggleField
|
||||||
}
|
}
|
||||||
|
@ -335,10 +456,10 @@ func newExpenseFilterForm(ctx context.Context, conn *Conn, locale *Locale, compa
|
||||||
return &expenseFilterForm{
|
return &expenseFilterForm{
|
||||||
locale: locale,
|
locale: locale,
|
||||||
company: company,
|
company: company,
|
||||||
Customer: &SelectField{
|
Contact: &SelectField{
|
||||||
Name: "customer",
|
Name: "contact",
|
||||||
Label: pgettext("input", "Customer", locale),
|
Label: pgettext("input", "Contact", locale),
|
||||||
EmptyLabel: gettext("All customers", locale),
|
EmptyLabel: gettext("All contacts", locale),
|
||||||
Options: mustGetContactOptions(ctx, conn, company),
|
Options: mustGetContactOptions(ctx, conn, company),
|
||||||
},
|
},
|
||||||
InvoiceNumber: &InputField{
|
InvoiceNumber: &InputField{
|
||||||
|
@ -360,6 +481,12 @@ 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),
|
||||||
|
@ -382,34 +509,107 @@ 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.Customer.FillValue(r)
|
form.Contact.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 ServeEditExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func (form *expenseFilterForm) HasValue() bool {
|
||||||
conn := getConn(r)
|
return form.Contact.HasValue() ||
|
||||||
locale := getLocale(r)
|
form.InvoiceNumber.HasValue() ||
|
||||||
company := getCompany(r)
|
form.FromDate.HasValue() ||
|
||||||
slug := params[0].Value
|
form.ToDate.HasValue() ||
|
||||||
form := newTagsForm(companyURI(company, "/expenses/"+slug+"/tags"), slug, locale)
|
form.ExpenseStatus.HasValue() ||
|
||||||
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from expense where slug = $1`, form.Slug).Scan(form.Tags)) {
|
form.Tags.HasValue()
|
||||||
http.NotFound(w, r)
|
}
|
||||||
return
|
|
||||||
|
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)))
|
||||||
}
|
}
|
||||||
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
|
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) {
|
||||||
|
serveTagsEditForm(w, r, params, "/expenses/", "select tags from expense where slug = $1")
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleUpdateExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
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
|
||||||
|
switch slug {
|
||||||
|
case "batch":
|
||||||
|
HandleBatchExpenseAction(w, r, params)
|
||||||
|
default:
|
||||||
|
if !ValidUuid(slug) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actionUri := fmt.Sprintf("/invoices/%s/edit", slug)
|
||||||
|
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) {
|
||||||
|
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 := getCompany(r)
|
company := mustGetCompany(r)
|
||||||
slug := params[0].Value
|
form := newExpenseForm(r.Context(), conn, locale, company)
|
||||||
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
|
||||||
|
@ -418,29 +618,117 @@ func HandleUpdateExpenseTags(w http.ResponseWriter, r *http.Request, params http
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if conn.MustGetText(r.Context(), "", "update expense set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
|
actionField := r.Form.Get("action")
|
||||||
http.NotFound(w, r)
|
switch actionField {
|
||||||
|
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 ServeExpenseAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func HandleBatchExpenseAction(w http.ResponseWriter, r *http.Request, _ 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
|
||||||
conn := getConn(r)
|
if !ValidUuid(slug) {
|
||||||
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)
|
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(content)), 10))
|
if err := verifyCsrfTokenValid(r); err != nil {
|
||||||
w.WriteHeader(http.StatusOK)
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
w.Write(content)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := getConn(r)
|
||||||
|
conn.MustExec(r.Context(), "select remove_expense($1)", slug)
|
||||||
|
|
||||||
|
company := mustGetCompany(r)
|
||||||
|
htmxRedirect(w, r, companyURI(company, "/expenses"))
|
||||||
}
|
}
|
||||||
|
|
113
pkg/form.go
113
pkg/form.go
|
@ -8,7 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jackc/pgtype"
|
"github.com/jackc/pgtype"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -59,6 +59,10 @@ 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))
|
||||||
}
|
}
|
||||||
|
@ -184,6 +188,10 @@ 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 {
|
||||||
|
@ -287,6 +295,64 @@ 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
|
||||||
|
@ -294,6 +360,7 @@ type FileField struct {
|
||||||
OriginalFileName string
|
OriginalFileName string
|
||||||
ContentType string
|
ContentType string
|
||||||
Content []byte
|
Content []byte
|
||||||
|
Required bool
|
||||||
Errors []error
|
Errors []error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,7 +373,7 @@ func (field *FileField) FillValue(r *http.Request) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
field.Content, err = ioutil.ReadAll(file)
|
field.Content, err = io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -334,6 +401,10 @@ 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
|
||||||
|
@ -384,6 +455,10 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -404,19 +479,29 @@ 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(field *InputField, country string, message string) bool {
|
func (v *FormValidator) CheckValidVATINInput(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool {
|
||||||
// TODO: actual VATIN validation
|
return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid($1 || $2, 'vatin')", country, field.Val), message)
|
||||||
return v.checkInput(field, true, message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FormValidator) CheckValidPhoneInput(field *InputField, country string, message string) bool {
|
func (v *FormValidator) CheckValidPhoneInput(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool {
|
||||||
// TODO: actual phone validation
|
return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid_phone($1, $2)", field.Val, country), message)
|
||||||
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 {
|
||||||
|
@ -427,6 +512,10 @@ 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{}
|
||||||
|
@ -485,3 +574,11 @@ 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
Loading…
Reference in New Issue