Compare commits
No commits in common. "master" and "v0" have entirely different histories.
|
@ -2,7 +2,6 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -20,12 +19,9 @@ func main() {
|
|||
}
|
||||
defer db.Close()
|
||||
|
||||
var demo bool
|
||||
_ = db.QueryRow(context.Background(), "select database_is_numerus_demo()").Scan(&demo)
|
||||
|
||||
srv := http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: numerus.NewRouter(db, demo),
|
||||
Handler: numerus.NewRouter(db),
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 2 * time.Minute,
|
||||
|
@ -33,7 +29,7 @@ func main() {
|
|||
|
||||
go func() {
|
||||
log.Printf("INFO - listening on %s\n", srv.Addr)
|
||||
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatalf("http server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
|
|
@ -10,17 +10,14 @@ Build-Depends:
|
|||
golang-github-jackc-pgx-v4-dev,
|
||||
golang-github-julienschmidt-httprouter-dev,
|
||||
golang-github-leonelquinteros-gotext-dev,
|
||||
golang-github-rainycape-unidecode-dev,
|
||||
golang-github-tealeg-xlsx-dev,
|
||||
golang-golang-x-text-dev,
|
||||
postgresql-all (>= 217~),
|
||||
sqitch,
|
||||
pgtap,
|
||||
postgresql-15-pg-libphonenumber,
|
||||
postgresql-15-pgtap,
|
||||
postgresql-15-pguri,
|
||||
postgresql-15-vat,
|
||||
postgresql-15-iban
|
||||
postgresql-13-pg-libphonenumber,
|
||||
postgresql-13-pgtap,
|
||||
postgresql-13-pguri,
|
||||
postgresql-13-vat
|
||||
Standards-Version: 4.6.0
|
||||
XS-Go-Import-Path: dev.tandem.ws/tandem/numerus
|
||||
Vcs-Browser: https://dev.tandem.ws/tandem/numerus
|
||||
|
@ -55,10 +52,9 @@ Package: numerus-sqitch
|
|||
Architecture: all
|
||||
Depends:
|
||||
${misc:Depends},
|
||||
postgresql-15-pg-libphonenumber,
|
||||
postgresql-15-pguri,
|
||||
postgresql-15-vat,
|
||||
postgresql-15-iban,
|
||||
postgresql-13-pg-libphonenumber,
|
||||
postgresql-13-pguri,
|
||||
postgresql-13-vat,
|
||||
sqitch
|
||||
Description: Simple invoicing and accounting web application
|
||||
A simple web application to keep invoice and accouting records, intended for
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
# Create numerus user and group
|
||||
if ! getent group numerus >/dev/null; then
|
||||
addgroup --system --quiet numerus
|
||||
fi
|
||||
if ! getent passwd numerus >/dev/null; then
|
||||
adduser --quiet \
|
||||
--system \
|
||||
--disabled-login \
|
||||
--no-create-home \
|
||||
--shell /bin/bash \
|
||||
--ingroup numerus \
|
||||
--home /usr/share/numerus \
|
||||
--gecos "Numerus Daemon" \
|
||||
numerus
|
||||
fi
|
||||
|
||||
# Make sure log directory has correct permissions set
|
||||
dpkg-statoverride --list "/var/log/numerus" >/dev/null || \
|
||||
dpkg-statoverride --add --force --quiet --update numerus adm 0750 /var/log/numerus
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "postinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
|
@ -1,15 +0,0 @@
|
|||
[Unit]
|
||||
Description=Numerus application server
|
||||
Requires=postgresql.service
|
||||
After=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=numerus
|
||||
Group=numerus
|
||||
WorkingDirectory=/usr/share/numerus
|
||||
EnvironmentFile=-/etc/default/numerus
|
||||
ExecStart=/usr/bin/numerus
|
||||
Restart=always
|
||||
StandardOutput=append:/var/log/numerus/access.log
|
||||
StandardError=append:/var/log/numerus/error.log
|
|
@ -1,14 +1,10 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
include /usr/share/dpkg/pkg-info.mk
|
||||
|
||||
%:
|
||||
dh $@ --builddirectory=_build --buildsystem=golang --with=golang
|
||||
|
||||
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
|
||||
|
||||
execute_after_dh_auto_test:
|
||||
pg_virtualenv -v 15 make test-deploy
|
||||
pg_virtualenv -v 13 make test-deploy
|
||||
|
|
209
demo/demo.sql
209
demo/demo.sql
|
@ -2,9 +2,7 @@ begin;
|
|||
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
create or replace function public.database_is_numerus_demo() returns bool as $$ select true $$ language sql;
|
||||
|
||||
alter sequence user_user_id_seq restart with 123;
|
||||
alter sequence user_user_id_seq restart;
|
||||
insert into auth."user" (email, name, password, role)
|
||||
values ('demo@numerus', 'Demo User', 'demo', 'invoicer')
|
||||
, ('admin@numerus', 'Demo Admin', 'admin', 'admin')
|
||||
|
@ -12,188 +10,65 @@ values ('demo@numerus', 'Demo User', 'demo', 'invoicer')
|
|||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
alter sequence company_company_id_seq restart with 123;
|
||||
alter sequence company_company_id_seq restart;
|
||||
insert into company (business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id, legal_disclaimer)
|
||||
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.')
|
||||
;
|
||||
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.');
|
||||
|
||||
alter sequence payment_method_payment_method_id_seq restart with 123;
|
||||
alter sequence payment_method_payment_method_id_seq restart;
|
||||
insert into payment_method (company_id, name, instructions)
|
||||
values (123, 'Efectiu', 'Pagament en efectiu al comptat.')
|
||||
, (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')
|
||||
values (1, 'Efectiu', 'Pagament en efectiu al comptat.')
|
||||
, (1, '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;
|
||||
|
||||
insert into company_user (company_id, user_id)
|
||||
values (123, 123)
|
||||
, (123, 124)
|
||||
, (124, 123)
|
||||
, (124, 124)
|
||||
values (1, 1)
|
||||
, (1, 2)
|
||||
;
|
||||
|
||||
alter sequence tax_class_tax_class_id_seq restart with 123;
|
||||
alter sequence tax_class_tax_class_id_seq restart;
|
||||
insert into tax_class (company_id, name)
|
||||
values (123, 'IRPF')
|
||||
, (123, 'IVA')
|
||||
, (124, 'IRPF')
|
||||
, (124, 'IVA')
|
||||
values (1, 'IRPF')
|
||||
, (1, 'IVA')
|
||||
;
|
||||
|
||||
alter sequence tax_tax_id_seq restart with 123;
|
||||
alter sequence tax_tax_id_seq restart;
|
||||
insert into tax (company_id, tax_class_id, name, rate)
|
||||
values (123, 123, 'Retenció 15 %', -0.15)
|
||||
, (123, 124, 'IVA 21 %', 0.21)
|
||||
, (123, 124, 'IVA 10 %', 0.10)
|
||||
, (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)
|
||||
values (1, 1, 'Retenció 15 %', -0.15)
|
||||
, (1, 2, 'IVA 21 %', 0.21)
|
||||
, (1, 2, 'IVA 10 %', 0.10)
|
||||
, (1, 2, 'IVA 4 %', 0.04)
|
||||
;
|
||||
|
||||
alter sequence contact_contact_id_seq restart with 123;
|
||||
-- customers
|
||||
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 (123, 'Gaspar', '111', 'gaspar@reismags.cat', '', '(Rei Ros,2,"C/ Principal, 2",Nova Delhi,Delhi,2,IN)', '', '', array['pesebre', 'mag', 'client']);
|
||||
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 (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 (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 contact_contact_id_seq restart;
|
||||
select add_contact (1, 'Melcior', '1', 'Rei Blanc', '0732621', 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR', array['pesebre', 'mag']);
|
||||
select add_contact (1, 'Gaspar', '2', 'Rei Ros', '111', 'gaspar@reismags.cat', '', 'C/ Principal, 2', 'Nova Delhi', 'Delhi', '2', 'IN', array['pesebre', 'mag']);
|
||||
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 (1, 'Caganera', '41414141L', '', '222 222 222', 'caganera@pesebre.cat', '', 'C/ De l’Hort, 4', 'Olot', 'Girona', '17800', 'ES', array['pesebre', 'persona']);
|
||||
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 (1, 'Rabadà', '41414143K', '', '444 444 444', 'rabada@pesebre.cat', '', 'C/ De les Ovelles, 6', 'Fornells de la Selva', 'Girona', '17458', 'ES', array['pesebre', 'persona']);
|
||||
|
||||
alter sequence product_product_id_seq restart with 123;
|
||||
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(123, 'Encens', 'Goma resina fragrant que desprèn una olor característica quan es crema.', '2.15', array[124], 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(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(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(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(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 product_product_id_seq restart;
|
||||
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(1, 'Encens', 'Goma resina fragrant que desprèn una olor característica quan es crema.', '2.15', array[2], 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(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(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(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(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']);
|
||||
|
||||
alter table payment_account alter column payment_account_id restart with 123;
|
||||
select add_payment_account_bank(123, 'Guardiola', 'ES2820958297603648596978');
|
||||
select add_payment_account_cash(123, 'Matalàs');
|
||||
alter sequence invoice_invoice_id_seq restart;
|
||||
alter sequence invoice_product_invoice_product_id_seq restart;
|
||||
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_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})"}');
|
||||
|
||||
alter sequence invoice_invoice_id_seq restart with 123;
|
||||
alter sequence invoice_product_invoice_product_id_seq restart with 123;
|
||||
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}', '{}');
|
||||
update invoice set invoice_status = 'paid' where invoice_id in (1, 5);
|
||||
update invoice set invoice_status = 'unpaid' where invoice_id = 3;
|
||||
update invoice set invoice_status = 'sent' where invoice_id = 4;
|
||||
|
||||
commit;
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
-- Deploy numerus:_merge_collection_into_payment to pg
|
||||
-- requires: collection
|
||||
-- requires: collection_attachment
|
||||
-- requires: invoice_collection
|
||||
-- requires: payment
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
alter table payment
|
||||
drop constraint if exists payment_amount_positive
|
||||
, add constraint payment_amount_not_zero check (amount <> 0)
|
||||
;
|
||||
|
||||
update payment
|
||||
set amount = -amount
|
||||
;
|
||||
|
||||
insert into payment (company_id, slug, description, payment_date, payment_account_id, amount, currency_code, tags, payment_status, created_at)
|
||||
select company_id, slug, description, collection_date, payment_account_id, amount, currency_code, tags, payment_status, created_at
|
||||
from collection
|
||||
;
|
||||
|
||||
insert into payment_attachment (payment_id, original_filename, mime_type, content)
|
||||
select payment_id, original_filename, mime_type, content
|
||||
from collection_attachment
|
||||
join collection using (collection_id)
|
||||
join payment using (slug)
|
||||
;
|
||||
|
||||
drop table collection_attachment;
|
||||
|
||||
insert into invoice_payment (invoice_id, payment_id)
|
||||
select invoice_id, payment_id
|
||||
from invoice_collection
|
||||
join collection using (collection_id)
|
||||
join payment using (slug)
|
||||
;
|
||||
|
||||
drop table invoice_collection;
|
||||
|
||||
drop table collection;
|
||||
|
||||
commit;
|
|
@ -1,68 +0,0 @@
|
|||
-- Deploy numerus:add_collection to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment
|
||||
-- requires: invoice_payment
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tag_name
|
||||
-- requires: update_invoice_collection_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_collection(company integer, invoice_id integer, collection_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
cslug uuid;
|
||||
amount_cents integer;
|
||||
begin
|
||||
insert into payment
|
||||
( company_id
|
||||
, payment_account_id
|
||||
, description
|
||||
, payment_date
|
||||
, amount
|
||||
, currency_code
|
||||
, payment_status
|
||||
, tags
|
||||
)
|
||||
select company_id
|
||||
, payment_account_id
|
||||
, description
|
||||
, collection_date
|
||||
, parse_price(amount, currency.decimal_digits)
|
||||
, currency_code
|
||||
, 'complete'
|
||||
, tags
|
||||
from company
|
||||
join currency using (currency_code)
|
||||
where company.company_id = add_collection.company
|
||||
returning payment_id, slug, payment.amount
|
||||
into cid, cslug, amount_cents
|
||||
;
|
||||
|
||||
if invoice_id is not null then
|
||||
-- must be inserted before updating statuses, so that it can see this
|
||||
-- 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;
|
|
@ -1,68 +0,0 @@
|
|||
-- Deploy numerus:add_collection to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: collection
|
||||
-- requires: invoice_collection
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tag_name
|
||||
-- requires: update_invoice_collection_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_collection(company integer, invoice_id integer, collection_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
cslug uuid;
|
||||
amount_cents integer;
|
||||
begin
|
||||
insert into collection
|
||||
( company_id
|
||||
, payment_account_id
|
||||
, description
|
||||
, collection_date
|
||||
, amount
|
||||
, currency_code
|
||||
, payment_status
|
||||
, tags
|
||||
)
|
||||
select company_id
|
||||
, payment_account_id
|
||||
, description
|
||||
, collection_date
|
||||
, parse_price(amount, currency.decimal_digits)
|
||||
, currency_code
|
||||
, 'complete'
|
||||
, tags
|
||||
from company
|
||||
join currency using (currency_code)
|
||||
where company.company_id = add_collection.company
|
||||
returning collection_id, slug, collection.amount
|
||||
into cid, cslug, amount_cents
|
||||
;
|
||||
|
||||
if invoice_id is not null then
|
||||
-- must be inserted before updating statuses, so that it can see this
|
||||
-- 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,65 +7,21 @@
|
|||
-- requires: country_code
|
||||
-- requires: contact
|
||||
-- requires: tag_name
|
||||
-- requires: tax_details
|
||||
-- requires: contact_web
|
||||
-- requires: contact_phone
|
||||
-- requires: contact_tax_details
|
||||
-- requires: contact_iban
|
||||
-- requires: contact_bic
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_contact(company_id integer, name text, phone text, email text, web text, tax_details tax_details, iban text, bic text, tags tag_name[]) returns uuid as
|
||||
create or replace function add_contact(company_id integer, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
cslug uuid;
|
||||
begin
|
||||
insert into contact (company_id, name, tags)
|
||||
values (add_contact.company_id, add_contact.name, add_contact.tags)
|
||||
insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, tags)
|
||||
values (add_contact.company_id, add_contact.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
|
||||
;
|
||||
|
||||
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;
|
||||
|
||||
into cid, cslug;
|
||||
|
||||
return cslug;
|
||||
end
|
||||
|
@ -73,10 +29,8 @@ $$
|
|||
language plpgsql
|
||||
;
|
||||
|
||||
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, tax_details, text, text, tag_name[]) to invoicer;
|
||||
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[]);
|
||||
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;
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
-- Deploy numerus:add_contact to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: extension_vat
|
||||
-- requires: email
|
||||
-- requires: extension_pg_libphonenumber
|
||||
-- requires: extension_uri
|
||||
-- requires: country_code
|
||||
-- requires: contact
|
||||
-- requires: tag_name
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_contact(company_id integer, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
cslug uuid;
|
||||
begin
|
||||
insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, tags)
|
||||
values (add_contact.company_id, add_contact.business_name, (add_contact.country_code || add_contact.vatin)::vatin, add_contact.trade_name, parse_packed_phone_number(add_contact.phone, add_contact.country_code), add_contact.email, add_contact.web, add_contact.address, add_contact.city, add_contact.province, add_contact.postal_code, add_contact.country_code, add_contact.tags)
|
||||
returning contact_id, slug
|
||||
into cid, cslug;
|
||||
|
||||
return cslug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public;
|
||||
grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer;
|
||||
grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -8,15 +8,11 @@
|
|||
-- requires: parse_price
|
||||
-- requires: tax
|
||||
-- requires: tag_name
|
||||
-- requires: expense_status
|
||||
-- requires: expense_expense_status
|
||||
|
||||
begin;
|
||||
|
||||
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
|
||||
$$
|
||||
declare
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
-- Deploy numerus:add_expense to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense
|
||||
-- requires: expense_tax
|
||||
-- requires: tax
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tax
|
||||
-- requires: tag_name
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_expense(company integer, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
eid integer;
|
||||
eslug uuid;
|
||||
begin
|
||||
insert into expense (company_id, contact_id, invoice_number, invoice_date, amount, currency_code, tags)
|
||||
select company_id
|
||||
, contact_id
|
||||
, invoice_number
|
||||
, invoice_date
|
||||
, parse_price(amount, currency.decimal_digits)
|
||||
, currency_code
|
||||
, tags
|
||||
from company
|
||||
join currency using (currency_code)
|
||||
where company.company_id = add_expense.company
|
||||
returning expense_id, slug
|
||||
into eid, eslug;
|
||||
|
||||
insert into expense_tax (expense_id, tax_id, tax_rate)
|
||||
select eid, tax_id, tax.rate
|
||||
from tax
|
||||
join unnest(taxes) as etax(tax_id) using (tax_id);
|
||||
|
||||
return eslug;
|
||||
end;
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) from public;
|
||||
grant execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) to invoicer;
|
||||
grant execute on function add_expense(integer, date, integer, text, text, integer[], tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -1,55 +0,0 @@
|
|||
-- Deploy numerus:add_expense to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense
|
||||
-- requires: expense_tax
|
||||
-- requires: tax
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tax
|
||||
-- requires: tag_name
|
||||
-- requires: expense_status
|
||||
-- requires: expense_expense_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_expense(company integer, status text, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
eid integer;
|
||||
eslug uuid;
|
||||
begin
|
||||
insert into expense (company_id, contact_id, invoice_number, invoice_date, amount, currency_code, expense_status, tags)
|
||||
select company_id
|
||||
, contact_id
|
||||
, invoice_number
|
||||
, invoice_date
|
||||
, parse_price(amount, currency.decimal_digits)
|
||||
, currency_code
|
||||
, status
|
||||
, tags
|
||||
from company
|
||||
join currency using (currency_code)
|
||||
where company.company_id = add_expense.company
|
||||
returning expense_id, slug
|
||||
into eid, eslug;
|
||||
|
||||
insert into expense_tax (expense_id, tax_id, tax_rate)
|
||||
select eid, tax_id, tax.rate
|
||||
from tax
|
||||
join unnest(taxes) as etax(tax_id) using (tax_id);
|
||||
|
||||
return eslug;
|
||||
end;
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function add_expense(integer, text, date, integer, text, text, integer[], tag_name[]) from public;
|
||||
grant execute on function add_expense(integer, text, date, integer, text, text, integer[], tag_name[]) to invoicer;
|
||||
grant execute on function add_expense(integer, text, date, integer, text, text, integer[], tag_name[]) to admin;
|
||||
|
||||
drop function if exists add_expense(integer, date, integer, text, text, integer[], tag_name[]);
|
||||
|
||||
commit;
|
|
@ -1,67 +0,0 @@
|
|||
-- Deploy numerus:add_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment
|
||||
-- requires: expense_payment
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tag_name
|
||||
-- requires: update_expense_payment_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_payment(company integer, expense_id integer, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
pslug uuid;
|
||||
pid integer;
|
||||
amount_cents integer;
|
||||
begin
|
||||
insert into payment
|
||||
( company_id
|
||||
, payment_account_id
|
||||
, description
|
||||
, payment_date
|
||||
, amount
|
||||
, currency_code
|
||||
, payment_status
|
||||
, tags
|
||||
)
|
||||
select company_id
|
||||
, payment_account_id
|
||||
, description
|
||||
, payment_date
|
||||
, -parse_price(amount, currency.decimal_digits)
|
||||
, currency_code
|
||||
, 'complete'
|
||||
, tags
|
||||
from company
|
||||
join currency using (currency_code)
|
||||
where company.company_id = add_payment.company
|
||||
returning payment_id, slug, payment.amount
|
||||
into pid, pslug, amount_cents
|
||||
;
|
||||
|
||||
if expense_id is not null then
|
||||
-- must be inserted before updating statuses, so that it can see this
|
||||
-- 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;
|
|
@ -1,67 +0,0 @@
|
|||
-- Deploy numerus:add_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment
|
||||
-- requires: expense_payment
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tag_name
|
||||
-- requires: update_expense_payment_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_payment(company integer, expense_id integer, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
pslug uuid;
|
||||
pid integer;
|
||||
amount_cents integer;
|
||||
begin
|
||||
insert into payment
|
||||
( company_id
|
||||
, payment_account_id
|
||||
, description
|
||||
, payment_date
|
||||
, amount
|
||||
, currency_code
|
||||
, payment_status
|
||||
, tags
|
||||
)
|
||||
select company_id
|
||||
, payment_account_id
|
||||
, description
|
||||
, payment_date
|
||||
, parse_price(amount, currency.decimal_digits)
|
||||
, currency_code
|
||||
, 'complete'
|
||||
, tags
|
||||
from company
|
||||
join currency using (currency_code)
|
||||
where company.company_id = add_payment.company
|
||||
returning payment_id, slug, payment.amount
|
||||
into pid, pslug, amount_cents
|
||||
;
|
||||
|
||||
if expense_id is not null then
|
||||
-- must be inserted before updating statuses, so that it can see this
|
||||
-- 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;
|
|
@ -1,35 +0,0 @@
|
|||
-- Deploy numerus:add_payment_account_bank to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account
|
||||
-- requires: payment_account_bank
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_payment_account_bank(company integer, name text, iban iban) returns uuid as
|
||||
$$
|
||||
declare
|
||||
account_id integer;
|
||||
account_slug uuid;
|
||||
begin
|
||||
insert into payment_account (company_id, payment_account_type, name)
|
||||
select company, 'bank', add_payment_account_bank.name
|
||||
returning payment_account_id, slug into account_id, account_slug;
|
||||
|
||||
insert into payment_account_bank (payment_account_id, iban)
|
||||
values (account_id, iban)
|
||||
;
|
||||
|
||||
return account_slug;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function add_payment_account_bank(integer, text, iban) from public;
|
||||
grant execute on function add_payment_account_bank(integer, text, iban) to invoicer;
|
||||
grant execute on function add_payment_account_bank(integer, text, iban) to admin;
|
||||
|
||||
commit;
|
|
@ -1,34 +0,0 @@
|
|||
-- Deploy numerus:add_payment_account_card to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account
|
||||
-- requires: payment_account_card
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_payment_account_card(company integer, name text, four_digits text, exp_date date) returns uuid as
|
||||
$$
|
||||
declare
|
||||
account_id integer;
|
||||
account_slug uuid;
|
||||
begin
|
||||
insert into payment_account (company_id, payment_account_type, name)
|
||||
select company, 'card', add_payment_account_card.name
|
||||
returning payment_account_id, slug into account_id, account_slug;
|
||||
|
||||
insert into payment_account_card (payment_account_id, last_four_digits, expiration_date)
|
||||
values (account_id, four_digits, exp_date);
|
||||
|
||||
return account_slug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function add_payment_account_card(integer, text, text, date) from public;
|
||||
grant execute on function add_payment_account_card(integer, text, text, date) to invoicer;
|
||||
grant execute on function add_payment_account_card(integer, text, text, date) to admin;
|
||||
|
||||
commit;
|
|
@ -1,23 +0,0 @@
|
|||
-- Deploy numerus:add_payment_account_cash to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_payment_account_cash(company integer, name text) returns uuid as
|
||||
$$
|
||||
insert into payment_account (company_id, payment_account_type, name)
|
||||
values (company, 'cash', name)
|
||||
returning slug;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function add_payment_account_cash(integer, text) from public;
|
||||
grant execute on function add_payment_account_cash(integer, text) to invoicer;
|
||||
grant execute on function add_payment_account_cash(integer, text) to admin;
|
||||
|
||||
commit;
|
|
@ -1,23 +0,0 @@
|
|||
-- Deploy numerus:add_payment_account_other to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_payment_account_other(company integer, name text) returns uuid as
|
||||
$$
|
||||
insert into payment_account (company_id, payment_account_type, name)
|
||||
values (company, 'other', name)
|
||||
returning slug;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function add_payment_account_other(integer, text) from public;
|
||||
grant execute on function add_payment_account_other(integer, text) to invoicer;
|
||||
grant execute on function add_payment_account_other(integer, text) to admin;
|
||||
|
||||
commit;
|
|
@ -1,20 +0,0 @@
|
|||
-- Deploy numerus:attach_to_collection to pg
|
||||
-- requires: roles
|
||||
-- requires: attach_to_payment
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function attach_to_collection(collection_slug uuid, original_filename text, mime_type text, content bytea) returns void as
|
||||
$$
|
||||
select attach_to_payment(collection_slug, original_filename, mime_type, content);
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function attach_to_collection(uuid, text, text, bytea) from public;
|
||||
grant execute on function attach_to_collection(uuid, text, text, bytea) to invoicer;
|
||||
grant execute on function attach_to_collection(uuid, text, text, bytea) to admin;
|
||||
|
||||
commit;
|
|
@ -1,30 +0,0 @@
|
|||
-- Deploy numerus:attach_to_collection to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: collection
|
||||
-- requires: collection_attachment
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function attach_to_collection(collection_slug uuid, original_filename text, mime_type text, content bytea) returns void as
|
||||
$$
|
||||
insert into collection_attachment (collection_id, original_filename, mime_type, content)
|
||||
select collection_id, original_filename, mime_type, content
|
||||
from collection
|
||||
where slug = collection_slug
|
||||
on conflict (collection_id) do update
|
||||
set original_filename = excluded.original_filename
|
||||
, mime_type = excluded.mime_type
|
||||
, content = excluded.content
|
||||
;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function attach_to_collection(uuid, text, text, bytea) from public;
|
||||
grant execute on function attach_to_collection(uuid, text, text, bytea) to invoicer;
|
||||
grant execute on function attach_to_collection(uuid, text, text, bytea) to admin;
|
||||
|
||||
commit;
|
|
@ -1,30 +0,0 @@
|
|||
-- Deploy numerus:attach_to_invoice to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: roles
|
||||
-- requires: invoice
|
||||
-- requires: invoice_attachment
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function attach_to_invoice(invoice_slug uuid, original_filename text, mime_type text, content bytea) returns void as
|
||||
$$
|
||||
insert into invoice_attachment (invoice_id, original_filename, mime_type, content)
|
||||
select invoice_id, original_filename, mime_type, content
|
||||
from invoice
|
||||
where slug = invoice_slug
|
||||
on conflict (invoice_id) do update
|
||||
set original_filename = excluded.original_filename
|
||||
, mime_type = excluded.mime_type
|
||||
, content = excluded.content
|
||||
;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function attach_to_invoice(uuid, text, text, bytea) from public;
|
||||
grant execute on function attach_to_invoice(uuid, text, text, bytea) to invoicer;
|
||||
grant execute on function attach_to_invoice(uuid, text, text, bytea) to admin;
|
||||
|
||||
commit;
|
|
@ -1,30 +0,0 @@
|
|||
-- Deploy numerus:attach_to_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment
|
||||
-- requires: payment_attachment
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function attach_to_payment(payment_slug uuid, original_filename text, mime_type text, content bytea) returns void as
|
||||
$$
|
||||
insert into payment_attachment (payment_id, original_filename, mime_type, content)
|
||||
select payment_id, original_filename, mime_type, content
|
||||
from payment
|
||||
where slug = payment_slug
|
||||
on conflict (payment_id) do update
|
||||
set original_filename = excluded.original_filename
|
||||
, mime_type = excluded.mime_type
|
||||
, content = excluded.content
|
||||
;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function attach_to_payment(uuid, text, text, bytea) from public;
|
||||
grant execute on function attach_to_payment(uuid, text, text, bytea) to invoicer;
|
||||
grant execute on function attach_to_payment(uuid, text, text, bytea) to admin;
|
||||
|
||||
commit;
|
|
@ -1,27 +0,0 @@
|
|||
-- Deploy numerus:available_expense_status to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense_status
|
||||
-- requires: expense_status_i18n
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus;
|
||||
|
||||
insert into expense_status (expense_status, name)
|
||||
values ('pending', 'Pending')
|
||||
, ('partial', 'Partial')
|
||||
, ('paid', 'Paid')
|
||||
on conflict (expense_status) do nothing
|
||||
;
|
||||
|
||||
insert into expense_status_i18n (expense_status, lang_tag, name)
|
||||
values ('pending', 'ca', 'Pendent')
|
||||
, ('partial', 'ca', 'Parcial')
|
||||
, ('paid', 'ca', 'Pagada')
|
||||
, ('pending', 'es', 'Pendiente')
|
||||
, ('partial', 'es', 'Parcial')
|
||||
, ('paid', 'es', 'Pagada')
|
||||
on conflict (expense_status, lang_tag) do nothing
|
||||
;
|
||||
|
||||
commit;
|
|
@ -1,22 +0,0 @@
|
|||
-- Deploy numerus:available_expense_status to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense_status
|
||||
-- requires: expense_status_i18n
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus;
|
||||
|
||||
insert into expense_status (expense_status, name)
|
||||
values ('pending', 'Pending')
|
||||
, ('paid', 'Paid')
|
||||
;
|
||||
|
||||
insert into expense_status_i18n (expense_status, lang_tag, name)
|
||||
values ('pending', 'ca', 'Pendent')
|
||||
, ('paid', 'ca', 'Pagada')
|
||||
, ('pending', 'es', 'Pendiente')
|
||||
, ('paid', 'es', 'Pagada')
|
||||
;
|
||||
|
||||
commit;
|
|
@ -10,28 +10,19 @@ set search_path to numerus;
|
|||
insert into invoice_status (invoice_status, name)
|
||||
values ('created', 'Created')
|
||||
, ('sent', 'Sent')
|
||||
, ('partial', 'Partial')
|
||||
, ('paid', 'Paid')
|
||||
, ('unpaid', 'Unpaid')
|
||||
on conflict (invoice_status) do nothing
|
||||
;
|
||||
|
||||
insert into invoice_status_i18n (invoice_status, lang_tag, name)
|
||||
values ('created', 'ca', 'Creada')
|
||||
, ('sent', 'ca', 'Enviada')
|
||||
, ('partial', 'ca', 'Parcial')
|
||||
, ('paid', 'ca', 'Cobrada')
|
||||
, ('unpaid', 'ca', 'No cobrada')
|
||||
, ('created', 'es', 'Creada')
|
||||
, ('sent', 'es', 'Enviada')
|
||||
, ('partial', 'es', 'Parcial')
|
||||
, ('paid', 'es', '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;
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
-- Deploy numerus:available_invoice_status to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: invoice_status
|
||||
-- requires: invoice_status_i18n
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus;
|
||||
|
||||
insert into invoice_status (invoice_status, name)
|
||||
values ('created', 'Created')
|
||||
, ('sent', 'Sent')
|
||||
, ('paid', 'Paid')
|
||||
, ('unpaid', 'Unpaid')
|
||||
;
|
||||
|
||||
insert into invoice_status_i18n (invoice_status, lang_tag, name)
|
||||
values ('created', 'ca', 'Creada')
|
||||
, ('sent', 'ca', 'Enviada')
|
||||
, ('paid', 'ca', 'Cobrada')
|
||||
, ('unpaid', 'ca', 'No cobrada')
|
||||
, ('created', 'es', 'Creada')
|
||||
, ('sent', 'es', 'Enviada')
|
||||
, ('paid', 'es', 'Cobrada')
|
||||
, ('unpaid', 'es', 'No cobrada')
|
||||
;
|
||||
|
||||
commit;
|
|
@ -1,28 +0,0 @@
|
|||
-- Deploy numerus:available_payment_account_types to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account_type
|
||||
-- requires: payment_account_type_i18n
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus;
|
||||
|
||||
insert into payment_account_type (payment_account_type, name)
|
||||
values ('bank', 'Bank')
|
||||
, ('card', 'Credit Card')
|
||||
, ('cash', 'Cash')
|
||||
, ('other', 'Other')
|
||||
;
|
||||
|
||||
insert into payment_account_type_i18n (payment_account_type, lang_tag, name)
|
||||
values ('bank', 'ca', 'Banc')
|
||||
, ('card', 'ca', 'Targeta de crèdit')
|
||||
, ('cash', 'ca', 'Efectiu')
|
||||
, ('other', 'ca', 'Altres')
|
||||
, ('bank', 'es', 'Banco')
|
||||
, ('card', 'es', 'Tarjeta de crédito')
|
||||
, ('cash', 'es', 'Efectivo')
|
||||
, ('other', 'es', 'Otros')
|
||||
;
|
||||
|
||||
commit;
|
|
@ -1,22 +0,0 @@
|
|||
-- Deploy numerus:available_payment_status to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_status
|
||||
-- requires: payment_status_i18n
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
insert into payment_status (payment_status, name)
|
||||
values ('partial', 'Partial')
|
||||
, ('complete', 'Complete')
|
||||
;
|
||||
|
||||
insert into payment_status_i18n (payment_status, lang_tag, name)
|
||||
values ('partial', 'ca', 'Parcial')
|
||||
, ('partial', 'es', 'Parcial')
|
||||
, ('complete', 'ca', 'Complet')
|
||||
, ('complete', 'es', 'Completo')
|
||||
;
|
||||
|
||||
commit;
|
|
@ -1,14 +0,0 @@
|
|||
-- Deploy numerus:bic to pg
|
||||
-- requires: schema_numerus
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create domain bic as text
|
||||
check ( value ~ '^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$' )
|
||||
;
|
||||
|
||||
comment on domain bic is 'A valid, but not necessarily existent, Business Identifier Code (ISO 9362)';
|
||||
|
||||
commit;
|
|
@ -1,45 +0,0 @@
|
|||
-- Deploy numerus:collection to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: company
|
||||
-- requires: payment_account
|
||||
-- requires: currency
|
||||
-- requires: tag_name
|
||||
-- requires: payment_status
|
||||
-- requires: extension_pgcrypto
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table collection (
|
||||
collection_id integer generated by default as identity primary key,
|
||||
company_id integer not null references company,
|
||||
slug uuid not null unique default gen_random_uuid(),
|
||||
description text not null,
|
||||
collection_date date not null default current_date,
|
||||
payment_account_id integer not null references payment_account,
|
||||
amount integer not null constraint collection_amount_positive check (amount > 0),
|
||||
currency_code text not null references currency,
|
||||
tags tag_name[] not null default '{}',
|
||||
payment_status text not null default 'complete' references payment_status,
|
||||
created_at timestamptz not null default current_timestamp
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table collection to invoicer;
|
||||
grant select, insert, update, delete on table collection to admin;
|
||||
|
||||
alter table collection enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on collection
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = collection.company_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,32 +0,0 @@
|
|||
-- Deploy numerus:collection_attachment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: collection
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table collection_attachment (
|
||||
collection_id integer primary key references collection,
|
||||
original_filename text not null,
|
||||
mime_type text not null,
|
||||
content bytea not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table collection_attachment to invoicer;
|
||||
grant select, insert, update, delete on table collection_attachment to admin;
|
||||
|
||||
alter table collection_attachment enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on collection_attachment
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from collection
|
||||
where collection.collection_id = collection_attachment.collection_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,68 +0,0 @@
|
|||
-- Deploy numerus:compute_new_expense_amount to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: roles
|
||||
-- requires: company
|
||||
-- requires: tax
|
||||
-- requires: new_expense_amount
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function compute_new_expense_amount(company_id integer, subtotal text, taxes integer[]) returns new_expense_amount as
|
||||
$$
|
||||
declare
|
||||
result new_expense_amount;
|
||||
begin
|
||||
if trim(subtotal) = '' then
|
||||
subtotal = '0';
|
||||
end if;
|
||||
if array_length(taxes, 1) > 0 then
|
||||
with line as (
|
||||
select round(parse_price(subtotal, currency.decimal_digits)) as price
|
||||
, tax_id
|
||||
, decimal_digits
|
||||
from unnest (taxes) as tax(tax_id)
|
||||
join company on company.company_id = compute_new_expense_amount.company_id
|
||||
join currency using (currency_code)
|
||||
)
|
||||
, tax_amount as (
|
||||
select tax_id
|
||||
, sum(round(price * tax.rate)::integer)::integer as amount
|
||||
, decimal_digits
|
||||
from line
|
||||
join tax using (tax_id)
|
||||
group by tax_id, decimal_digits
|
||||
)
|
||||
, tax_total as (
|
||||
select sum(amount)::integer as amount
|
||||
, array_agg(array[name, to_price(amount, decimal_digits)]) as taxes
|
||||
from tax_amount
|
||||
join tax using (tax_id)
|
||||
)
|
||||
select coalesce(tax_total.taxes, array[]::text[][])
|
||||
, to_price(price::integer + coalesce(tax_total.amount, 0), decimal_digits) as total
|
||||
from line, tax_total
|
||||
into result.taxes, result.total;
|
||||
else
|
||||
select array[]::text[][]
|
||||
, to_price(coalesce(parse_price(subtotal, decimal_digits), 0), decimal_digits)
|
||||
from company
|
||||
join currency using (currency_code)
|
||||
where company.company_id = compute_new_expense_amount.company_id
|
||||
into result.taxes, result.total
|
||||
;
|
||||
end if;
|
||||
|
||||
return result;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
stable
|
||||
;
|
||||
|
||||
revoke execute on function compute_new_expense_amount(integer, text, integer[]) from public;
|
||||
grant execute on function compute_new_expense_amount(integer, text, integer[]) to invoicer;
|
||||
grant execute on function compute_new_expense_amount(integer, text, integer[]) to admin;
|
||||
|
||||
commit;
|
|
@ -1,39 +0,0 @@
|
|||
-- Deploy numerus:contact_email to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: email
|
||||
-- requires: contact
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table contact_email (
|
||||
contact_id integer primary key references contact,
|
||||
email email not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table contact_email to invoicer;
|
||||
grant select, insert, update, delete on table contact_email to admin;
|
||||
|
||||
alter table contact_email enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on contact_email
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from contact
|
||||
where contact.contact_id = contact_email.contact_id
|
||||
)
|
||||
);
|
||||
|
||||
insert into contact_email
|
||||
select contact_id, email
|
||||
from contact;
|
||||
|
||||
alter table contact
|
||||
drop column email
|
||||
;
|
||||
|
||||
commit;
|
|
@ -1,31 +0,0 @@
|
|||
-- Deploy numerus:contact_iban to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: roles
|
||||
-- requires: contact
|
||||
-- requires: extension_iban
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table contact_iban (
|
||||
contact_id integer primary key references contact,
|
||||
iban iban not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table contact_iban to invoicer;
|
||||
grant select, insert, update, delete on table contact_iban to admin;
|
||||
|
||||
alter table contact_iban enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on contact_iban
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from contact
|
||||
where contact.contact_id = contact_iban.contact_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,38 +0,0 @@
|
|||
-- Deploy numerus:contact_phone to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: extension_pg_libphonenumber
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table contact_phone (
|
||||
contact_id integer primary key references contact,
|
||||
phone packed_phone_number not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table contact_phone to invoicer;
|
||||
grant select, insert, update, delete on table contact_phone to admin;
|
||||
|
||||
alter table contact_phone enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on contact_phone
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from contact
|
||||
where contact.contact_id = contact_phone.contact_id
|
||||
)
|
||||
);
|
||||
|
||||
insert into contact_phone
|
||||
select contact_id, phone
|
||||
from contact;
|
||||
|
||||
alter table contact
|
||||
drop column phone
|
||||
;
|
||||
|
||||
commit;
|
|
@ -1,31 +0,0 @@
|
|||
-- Deploy numerus:contact_swift to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: roles
|
||||
-- requires: contact
|
||||
-- requires: bic
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table contact_swift (
|
||||
contact_id integer primary key references contact,
|
||||
bic bic not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table contact_swift to invoicer;
|
||||
grant select, insert, update, delete on table contact_swift to admin;
|
||||
|
||||
alter table contact_swift enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on contact_swift
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from contact
|
||||
where contact.contact_id = contact_swift.contact_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,60 +0,0 @@
|
|||
-- Deploy numerus:contact_tax_details to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: contact
|
||||
-- requires: extension_vat
|
||||
-- requires: country_code
|
||||
-- requires: country
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table contact_tax_details (
|
||||
contact_id integer primary key references contact,
|
||||
business_name text not null constraint business_name_not_empty check(length(trim(business_name)) > 1),
|
||||
vatin vatin not null,
|
||||
address text not null,
|
||||
city text not null,
|
||||
province text not null,
|
||||
postal_code text not null,
|
||||
country_code country_code not null references country
|
||||
);
|
||||
|
||||
alter table contact_tax_details enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on contact_tax_details
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from contact
|
||||
where contact.contact_id = contact_tax_details.contact_id
|
||||
)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table contact_tax_details to invoicer;
|
||||
grant select, insert, update, delete on table contact_tax_details to admin;
|
||||
|
||||
insert into contact_tax_details
|
||||
select contact_id, business_name, vatin, address, city, province, postal_code, country_code
|
||||
from contact;
|
||||
|
||||
update contact set trade_name = business_name where trade_name = '';
|
||||
|
||||
alter table contact
|
||||
rename column trade_name to name
|
||||
;
|
||||
|
||||
alter table contact
|
||||
drop column business_name
|
||||
, drop column vatin
|
||||
, drop column address
|
||||
, drop column city
|
||||
, drop column province
|
||||
, drop column postal_code
|
||||
, drop column country_code
|
||||
, add constraint name_not_empty check(length(trim(name)) > 1)
|
||||
;
|
||||
|
||||
commit;
|
|
@ -1,39 +0,0 @@
|
|||
-- Deploy numerus:contact_web to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: extension_uri
|
||||
-- requires: contact
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table contact_web (
|
||||
contact_id integer primary key references contact,
|
||||
uri uri not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table contact_web to invoicer;
|
||||
grant select, insert, update, delete on table contact_web to admin;
|
||||
|
||||
alter table contact_web enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on contact_web
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from contact
|
||||
where contact.contact_id = contact_web.contact_id
|
||||
)
|
||||
);
|
||||
|
||||
insert into contact_web
|
||||
select contact_id, web
|
||||
from contact;
|
||||
|
||||
alter table contact
|
||||
drop column web
|
||||
;
|
||||
|
||||
commit;
|
|
@ -1,53 +0,0 @@
|
|||
-- Deploy numerus:edit_collection to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment
|
||||
-- requires: invoice_payment
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tag_name
|
||||
-- requires: update_invoice_collection_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_collection(collection_slug uuid, collection_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
iid integer;
|
||||
amount_cents integer;
|
||||
begin
|
||||
update payment
|
||||
set payment_date = edit_collection.collection_date
|
||||
, payment_account_id = edit_collection.payment_account_id
|
||||
, description = edit_collection.description
|
||||
, amount = parse_price(edit_collection.amount, decimal_digits)
|
||||
, tags = edit_collection.tags
|
||||
from currency
|
||||
where slug = collection_slug
|
||||
and currency.currency_code = payment.currency_code
|
||||
returning payment_id, payment.amount
|
||||
into cid, amount_cents
|
||||
;
|
||||
|
||||
select invoice_id into iid
|
||||
from invoice_payment
|
||||
where payment_id = cid;
|
||||
|
||||
if iid is not null then
|
||||
perform update_invoice_collection_status(cid, iid, amount_cents);
|
||||
end if;
|
||||
|
||||
return collection_slug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) from public;
|
||||
grant execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) to invoicer;
|
||||
grant execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -1,53 +0,0 @@
|
|||
-- Deploy numerus:edit_collection to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: collection
|
||||
-- requires: invoice_collection
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tag_name
|
||||
-- requires: update_invoice_collection_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_collection(collection_slug uuid, collection_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
iid integer;
|
||||
amount_cents integer;
|
||||
begin
|
||||
update collection
|
||||
set collection_date = edit_collection.collection_date
|
||||
, payment_account_id = edit_collection.payment_account_id
|
||||
, description = edit_collection.description
|
||||
, amount = parse_price(edit_collection.amount, decimal_digits)
|
||||
, tags = edit_collection.tags
|
||||
from currency
|
||||
where slug = collection_slug
|
||||
and currency.currency_code = collection.currency_code
|
||||
returning collection_id, collection.amount
|
||||
into cid, amount_cents
|
||||
;
|
||||
|
||||
select invoice_id into iid
|
||||
from invoice_collection
|
||||
where collection_id = cid;
|
||||
|
||||
if iid is not null then
|
||||
perform update_invoice_collection_status(cid, iid, amount_cents);
|
||||
end if;
|
||||
|
||||
return collection_slug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) from public;
|
||||
grant execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) to invoicer;
|
||||
grant execute on function edit_collection(uuid, date, integer, text, text, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -7,25 +7,29 @@
|
|||
-- requires: contact
|
||||
-- requires: extension_vat
|
||||
-- requires: extension_pg_libphonenumber
|
||||
-- requires: tax_details
|
||||
-- requires: contact_web
|
||||
-- requires: contact_phone
|
||||
-- requires: contact_tax_details
|
||||
-- requires: contact_iban
|
||||
-- requires: contact_bic
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_contact(contact_slug uuid, name text, phone text, email text, web text, tax_details tax_details, iban text, bic text, tags tag_name[]) returns uuid as
|
||||
create or replace function edit_contact(contact_slug uuid, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
company integer;
|
||||
begin
|
||||
update contact
|
||||
set name = edit_contact.name
|
||||
set business_name = edit_contact.business_name
|
||||
, vatin = (edit_contact.country_code || edit_contact.vatin)::vatin
|
||||
, trade_name = edit_contact.trade_name
|
||||
, phone = parse_packed_phone_number( edit_contact.phone, edit_contact.country_code)
|
||||
, email = edit_contact.email
|
||||
, web = edit_contact.web
|
||||
, address = edit_contact.address
|
||||
, city = edit_contact.city
|
||||
, province = edit_contact.province
|
||||
, postal_code = edit_contact.postal_code
|
||||
, country_code = edit_contact.country_code
|
||||
, tags = edit_contact.tags
|
||||
where slug = contact_slug
|
||||
returning contact_id, company_id
|
||||
|
@ -36,96 +40,14 @@ begin
|
|||
return null;
|
||||
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;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
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, tax_details, text, text, tag_name[]) to invoicer;
|
||||
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[]);
|
||||
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;
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
-- Deploy numerus:edit_contact to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: email
|
||||
-- requires: extension_uri
|
||||
-- requires: country_code
|
||||
-- requires: tag_name
|
||||
-- requires: contact
|
||||
-- requires: extension_vat
|
||||
-- requires: extension_pg_libphonenumber
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_contact(contact_slug uuid, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
company integer;
|
||||
begin
|
||||
update contact
|
||||
set business_name = edit_contact.business_name
|
||||
, vatin = (edit_contact.country_code || edit_contact.vatin)::vatin
|
||||
, trade_name = edit_contact.trade_name
|
||||
, phone = parse_packed_phone_number( edit_contact.phone, edit_contact.country_code)
|
||||
, email = edit_contact.email
|
||||
, web = edit_contact.web
|
||||
, address = edit_contact.address
|
||||
, city = edit_contact.city
|
||||
, province = edit_contact.province
|
||||
, postal_code = edit_contact.postal_code
|
||||
, country_code = edit_contact.country_code
|
||||
, tags = edit_contact.tags
|
||||
where slug = contact_slug
|
||||
returning contact_id, company_id
|
||||
into cid, company
|
||||
;
|
||||
|
||||
if cid is null then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
return contact_slug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public;
|
||||
grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer;
|
||||
grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -5,15 +5,11 @@
|
|||
-- requires: parse_price
|
||||
-- requires: tax
|
||||
-- requires: tag_name
|
||||
-- requires: expense_status
|
||||
-- requires: expense_expense_status
|
||||
|
||||
begin;
|
||||
|
||||
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
|
||||
$$
|
||||
declare
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
-- Deploy numerus:edit_expense to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tax
|
||||
-- requires: tag_name
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_expense(expense_slug uuid, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
eid integer;
|
||||
begin
|
||||
update expense
|
||||
set invoice_date = edit_expense.invoice_date
|
||||
, contact_id = edit_expense.contact_id
|
||||
, invoice_number = edit_expense.invoice_number
|
||||
, amount = parse_price(edit_expense.amount, decimal_digits)
|
||||
, tags = edit_expense.tags
|
||||
from currency
|
||||
where slug = expense_slug
|
||||
and currency.currency_code = expense.currency_code
|
||||
returning expense_id
|
||||
into eid;
|
||||
|
||||
if eid is null then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
delete from expense_tax where expense_id = eid;
|
||||
|
||||
insert into expense_tax (expense_id, tax_id, tax_rate)
|
||||
select eid, tax_id, tax.rate
|
||||
from tax
|
||||
join unnest(taxes) as etax(tax_id) using (tax_id);
|
||||
|
||||
return expense_slug;
|
||||
end;
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) from public;
|
||||
grant execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) to invoicer;
|
||||
grant execute on function edit_expense(uuid, date, integer, text, text, integer[], tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -1,53 +0,0 @@
|
|||
-- Deploy numerus:edit_expense to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tax
|
||||
-- requires: tag_name
|
||||
-- requires: expense_status
|
||||
-- requires: expense_expense_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_expense(expense_slug uuid, status text, invoice_date date, contact_id integer, invoice_number text, amount text, taxes integer[], tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
eid integer;
|
||||
begin
|
||||
update expense
|
||||
set invoice_date = edit_expense.invoice_date
|
||||
, contact_id = edit_expense.contact_id
|
||||
, invoice_number = edit_expense.invoice_number
|
||||
, amount = parse_price(edit_expense.amount, decimal_digits)
|
||||
, expense_status = status
|
||||
, tags = edit_expense.tags
|
||||
from currency
|
||||
where slug = expense_slug
|
||||
and currency.currency_code = expense.currency_code
|
||||
returning expense_id
|
||||
into eid;
|
||||
|
||||
if eid is null then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
delete from expense_tax where expense_id = eid;
|
||||
|
||||
insert into expense_tax (expense_id, tax_id, tax_rate)
|
||||
select eid, tax_id, tax.rate
|
||||
from tax
|
||||
join unnest(taxes) as etax(tax_id) using (tax_id);
|
||||
|
||||
return expense_slug;
|
||||
end;
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]) from public;
|
||||
grant execute on function edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]) to invoicer;
|
||||
grant execute on function edit_expense(uuid, text, date, integer, text, text, integer[], tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -14,9 +14,7 @@ begin;
|
|||
|
||||
set search_path to numerus, public;
|
||||
|
||||
drop function if exists edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]);
|
||||
|
||||
create or replace function edit_invoice(invoice_slug uuid, contact_id integer, notes text, payment_method_id integer, tags tag_name[], products edited_invoice_product[]) returns uuid as
|
||||
create or replace function edit_invoice(invoice_slug uuid, 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;
|
||||
|
@ -29,6 +27,7 @@ declare
|
|||
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
|
||||
|
@ -104,9 +103,9 @@ end;
|
|||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]) from public;
|
||||
grant execute on function edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]) to invoicer;
|
||||
grant execute on function edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]) to admin;
|
||||
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;
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
-- Deploy numerus:edit_invoice to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: invoice
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: edited_invoice_product
|
||||
-- requires: tax
|
||||
-- requires: invoice_product
|
||||
-- requires: invoice_product_product
|
||||
-- requires: invoice_product_tax
|
||||
-- requires: tag_name
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_invoice(invoice_slug uuid, invoice_status text, contact_id integer, notes text, payment_method_id integer, tags tag_name[], products edited_invoice_product[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
iid integer;
|
||||
products_to_keep integer[];
|
||||
products_to_delete integer[];
|
||||
company integer;
|
||||
ccode text;
|
||||
product edited_invoice_product;
|
||||
ipid integer;
|
||||
begin
|
||||
update invoice
|
||||
set contact_id = edit_invoice.contact_id
|
||||
, invoice_status = edit_invoice.invoice_status
|
||||
, notes = edit_invoice.notes
|
||||
, payment_method_id = edit_invoice.payment_method_id
|
||||
, tags = edit_invoice.tags
|
||||
where slug = invoice_slug
|
||||
returning invoice_id, company_id, currency_code
|
||||
into iid, company, ccode
|
||||
;
|
||||
|
||||
if iid is null then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
foreach product in array products
|
||||
loop
|
||||
if product.invoice_product_id is null then
|
||||
insert into invoice_product (invoice_id, name, description, price, quantity, discount_rate)
|
||||
select iid
|
||||
, product.name
|
||||
, coalesce(product.description, '')
|
||||
, parse_price(product.price, currency.decimal_digits)
|
||||
, product.quantity
|
||||
, product.discount_rate
|
||||
from currency
|
||||
where currency_code = ccode
|
||||
returning invoice_product_id
|
||||
into ipid;
|
||||
else
|
||||
ipid := product.invoice_product_id;
|
||||
|
||||
update invoice_product
|
||||
set name = product.name
|
||||
, description = coalesce(product.description, '')
|
||||
, price = parse_price(product.price, currency.decimal_digits)
|
||||
, quantity = product.quantity
|
||||
, discount_rate = product.discount_rate
|
||||
from currency
|
||||
where invoice_product_id = ipid
|
||||
and currency_code = ccode;
|
||||
end if;
|
||||
products_to_keep := array_append(products_to_keep, ipid);
|
||||
|
||||
if product.product_id is null then
|
||||
delete from invoice_product_product where invoice_product_id = ipid;
|
||||
else
|
||||
insert into invoice_product_product (invoice_product_id, product_id)
|
||||
values (ipid, product.product_id)
|
||||
on conflict (invoice_product_id) do update
|
||||
set product_id = product.product_id;
|
||||
end if;
|
||||
|
||||
delete from invoice_product_tax where invoice_product_id = ipid;
|
||||
|
||||
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
|
||||
select ipid, tax_id, tax.rate
|
||||
from tax
|
||||
join unnest(product.tax) as ptax(tax_id) using (tax_id);
|
||||
end loop;
|
||||
|
||||
select array_agg(invoice_product_id)
|
||||
into products_to_delete
|
||||
from invoice_product
|
||||
where invoice_id = iid
|
||||
and not (invoice_product_id = any(products_to_keep));
|
||||
|
||||
if array_length(products_to_delete, 1) > 0 then
|
||||
delete from invoice_product_tax where invoice_product_id = any(products_to_delete);
|
||||
delete from invoice_product_product where invoice_product_id = any(products_to_delete);
|
||||
delete from invoice_product where invoice_product_id = any(products_to_delete);
|
||||
end if;
|
||||
|
||||
return invoice_slug;
|
||||
end;
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) from public;
|
||||
grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to invoicer;
|
||||
grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to admin;
|
||||
|
||||
|
||||
commit;
|
|
@ -1,53 +0,0 @@
|
|||
-- Deploy numerus:edit_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment
|
||||
-- requires: expense_payment
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tag_name
|
||||
-- requires: update_expense_payment_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_payment(payment_slug uuid, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
pid integer;
|
||||
eid integer;
|
||||
amount_cents integer;
|
||||
begin
|
||||
update payment
|
||||
set payment_date = edit_payment.payment_date
|
||||
, payment_account_id = edit_payment.payment_account_id
|
||||
, description = edit_payment.description
|
||||
, amount = -parse_price(edit_payment.amount, decimal_digits)
|
||||
, tags = edit_payment.tags
|
||||
from currency
|
||||
where slug = payment_slug
|
||||
and currency.currency_code = payment.currency_code
|
||||
returning payment_id, payment.amount
|
||||
into pid, amount_cents
|
||||
;
|
||||
|
||||
select expense_id into eid
|
||||
from expense_payment
|
||||
where payment_id = pid;
|
||||
|
||||
if eid is not null then
|
||||
perform update_expense_payment_status(pid, eid, amount_cents);
|
||||
end if;
|
||||
|
||||
return payment_slug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) from public;
|
||||
grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to invoicer;
|
||||
grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -1,53 +0,0 @@
|
|||
-- Deploy numerus:edit_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment
|
||||
-- requires: expense_payment
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: tag_name
|
||||
-- requires: update_expense_payment_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_payment(payment_slug uuid, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
pid integer;
|
||||
eid integer;
|
||||
amount_cents integer;
|
||||
begin
|
||||
update payment
|
||||
set payment_date = edit_payment.payment_date
|
||||
, payment_account_id = edit_payment.payment_account_id
|
||||
, description = edit_payment.description
|
||||
, amount = parse_price(edit_payment.amount, decimal_digits)
|
||||
, tags = edit_payment.tags
|
||||
from currency
|
||||
where slug = payment_slug
|
||||
and currency.currency_code = payment.currency_code
|
||||
returning payment_id, payment.amount
|
||||
into pid, amount_cents
|
||||
;
|
||||
|
||||
select expense_id into eid
|
||||
from expense_payment
|
||||
where payment_id = pid;
|
||||
|
||||
if eid is not null then
|
||||
perform update_expense_payment_status(pid, eid, amount_cents);
|
||||
end if;
|
||||
|
||||
return payment_slug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) from public;
|
||||
grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to invoicer;
|
||||
grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -1,43 +0,0 @@
|
|||
-- Deploy numerus:edit_payment_account_bank to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account
|
||||
-- requires: payment_account_bank
|
||||
-- requires: extension_iban
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_payment_account_bank(account_slug uuid, new_name text, new_iban iban) returns uuid as
|
||||
$$
|
||||
declare
|
||||
account_id int;
|
||||
begin
|
||||
update payment_account
|
||||
set name = new_name
|
||||
where slug = account_slug
|
||||
and payment_account_type = 'bank'
|
||||
returning payment_account_id into account_id
|
||||
;
|
||||
|
||||
if account_id is null then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
update payment_account_bank
|
||||
set iban = new_iban
|
||||
where payment_account_id = account_id
|
||||
;
|
||||
|
||||
return account_slug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_payment_account_bank(uuid, text, iban) from public;
|
||||
grant execute on function edit_payment_account_bank(uuid, text, iban) to invoicer;
|
||||
grant execute on function edit_payment_account_bank(uuid, text, iban) to admin;
|
||||
|
||||
commit;
|
|
@ -1,43 +0,0 @@
|
|||
-- Deploy numerus:edit_payment_account_card to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account
|
||||
-- requires: payment_account_card
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_payment_account_card(account_slug uuid, new_name text, new_last_digits text, new_exp_date date) returns uuid as
|
||||
$$
|
||||
declare
|
||||
account_id integer;
|
||||
begin
|
||||
update payment_account
|
||||
set name = new_name
|
||||
where slug = account_slug
|
||||
and payment_account_type = 'card'
|
||||
returning payment_account_id into account_id
|
||||
;
|
||||
|
||||
if account_id is null then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
update payment_account_card
|
||||
set last_four_digits = new_last_digits
|
||||
, expiration_date = new_exp_date
|
||||
where payment_account_id = account_id
|
||||
;
|
||||
|
||||
return account_slug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_payment_account_card(uuid, text, text, date) from public;
|
||||
grant execute on function edit_payment_account_card(uuid, text, text, date) to invoicer;
|
||||
grant execute on function edit_payment_account_card(uuid, text, text, date) to admin;
|
||||
|
||||
commit;
|
|
@ -1,26 +0,0 @@
|
|||
-- Deploy numerus:edit_payment_account_cash to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_payment_account_cash(account_slug uuid, new_name text) returns uuid as
|
||||
$$
|
||||
update payment_account
|
||||
set name = new_name
|
||||
where slug = account_slug
|
||||
and payment_account_type = 'cash'
|
||||
returning slug
|
||||
;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function edit_payment_account_cash(uuid, text) from public;
|
||||
grant execute on function edit_payment_account_cash(uuid, text) to invoicer;
|
||||
grant execute on function edit_payment_account_cash(uuid, text) to admin;
|
||||
|
||||
commit;
|
|
@ -1,26 +0,0 @@
|
|||
-- Deploy numerus:edit_payment_account_other to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_payment_account_other(account_slug uuid, new_name text) returns uuid as
|
||||
$$
|
||||
update payment_account
|
||||
set name = new_name
|
||||
where slug = account_slug
|
||||
and payment_account_type = 'other'
|
||||
returning slug
|
||||
;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function edit_payment_account_other(uuid, text) from public;
|
||||
grant execute on function edit_payment_account_other(uuid, text) to invoicer;
|
||||
grant execute on function edit_payment_account_other(uuid, text) to admin;
|
||||
|
||||
commit;
|
|
@ -1,12 +0,0 @@
|
|||
-- Deploy numerus:expense_expense_status to pg
|
||||
-- requires: expense
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
alter table expense
|
||||
add column expense_status text not null default 'pending' references expense_status
|
||||
;
|
||||
|
||||
commit;
|
|
@ -1,32 +0,0 @@
|
|||
-- Deploy numerus:expense_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense
|
||||
-- requires: payment
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table expense_payment (
|
||||
expense_id integer not null references expense,
|
||||
payment_id integer not null references payment,
|
||||
primary key (expense_id, payment_id)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table expense_payment to invoicer;
|
||||
grant select, insert, update, delete on table expense_payment to admin;
|
||||
|
||||
alter table expense_payment enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on expense_payment
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from expense
|
||||
where expense.expense_id = expense_payment.expense_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,17 +0,0 @@
|
|||
-- Deploy numerus:expense_status to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: roles
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table expense_status (
|
||||
expense_status text primary key,
|
||||
name text not null
|
||||
);
|
||||
|
||||
grant select on table expense_status to invoicer;
|
||||
grant select on table expense_status to admin;
|
||||
|
||||
commit;
|
|
@ -1,21 +0,0 @@
|
|||
-- Deploy numerus:expense_status_i18n to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: roles
|
||||
-- requires: expense_status
|
||||
-- requires: language
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table expense_status_i18n (
|
||||
expense_status text not null references expense_status,
|
||||
lang_tag text not null references language,
|
||||
name text not null,
|
||||
primary key (expense_status, lang_tag)
|
||||
);
|
||||
|
||||
grant select on table expense_status_i18n to invoicer;
|
||||
grant select on table expense_status_i18n to admin;
|
||||
|
||||
commit;
|
|
@ -1,8 +0,0 @@
|
|||
-- Deploy numerus:extension_iban to pg
|
||||
-- requires: schema_numerus
|
||||
|
||||
begin;
|
||||
|
||||
create extension if not exists iban;
|
||||
|
||||
commit;
|
|
@ -1,165 +0,0 @@
|
|||
-- Deploy numerus:import_contact to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: roles
|
||||
-- requires: contact
|
||||
-- requires: contact_web
|
||||
-- requires: contact_phone
|
||||
-- requires: contact_email
|
||||
-- requires: contact_iban
|
||||
-- requires: contact_swift
|
||||
-- requires: contact_tax_details
|
||||
-- requires: input_is_valid
|
||||
-- requires: input_is_valid_phone
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function begin_import_contacts() returns name as
|
||||
$$
|
||||
create temporary table imported_contact (
|
||||
contact_id integer
|
||||
, name text not null default ''
|
||||
, vatin text not null default ''
|
||||
, email text not null default ''
|
||||
, phone text not null default ''
|
||||
, web text not null default ''
|
||||
, address text not null default ''
|
||||
, city text not null default ''
|
||||
, province text not null default ''
|
||||
, postal_code text not null default ''
|
||||
, country_code text not null default ''
|
||||
, iban text not null default ''
|
||||
, bic text not null default ''
|
||||
, tags text not null default ''
|
||||
);
|
||||
select 'imported_contact'::name;
|
||||
$$
|
||||
language sql;
|
||||
|
||||
revoke execute on function begin_import_contacts() from public;
|
||||
grant execute on function begin_import_contacts() to invoicer;
|
||||
grant execute on function begin_import_contacts() to admin;
|
||||
|
||||
create or replace function end_import_contacts(company_id integer) returns integer as
|
||||
$$
|
||||
declare
|
||||
imported integer;
|
||||
begin
|
||||
update imported_contact
|
||||
set country_code = upper(trim(country_code))
|
||||
, name = trim(name)
|
||||
, vatin = trim(vatin)
|
||||
, email = trim(email)
|
||||
, phone = trim(phone)
|
||||
, web = trim(web)
|
||||
, address = trim(address)
|
||||
, city = trim(city)
|
||||
, province = trim(province)
|
||||
, postal_code = trim(postal_code)
|
||||
, iban = trim(iban)
|
||||
, bic = trim(bic)
|
||||
, tags = lower(trim(regexp_replace(regexp_replace(tags, '[^\sA-Za-z0-9-]', '', 'g'), '\s\s+', ' ', 'g')))
|
||||
;
|
||||
|
||||
update imported_contact
|
||||
set contact_id = tax_details.contact_id
|
||||
from contact_tax_details as tax_details
|
||||
join contact using (contact_id)
|
||||
where contact.company_id = end_import_contacts.company_id
|
||||
and tax_details.vatin::text = imported_contact.country_code || imported_contact.vatin
|
||||
;
|
||||
|
||||
update imported_contact
|
||||
set contact_id = nextval('contact_contact_id_seq'::regclass)
|
||||
where length(trim(name)) > 1
|
||||
and contact_id is null
|
||||
;
|
||||
|
||||
insert into contact (contact_id, company_id, name, tags)
|
||||
select contact_id, end_import_contacts.company_id, name, string_to_array(tags, ' ')::tag_name[]
|
||||
from imported_contact
|
||||
where contact_id is not null
|
||||
on conflict (contact_id) do update
|
||||
set tags = array_cat(contact.tags, excluded.tags)
|
||||
;
|
||||
|
||||
insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code)
|
||||
select contact_id, imported_contact.name, (country_code || vatin)::vatin, address, city, province, postal_code, country_code
|
||||
from imported_contact
|
||||
join country using (country_code)
|
||||
where contact_id is not null
|
||||
and length(address) > 1
|
||||
and length(city) > 1
|
||||
and length(province) > 1
|
||||
and postal_code ~ postal_code_regex
|
||||
and input_is_valid(country_code || vatin, 'vatin')
|
||||
on conflict (contact_id) do update
|
||||
set business_name = excluded.business_name
|
||||
, vatin = excluded.vatin
|
||||
, address = excluded.address
|
||||
, city = excluded.city
|
||||
, province = excluded.province
|
||||
, postal_code = excluded.postal_code
|
||||
, country_code = excluded.country_code
|
||||
;
|
||||
|
||||
insert into contact_email (contact_id, email)
|
||||
select contact_id, email::email
|
||||
from imported_contact
|
||||
where contact_id is not null
|
||||
and input_is_valid(email, 'email')
|
||||
on conflict (contact_id) do update
|
||||
set email = excluded.email
|
||||
;
|
||||
|
||||
insert into contact_web (contact_id, uri)
|
||||
select contact_id, web::uri
|
||||
from imported_contact
|
||||
where contact_id is not null
|
||||
and input_is_valid(web, 'uri')
|
||||
and length(web) > 1
|
||||
on conflict (contact_id) do update
|
||||
set uri = excluded.uri
|
||||
;
|
||||
|
||||
insert into contact_iban (contact_id, iban)
|
||||
select contact_id, iban::iban
|
||||
from imported_contact
|
||||
where contact_id is not null
|
||||
and input_is_valid(iban, 'iban')
|
||||
on conflict (contact_id) do update
|
||||
set iban = excluded.iban
|
||||
;
|
||||
|
||||
insert into contact_swift (contact_id, bic)
|
||||
select contact_id, bic::bic
|
||||
from imported_contact
|
||||
where contact_id is not null
|
||||
and input_is_valid(bic, 'bic')
|
||||
on conflict (contact_id) do update
|
||||
set bic = excluded.bic
|
||||
;
|
||||
|
||||
insert into contact_phone (contact_id, phone)
|
||||
select contact_id, parse_packed_phone_number(phone, case when country_code = '' then 'ES' else country_code end)
|
||||
from imported_contact
|
||||
where contact_id is not null
|
||||
and input_is_valid_phone(phone, case when country_code = '' then 'ES' else country_code end)
|
||||
on conflict (contact_id) do update
|
||||
set phone = excluded.phone
|
||||
;
|
||||
|
||||
select count(*) from imported_contact where contact_id is not null into imported;
|
||||
return imported;
|
||||
|
||||
drop table imported_contact;
|
||||
end
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function end_import_contacts(integer) from public;
|
||||
grant execute on function end_import_contacts(integer) to invoicer;
|
||||
grant execute on function end_import_contacts(integer) to admin;
|
||||
|
||||
commit;
|
|
@ -1,23 +0,0 @@
|
|||
-- Deploy numerus:input_is_valid to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: roles
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to public;
|
||||
|
||||
create or replace function input_is_valid(input text, domname text) returns boolean as
|
||||
$$
|
||||
begin
|
||||
begin
|
||||
execute format('select %L::%s', input, domname);
|
||||
return true;
|
||||
exception when others then
|
||||
return false;
|
||||
end;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
stable;
|
||||
|
||||
commit;
|
|
@ -1,24 +0,0 @@
|
|||
-- Deploy numerus:input_is_valid_phone to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: roles
|
||||
-- requires: extension_pg_libphonenumber
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to public;
|
||||
|
||||
create or replace function input_is_valid_phone(phone text, country text) returns boolean as
|
||||
$$
|
||||
begin
|
||||
begin
|
||||
perform parse_packed_phone_number(phone, country);
|
||||
return true;
|
||||
exception when others then
|
||||
return false;
|
||||
end;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
stable;
|
||||
|
||||
commit;
|
|
@ -1,32 +0,0 @@
|
|||
-- Deploy numerus:invoice_attachment to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: roles
|
||||
-- requires: invoice
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table invoice_attachment (
|
||||
invoice_id integer primary key references invoice,
|
||||
original_filename text not null,
|
||||
mime_type text not null,
|
||||
content bytea not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table invoice_attachment to invoicer;
|
||||
grant select, insert, update, delete on table invoice_attachment to admin;
|
||||
|
||||
alter table invoice_attachment enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on invoice_attachment
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from invoice
|
||||
where invoice.invoice_id = invoice_attachment.invoice_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,32 +0,0 @@
|
|||
-- Deploy numerus:invoice_collection to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: invoice
|
||||
-- requires: collection
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table invoice_collection (
|
||||
invoice_id integer not null references invoice,
|
||||
collection_id integer not null references collection,
|
||||
primary key (invoice_id, collection_id)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table invoice_collection to invoicer;
|
||||
grant select, insert, update, delete on table invoice_collection to admin;
|
||||
|
||||
alter table invoice_collection enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on invoice_collection
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from invoice
|
||||
where invoice.invoice_id = invoice_collection.invoice_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,13 +0,0 @@
|
|||
-- Deploy numerus:invoice_contact_id_fkey to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: invoice
|
||||
-- requires: contact_tax_details
|
||||
|
||||
begin;
|
||||
|
||||
alter table numerus.invoice
|
||||
drop constraint invoice_contact_id_fkey
|
||||
, add foreign key (contact_id) references numerus.contact_tax_details (contact_id)
|
||||
;
|
||||
|
||||
commit;
|
|
@ -1,32 +0,0 @@
|
|||
-- Deploy numerus:invoice_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: invoice
|
||||
-- requires: payment
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table invoice_payment (
|
||||
invoice_id integer not null references invoice,
|
||||
payment_id integer not null references payment,
|
||||
primary key (invoice_id, payment_id)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table invoice_payment to invoicer;
|
||||
grant select, insert, update, delete on table invoice_payment to admin;
|
||||
|
||||
alter table invoice_payment enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on invoice_payment
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from invoice
|
||||
where invoice.invoice_id = invoice_payment.invoice_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,13 +0,0 @@
|
|||
-- Deploy numerus:new_expense_amount to pg
|
||||
-- requires: schema_numerus
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create type new_expense_amount as (
|
||||
taxes text[][],
|
||||
total text
|
||||
);
|
||||
|
||||
commit;
|
|
@ -24,9 +24,6 @@ begin
|
|||
end if;
|
||||
|
||||
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
|
||||
result := result * 10;
|
||||
end loop;
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
-- Deploy numerus:parse_price to pg
|
||||
-- requires: schema_public
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function parse_price(price text, decimal_digits integer) returns integer as
|
||||
$$
|
||||
declare
|
||||
parts text[];
|
||||
result int;
|
||||
frac_part text;
|
||||
sign int := 1;
|
||||
begin
|
||||
if price like '-%' Then
|
||||
sign := -1;
|
||||
price := substring(price from 2);
|
||||
end if;
|
||||
|
||||
parts := string_to_array(price, '.');
|
||||
if array_length(parts, 1) > 2 then
|
||||
raise invalid_parameter_value using message = price || ' is not a valid price representation.';
|
||||
end if;
|
||||
|
||||
result := parts[1]::integer;
|
||||
for d in 1..decimal_digits loop
|
||||
result := result * 10;
|
||||
end loop;
|
||||
|
||||
if array_length(parts, 1) = 2 then
|
||||
frac_part := rtrim(parts[2], '0');
|
||||
if length(frac_part) > decimal_digits then
|
||||
raise invalid_parameter_value using message = price || ' has too many digits in the fraction part.';
|
||||
end if;
|
||||
frac_part := rpad(frac_part, decimal_digits, '0');
|
||||
result := result + frac_part::integer;
|
||||
end if;
|
||||
|
||||
return sign * result;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
immutable;
|
||||
|
||||
comment on function parse_price(text, integer) is
|
||||
'Converts the string representation of a price in decimal form to cents, according to the number of decimal digits passed.';
|
||||
|
||||
revoke execute on function parse_price(text, integer) from public;
|
||||
grant execute on function parse_price(text, integer) to invoicer;
|
||||
grant execute on function parse_price(text, integer) to admin;
|
||||
|
||||
commit;
|
|
@ -1,47 +0,0 @@
|
|||
-- Deploy numerus:payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: company
|
||||
-- requires: payment_account
|
||||
-- requires: currency
|
||||
-- requires: tag_name
|
||||
-- requires: payment_status
|
||||
-- requires: extension_pgcrypto
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table payment (
|
||||
payment_id integer generated by default as identity primary key,
|
||||
company_id integer not null references company,
|
||||
slug uuid not null unique default gen_random_uuid(),
|
||||
description text not null,
|
||||
payment_date date not null default current_date,
|
||||
payment_account_id integer not null references payment_account,
|
||||
amount integer not null constraint payment_amount_positive check (amount > 0),
|
||||
currency_code text not null references currency,
|
||||
tags tag_name[] not null default '{}',
|
||||
payment_status text not null default 'complete' references payment_status,
|
||||
created_at timestamptz not null default current_timestamp
|
||||
);
|
||||
|
||||
create index on payment using gin (tags);
|
||||
|
||||
grant select, insert, update, delete on table payment to invoicer;
|
||||
grant select, insert, update, delete on table payment to admin;
|
||||
|
||||
alter table payment enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on payment
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = payment.company_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,38 +0,0 @@
|
|||
-- Deploy numerus:payment_account to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: company
|
||||
-- requires: payment_account_type
|
||||
-- requires: extension_pgcrypto
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table payment_account (
|
||||
payment_account_id integer generated by default as identity primary key,
|
||||
company_id integer not null references company,
|
||||
slug uuid not null unique default gen_random_uuid(),
|
||||
payment_account_type text not null references payment_account_type,
|
||||
name text not null constraint payment_account_name_not_empty check(length(trim(name)) > 0),
|
||||
unique (payment_account_id, payment_account_type)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table payment_account to invoicer;
|
||||
grant select, insert, update, delete on table payment_account to admin;
|
||||
|
||||
alter table payment_account enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on payment_account
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = payment_account.company_id
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
commit;
|
|
@ -1,33 +0,0 @@
|
|||
-- Deploy numerus:payment_account_bank to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account
|
||||
-- requires: extension_iban
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table payment_account_bank (
|
||||
payment_account_id integer primary key,
|
||||
payment_account_type text not null default 'bank' constraint payment_account_type_is_bank check (payment_account_type = 'bank'),
|
||||
iban iban not null,
|
||||
foreign key (payment_account_id, payment_account_type) references payment_account (payment_account_id, payment_account_type)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table payment_account_bank to invoicer;
|
||||
grant select, insert, update, delete on table payment_account_bank to admin;
|
||||
|
||||
alter table payment_account_bank enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on payment_account_bank
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from payment_account
|
||||
where payment_account.payment_account_id = payment_account_bank.payment_account_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,33 +0,0 @@
|
|||
-- Deploy numerus:payment_account_card to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table payment_account_card (
|
||||
payment_account_id integer primary key,
|
||||
payment_account_type text not null default 'card' constraint payment_account_type_is_card check (payment_account_type = 'card'),
|
||||
last_four_digits text not null constraint last_four_digits_are_digits check ( last_four_digits ~ '^\d{4}$'),
|
||||
expiration_date date not null,
|
||||
foreign key (payment_account_id, payment_account_type) references payment_account (payment_account_id, payment_account_type)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table payment_account_card to invoicer;
|
||||
grant select, insert, update, delete on table payment_account_card to admin;
|
||||
|
||||
alter table payment_account_card enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on payment_account_card
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from payment_account
|
||||
where payment_account.payment_account_id = payment_account_card.payment_account_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,17 +0,0 @@
|
|||
-- Deploy numerus:payment_account_type to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table payment_account_type (
|
||||
payment_account_type text primary key,
|
||||
name text not null
|
||||
);
|
||||
|
||||
grant select on table payment_account_type to invoicer;
|
||||
grant select on table payment_account_type to admin;
|
||||
|
||||
commit;
|
|
@ -1,21 +0,0 @@
|
|||
-- Deploy numerus:payment_account_type_i18n to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_account_type
|
||||
-- requires: language
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table payment_account_type_i18n (
|
||||
payment_account_type text not null references payment_account_type,
|
||||
lang_tag text not null references language,
|
||||
name text not null,
|
||||
primary key (payment_account_type, lang_tag)
|
||||
);
|
||||
|
||||
grant select on table payment_account_type_i18n to invoicer;
|
||||
grant select on table payment_account_type_i18n to admin;
|
||||
|
||||
commit;
|
|
@ -1,33 +0,0 @@
|
|||
-- Deploy numerus:payment_attachment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table payment_attachment (
|
||||
payment_id integer primary key references payment,
|
||||
original_filename text not null,
|
||||
mime_type text not null,
|
||||
content bytea not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table payment_attachment to invoicer;
|
||||
grant select, insert, update, delete on table payment_attachment to admin;
|
||||
|
||||
alter table payment_attachment enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on payment_attachment
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from payment
|
||||
where payment.payment_id = payment_attachment.payment_id
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
commit;
|
|
@ -1,17 +0,0 @@
|
|||
-- Deploy numerus:payment_status to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table payment_status (
|
||||
payment_status text primary key,
|
||||
name text not null
|
||||
);
|
||||
|
||||
grant select on table payment_status to invoicer;
|
||||
grant select on table payment_status to admin;
|
||||
|
||||
commit;
|
|
@ -1,21 +0,0 @@
|
|||
-- Deploy numerus:payment_status_i18n to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: payment_status
|
||||
-- requires: language
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table payment_status_i18n (
|
||||
payment_status text not null references payment_status,
|
||||
lang_tag text not null references language,
|
||||
name text not null,
|
||||
primary key (payment_status, lang_tag)
|
||||
);
|
||||
|
||||
grant select on table payment_status_i18n to invoicer;
|
||||
grant select on table payment_status_i18n to admin;
|
||||
|
||||
commit;
|
|
@ -1,40 +0,0 @@
|
|||
-- Deploy numerus:remove_collection to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: invoice_payment
|
||||
-- requires: payment
|
||||
-- requires: payment_attachment
|
||||
-- requires: update_invoice_collection_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function remove_collection(collection_slug uuid) returns void as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
iid integer;
|
||||
begin
|
||||
select payment_id into cid from payment where slug = collection_slug;
|
||||
if not found then
|
||||
return;
|
||||
end if;
|
||||
|
||||
delete from invoice_payment where payment_id = cid returning invoice_id into iid;
|
||||
if iid is not null then
|
||||
perform update_invoice_collection_status(null, iid, 0);
|
||||
end if;
|
||||
|
||||
delete from payment_attachment where payment_id = cid;
|
||||
delete from payment where payment_id = cid;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function remove_collection(uuid) from public;
|
||||
grant execute on function remove_collection(uuid) to invoicer;
|
||||
grant execute on function remove_collection(uuid) to admin;
|
||||
|
||||
commit;
|
|
@ -1,40 +0,0 @@
|
|||
-- Deploy numerus:remove_collection to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: invoice_collection
|
||||
-- requires: collection
|
||||
-- requires: collection_attachment
|
||||
-- requires: update_invoice_collection_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function remove_collection(collection_slug uuid) returns void as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
iid integer;
|
||||
begin
|
||||
select collection_id into cid from collection where slug = collection_slug;
|
||||
if not found then
|
||||
return;
|
||||
end if;
|
||||
|
||||
delete from invoice_collection where collection_id = cid returning invoice_id into iid;
|
||||
if iid is not null then
|
||||
perform update_invoice_collection_status(null, iid, 0);
|
||||
end if;
|
||||
|
||||
delete from collection_attachment where collection_id = cid;
|
||||
delete from collection where collection_id = cid;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function remove_collection(uuid) from public;
|
||||
grant execute on function remove_collection(uuid) to invoicer;
|
||||
grant execute on function remove_collection(uuid) to admin;
|
||||
|
||||
commit;
|
|
@ -1,34 +0,0 @@
|
|||
-- Deploy numerus:remove_expense to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense_tax
|
||||
-- requires: expense_attachment
|
||||
-- requires: expense
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function remove_expense(expense_slug uuid) returns void as
|
||||
$$
|
||||
declare
|
||||
eid integer;
|
||||
begin
|
||||
select expense_id into eid from expense where slug = expense_slug;
|
||||
if not found then
|
||||
return;
|
||||
end if;
|
||||
|
||||
delete from expense_tax where expense_id = eid;
|
||||
delete from expense_attachment where expense_id = eid;
|
||||
delete from expense where expense_id = eid;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function remove_expense(uuid) from public;
|
||||
grant execute on function remove_expense(uuid) to invoicer;
|
||||
grant execute on function remove_expense(uuid) to admin;
|
||||
|
||||
commit;
|
|
@ -1,40 +0,0 @@
|
|||
-- Deploy numerus:remove_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense_payment
|
||||
-- requires: payment
|
||||
-- requires: payment_attachment
|
||||
-- requires: update_expense_payment_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function remove_payment(payment_slug uuid) returns void as
|
||||
$$
|
||||
declare
|
||||
pid integer;
|
||||
eid integer;
|
||||
begin
|
||||
select payment_id into pid from payment where slug = payment_slug;
|
||||
if not found then
|
||||
return;
|
||||
end if;
|
||||
|
||||
delete from expense_payment where payment_id = pid returning expense_id into eid;
|
||||
if eid is not null then
|
||||
perform update_expense_payment_status(null, eid, 0);
|
||||
end if;
|
||||
|
||||
delete from payment_attachment where payment_id = pid;
|
||||
delete from payment where payment_id = pid;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function remove_payment(uuid) from public;
|
||||
grant execute on function remove_payment(uuid) to invoicer;
|
||||
grant execute on function remove_payment(uuid) to admin;
|
||||
|
||||
commit;
|
|
@ -1,20 +0,0 @@
|
|||
-- Deploy numerus:tax_details to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: extension_vat
|
||||
-- requires: country_code
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create type tax_details as (
|
||||
business_name text,
|
||||
vatin text,
|
||||
address text,
|
||||
city text,
|
||||
province text,
|
||||
postal_code text,
|
||||
country_code country_code
|
||||
);
|
||||
|
||||
commit;
|
|
@ -1,53 +0,0 @@
|
|||
-- Deploy numerus:update_expense_payment_status to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense
|
||||
-- requires: payment
|
||||
-- requires: expense_payment
|
||||
-- requires: expense_tax_amount
|
||||
-- requires: available_expense_status
|
||||
-- requires: available_payment_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function update_expense_payment_status(pid integer, eid integer, amount_cents integer) returns void as
|
||||
$$
|
||||
update payment
|
||||
set payment_status = case
|
||||
when (expense.amount > 0 and expense.amount + coalesce(tax.amount, 0) > -amount_cents) or (expense.amount < 0 and expense.amount + coalesce(tax.amount, 0) < -amount_cents) or exists (select 1 from expense_payment as ep where ep.expense_id = expense.expense_id and payment_id <> pid) then 'partial'
|
||||
else 'complete' end
|
||||
from expense
|
||||
left join ( select expense_id, sum(amount) as amount from expense_tax_amount group by expense_id) as tax using (expense_id)
|
||||
where expense.expense_id = eid
|
||||
and payment_id = pid
|
||||
;
|
||||
|
||||
update expense
|
||||
set expense_status = case
|
||||
when (expense.amount > 0 and -paid_amount >= expense.amount + tax_amount) or (expense.amount < 0 and -paid_amount <= expense.amount + tax_amount) then 'paid'
|
||||
when paid_amount = 0 then 'pending'
|
||||
else 'partial' end
|
||||
from (
|
||||
select coalesce (sum(payment.amount), 0) as paid_amount
|
||||
from expense_payment
|
||||
join payment using (payment_id)
|
||||
where expense_payment.expense_id = eid
|
||||
) as payment,
|
||||
(
|
||||
select coalesce (sum(amount), 0) as tax_amount
|
||||
from expense_tax_amount
|
||||
where expense_id = eid
|
||||
) as tax
|
||||
where expense.expense_id = eid
|
||||
;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function update_expense_payment_status(integer, integer, integer) from public;
|
||||
grant execute on function update_expense_payment_status(integer, integer, integer) to invoicer;
|
||||
grant execute on function update_expense_payment_status(integer, integer, integer) to admin;
|
||||
|
||||
commit;
|
|
@ -1,51 +0,0 @@
|
|||
-- Deploy numerus:update_expense_payment_status to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: expense
|
||||
-- requires: payment
|
||||
-- requires: expense_payment
|
||||
-- requires: expense_tax_amount
|
||||
-- requires: available_expense_status
|
||||
-- requires: available_payment_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function update_expense_payment_status(pid integer, eid integer, amount_cents integer) returns void as
|
||||
$$
|
||||
update payment
|
||||
set payment_status = case when expense.amount + coalesce(tax.amount, 0) > amount_cents or exists (select 1 from expense_payment as ep where ep.expense_id = expense.expense_id and payment_id <> pid) then 'partial' else 'complete' end
|
||||
from expense
|
||||
left join ( select expense_id, sum(amount) as amount from expense_tax_amount group by expense_id) as tax using (expense_id)
|
||||
where expense.expense_id = eid
|
||||
and payment_id = pid
|
||||
;
|
||||
|
||||
update expense
|
||||
set expense_status = case
|
||||
when paid_amount >= expense.amount + tax_amount then 'paid'
|
||||
when paid_amount = 0 then 'pending'
|
||||
else 'partial' end
|
||||
from (
|
||||
select coalesce (sum(payment.amount), 0) as paid_amount
|
||||
from expense_payment
|
||||
join payment using (payment_id)
|
||||
where expense_payment.expense_id = eid
|
||||
) as payment,
|
||||
(
|
||||
select coalesce (sum(amount), 0) as tax_amount
|
||||
from expense_tax_amount
|
||||
where expense_id = eid
|
||||
) as tax
|
||||
where expense.expense_id = eid
|
||||
;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function update_expense_payment_status(integer, integer, integer) from public;
|
||||
grant execute on function update_expense_payment_status(integer, integer, integer) to invoicer;
|
||||
grant execute on function update_expense_payment_status(integer, integer, integer) to admin;
|
||||
|
||||
commit;
|
|
@ -1,51 +0,0 @@
|
|||
-- Deploy numerus:update_invoice_collection_status to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: invoice
|
||||
-- requires: payment
|
||||
-- requires: invoice_payment
|
||||
-- requires: invoice_amount
|
||||
-- requires: available_invoice_status
|
||||
-- requires: available_payment_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function update_invoice_collection_status(cid integer, iid integer, amount_cents integer) returns void as
|
||||
$$
|
||||
update payment
|
||||
set payment_status = case
|
||||
when (invoice_amount.total > 0 and invoice_amount.total > amount_cents) or (invoice_amount.total < 0 and invoice_amount.total < amount_cents) or exists (select 1 from invoice_payment as ic where ic.invoice_id = invoice_amount.invoice_id and payment_id <> cid) then 'partial'
|
||||
else 'complete' end
|
||||
from invoice_amount
|
||||
where invoice_id = iid
|
||||
and payment_id = cid
|
||||
;
|
||||
|
||||
update invoice
|
||||
set invoice_status = case
|
||||
when (total_amount > 0 and collected_amount >= total_amount) or (total_amount < 0 and collected_amount <= total_amount) then 'paid'
|
||||
when collected_amount = 0 then 'created'
|
||||
else 'partial' end
|
||||
from (
|
||||
select coalesce(sum(payment.amount), 0) as collected_amount
|
||||
from invoice_payment
|
||||
join payment using (payment_id)
|
||||
where invoice_payment.invoice_id = iid
|
||||
) as collection,
|
||||
(
|
||||
select total as total_amount
|
||||
from invoice_amount
|
||||
where invoice_id = iid
|
||||
) as amount
|
||||
where invoice.invoice_id = iid;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function update_invoice_collection_status(integer, integer, integer) from public;
|
||||
grant execute on function update_invoice_collection_status(integer, integer, integer) to invoicer;
|
||||
grant execute on function update_invoice_collection_status(integer, integer, integer) to admin;
|
||||
|
||||
commit;
|
|
@ -1,49 +0,0 @@
|
|||
-- Deploy numerus:update_invoice_collection_status to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_numerus
|
||||
-- requires: invoice
|
||||
-- requires: collection
|
||||
-- requires: invoice_collection
|
||||
-- requires: invoice_amount
|
||||
-- requires: available_invoice_status
|
||||
-- requires: available_payment_status
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function update_invoice_collection_status(cid integer, iid integer, amount_cents integer) returns void as
|
||||
$$
|
||||
update collection
|
||||
set payment_status = case when invoice_amount.total > amount_cents or exists (select 1 from invoice_collection as ic where ic.collection_id = invoice_amount.invoice_id and collection_id <> cid) then 'partial' else 'complete' end
|
||||
from invoice_amount
|
||||
where invoice_id = iid
|
||||
and collection_id = cid
|
||||
;
|
||||
|
||||
update invoice
|
||||
set invoice_status = case
|
||||
when collected_amount >= total_amount then 'paid'
|
||||
when collected_amount = 0 then 'created'
|
||||
else 'partial' end
|
||||
from (
|
||||
select coalesce(sum(collection.amount), 0) as collected_amount
|
||||
from invoice_collection
|
||||
join collection using (collection_id)
|
||||
where invoice_collection.invoice_id = iid
|
||||
) as collection,
|
||||
(
|
||||
select total as total_amount
|
||||
from invoice_amount
|
||||
where invoice_id = iid
|
||||
) as amount
|
||||
where invoice.invoice_id = iid;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function update_invoice_collection_status(integer, integer, integer) from public;
|
||||
grant execute on function update_invoice_collection_status(integer, integer, integer) to invoicer;
|
||||
grant execute on function update_invoice_collection_status(integer, integer, integer) to admin;
|
||||
|
||||
commit;
|
19
go.mod
19
go.mod
|
@ -1,24 +1,15 @@
|
|||
module dev.tandem.ws/tandem/numerus
|
||||
|
||||
go 1.19
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/jackc/pgtype v1.10.0
|
||||
github.com/jackc/pgx/v4 v4.15.0
|
||||
github.com/jackc/pgx/v4 v4.17.2
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/leonelquinteros/gotext v1.5.0
|
||||
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
|
||||
github.com/leonelquinteros/gotext v1.5.1
|
||||
golang.org/x/text v0.3.8
|
||||
)
|
||||
|
||||
require (
|
||||
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
|
||||
github.com/jackc/pgtype v1.12.0
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
|
||||
)
|
||||
|
|
45
go.sum
45
go.sum
|
@ -14,6 +14,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
|
|||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
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/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
|
@ -24,7 +25,6 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
|
|||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.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.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
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/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
|
@ -35,6 +35,7 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W
|
|||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
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/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=
|
||||
|
@ -42,7 +43,6 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
|
|||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.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.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/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
|
@ -51,18 +51,17 @@ github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01C
|
|||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-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.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
|
||||
github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
|
||||
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-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.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
|
||||
github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
|
||||
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
|
||||
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/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/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
|
@ -70,14 +69,12 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
|
|||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
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.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/leonelquinteros/gotext v1.5.0 h1:ODY7LzLpZWWSJdAHnzhreOr6cwLXTAmc914FOauSkBM=
|
||||
github.com/leonelquinteros/gotext v1.5.0/go.mod h1:OCiUVHuhP9LGFBQ1oAmdtNCHJCiHiQA8lf4nAifHkr0=
|
||||
github.com/leonelquinteros/gotext v1.5.1 h1:vmddRn3gHp67YFjZLZE2AZsgYMT4IBTJhua4yfe7/4Q=
|
||||
github.com/leonelquinteros/gotext v1.5.1/go.mod h1:/A4Y7BvIsf5JHO60E43ZQDVkV3qO+7eP8HjeqD6ChIA=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
|
@ -92,8 +89,6 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
|
@ -116,8 +111,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/tealeg/xlsx v0.0.0-20181024002044-dbf71b6a931e h1:0AoAjM/7iqEZwTsWhk3nm9+H5mocFnh6dCGUaIOSTDQ=
|
||||
github.com/tealeg/xlsx v0.0.0-20181024002044-dbf71b6a931e/go.mod h1:uxu5UY2ovkuRPWKQ8Q7JG0JbSivrISjdPzZQKeo74mA=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
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=
|
||||
|
@ -139,19 +134,26 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-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-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-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/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.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-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-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-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-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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -165,16 +167,20 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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-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-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.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.4/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.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
|
@ -182,15 +188,16 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw
|
|||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-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-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-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-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-20191011141410-1b5146add898/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 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/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=
|
||||
|
|
257
guix.scm
257
guix.scm
|
@ -1,257 +0,0 @@
|
|||
(use-modules (gnu packages)
|
||||
(gnu packages base)
|
||||
(gnu packages compression)
|
||||
(gnu packages databases)
|
||||
(gnu packages geo)
|
||||
(gnu packages messaging)
|
||||
(gnu packages perl)
|
||||
(gnu packages perl-check)
|
||||
(gnu packages pkg-config)
|
||||
(gnu packages protobuf)
|
||||
(gnu packages web)
|
||||
(gnu packages xml)
|
||||
(guix build-system gnu)
|
||||
(guix build-system perl)
|
||||
(guix download)
|
||||
(guix git-download)
|
||||
(guix licenses)
|
||||
(guix packages))
|
||||
|
||||
(define vat
|
||||
(package
|
||||
(name "vat")
|
||||
(version "0.3")
|
||||
(source (origin
|
||||
(method url-fetch)
|
||||
(uri (string-append
|
||||
"https://dev.tandem.ws/tandem/" name "/archive/v" version ".tar.gz"))
|
||||
(sha256
|
||||
(base32
|
||||
"0jbgakz7ip09hrnbbg1dm02n5zx7sv0magvw7s6g7rbbpy6wpqwh"))))
|
||||
(build-system gnu-build-system)
|
||||
(arguments
|
||||
`(#:tests? #f
|
||||
#:make-flags
|
||||
(list (string-append "datadir=" (assoc-ref %outputs "out") "/share")
|
||||
(string-append "docdir="(assoc-ref %outputs "out") "/share/doc")
|
||||
(string-append "pkglibdir="(assoc-ref %outputs "out") "/lib")
|
||||
(string-append "bindir=" (assoc-ref %outputs "out") "/bin"))
|
||||
#:phases
|
||||
(modify-phases %standard-phases
|
||||
(delete 'configure))))
|
||||
(inputs
|
||||
`(("postgresql" ,postgresql-15)))
|
||||
(home-page "https://dev.tandem.ws/tandem/vat/")
|
||||
(synopsis "VAT identification numbers for PostgreSQL")
|
||||
(description "VAT identification numbers library for PostgreSQL")
|
||||
(license (x11-style "https://www.postgresql.org/about/licence/"))))
|
||||
|
||||
(define pg-libphonenumber
|
||||
(let ((commit "753e2fa4be452620455a099aeda917648f2da70a")
|
||||
(revision "1"))
|
||||
(package
|
||||
(name "pg-libphonenumber")
|
||||
(version (git-version "0.1.0" revision commit))
|
||||
(source (origin
|
||||
(method git-fetch)
|
||||
(uri (git-reference
|
||||
(url "https://github.com/blm768/pg-libphonenumber")
|
||||
(commit commit)))
|
||||
(file-name (git-file-name name version))
|
||||
(sha256
|
||||
(base32
|
||||
"01syw93giq0pz80qzrdr79cr4p6w8lx129y1gsn2avc97r7pqanj"))))
|
||||
(build-system gnu-build-system)
|
||||
(arguments
|
||||
`(#:tests? #f
|
||||
#:make-flags
|
||||
(list (string-append "datadir=" (assoc-ref %outputs "out") "/share")
|
||||
(string-append "docdir="(assoc-ref %outputs "out") "/share/doc")
|
||||
(string-append "pkglibdir="(assoc-ref %outputs "out") "/lib")
|
||||
(string-append "bindir=" (assoc-ref %outputs "out") "/bin"))
|
||||
#:phases
|
||||
(modify-phases %standard-phases
|
||||
(delete 'configure))))
|
||||
(inputs
|
||||
`(("postgresql" ,postgresql-15)
|
||||
("libphonenumber" ,libphonenumber)
|
||||
("protobuf" ,protobuf)))
|
||||
(home-page "https://github.com/blm768/pg-libphonenumber")
|
||||
(synopsis "PostgreSQL extension for libphonenumber")
|
||||
(description "pg_libphonenumber is a (partially implemented!) PostgreSQL extension that provides access to 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+))
|
338
pkg/accounts.go
338
pkg/accounts.go
|
@ -1,338 +0,0 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jackc/pgtype"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ExpirationDateFormat = "01/06"
|
||||
AccountTypeBank = "bank"
|
||||
AccountTypeCard = "card"
|
||||
AccountTypeCash = "cash"
|
||||
AccountTypeOther = "other"
|
||||
)
|
||||
|
||||
func servePaymentAccountIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
locale := getLocale(r)
|
||||
|
||||
page := NewPaymentAccountIndexPage(r.Context(), conn, company, locale)
|
||||
page.MustRender(w, r)
|
||||
}
|
||||
|
||||
type PaymentAccountIndexPage struct {
|
||||
Accounts []*PaymentAccountEntry
|
||||
}
|
||||
|
||||
func NewPaymentAccountIndexPage(ctx context.Context, conn *Conn, company *Company, locale *Locale) *PaymentAccountIndexPage {
|
||||
return &PaymentAccountIndexPage{
|
||||
Accounts: mustCollectPaymentAccountEntries(ctx, conn, company, locale),
|
||||
}
|
||||
}
|
||||
|
||||
func (page *PaymentAccountIndexPage) MustRender(w http.ResponseWriter, r *http.Request) {
|
||||
mustRenderMainTemplate(w, r, "payments/accounts/index.gohtml", page)
|
||||
}
|
||||
|
||||
type PaymentAccountEntry struct {
|
||||
ID int
|
||||
Slug string
|
||||
Type string
|
||||
TypeLabel string
|
||||
Name string
|
||||
IBAN string
|
||||
LastFourDigits string
|
||||
ExpirationDate string
|
||||
}
|
||||
|
||||
func mustCollectPaymentAccountEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale) []*PaymentAccountEntry {
|
||||
rows := conn.MustQuery(ctx, `
|
||||
select payment_account_id
|
||||
, slug
|
||||
, payment_account.payment_account_type
|
||||
, coalesce(i18n.name, payment_account_type.name)
|
||||
, payment_account.name
|
||||
, coalesce(iban::text, '') as iban
|
||||
, coalesce(last_four_digits, '') as last_four_digits
|
||||
, expiration_date
|
||||
from payment_account
|
||||
left join payment_account_bank using (payment_account_id, payment_account_type)
|
||||
left join payment_account_card using (payment_account_id, payment_account_type)
|
||||
join payment_account_type using (payment_account_type)
|
||||
left join payment_account_type_i18n as i18n on payment_account_type.payment_account_type = i18n.payment_account_type and i18n.lang_tag = $1
|
||||
where company_id = $2
|
||||
order by payment_account_id
|
||||
`, locale.Language.String(), company.Id)
|
||||
defer rows.Close()
|
||||
|
||||
var entries []*PaymentAccountEntry
|
||||
for rows.Next() {
|
||||
entry := &PaymentAccountEntry{}
|
||||
var expirationDate pgtype.Date
|
||||
if err := rows.Scan(&entry.ID, &entry.Slug, &entry.Type, &entry.TypeLabel, &entry.Name, &entry.IBAN, &entry.LastFourDigits, &expirationDate); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if expirationDate.Status == pgtype.Present {
|
||||
entry.ExpirationDate = expirationDate.Time.Format(ExpirationDateFormat)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
panic(rows.Err())
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func servePaymentAccountForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
locale := getLocale(r)
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
form := newPaymentAccountForm(r.Context(), conn, locale, company)
|
||||
slug := params[0].Value
|
||||
if slug == "new" {
|
||||
form.MustRender(w, r)
|
||||
return
|
||||
}
|
||||
if !ValidUuid(slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
form.MustRender(w, r)
|
||||
}
|
||||
|
||||
type PaymentAccountForm struct {
|
||||
locale *Locale
|
||||
company *Company
|
||||
Slug string
|
||||
Type *RadioField
|
||||
Name *InputField
|
||||
IBAN *InputField
|
||||
LastFourDigits *InputField
|
||||
ExpirationMonthYear *InputField
|
||||
}
|
||||
|
||||
func newPaymentAccountForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *PaymentAccountForm {
|
||||
return &PaymentAccountForm{
|
||||
locale: locale,
|
||||
company: company,
|
||||
Type: &RadioField{
|
||||
Name: "payment_account_type",
|
||||
Label: pgettext("input", "Type", locale),
|
||||
Required: true,
|
||||
Options: MustGetRadioOptions(ctx, conn, "select payment_account_type, i18n.name from payment_account_type join payment_account_type_i18n as i18n using (payment_account_type) where i18n.lang_tag = $1 order by payment_account_type", locale.Language.String()),
|
||||
Attributes: []template.HTMLAttr{
|
||||
`x-model="type"`,
|
||||
},
|
||||
},
|
||||
Name: &InputField{
|
||||
Name: "name",
|
||||
Label: pgettext("input", "Name", locale),
|
||||
Required: true,
|
||||
Type: "text",
|
||||
},
|
||||
IBAN: &InputField{
|
||||
Name: "iban",
|
||||
Label: pgettext("input", "IBAN", locale),
|
||||
Required: true,
|
||||
Type: "text",
|
||||
},
|
||||
LastFourDigits: &InputField{
|
||||
Name: "last_four_digits",
|
||||
Label: pgettext("input", "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"))
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func serveAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params, sql string) {
|
||||
slug := params[0].Value
|
||||
if !ValidUuid(slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
conn := getConn(r)
|
||||
var contentType string
|
||||
var content []byte
|
||||
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), sql, slug).Scan(&contentType, &content)) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(content)), 10))
|
||||
_, _ = w.Write(content)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package pkg
|
||||
|
||||
const Version = "1.4~git"
|
885
pkg/company.go
885
pkg/company.go
File diff suppressed because it is too large
Load Diff
330
pkg/contacts.go
330
pkg/contacts.go
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/tealeg/xlsx"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -42,21 +41,13 @@ func IndexContacts(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
|
|||
func GetContactForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
locale := getLocale(r)
|
||||
conn := getConn(r)
|
||||
slug := params[0].Value
|
||||
if slug == "import" {
|
||||
ServeImportPage(w, r, params)
|
||||
return
|
||||
}
|
||||
form := newContactForm(r.Context(), conn, locale)
|
||||
slug := params[0].Value
|
||||
if slug == "new" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
mustRenderNewContactForm(w, r, form)
|
||||
return
|
||||
}
|
||||
if !ValidUuid(slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
|
@ -78,7 +69,7 @@ type editContactPage struct {
|
|||
func mustRenderEditContactForm(w http.ResponseWriter, r *http.Request, slug string, form *contactForm) {
|
||||
page := &editContactPage{
|
||||
Slug: slug,
|
||||
ContactName: form.Name.String(),
|
||||
ContactName: form.BusinessName.Val,
|
||||
Form: form,
|
||||
}
|
||||
mustRenderMainTemplate(w, r, "contacts/edit.gohtml", page)
|
||||
|
@ -97,12 +88,14 @@ func HandleAddContact(w http.ResponseWriter, r *http.Request, _ httprouter.Param
|
|||
return
|
||||
}
|
||||
if !form.Validate(r.Context(), conn) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
if !IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
mustRenderNewContactForm(w, r, form)
|
||||
return
|
||||
}
|
||||
company := mustGetCompany(r)
|
||||
conn.MustExec(r.Context(), "select add_contact($1, $2, $3, $4, $5, $6, $7, $8, $9)", company.Id, form.Name, form.Phone, form.Email, form.Web, form.TaxDetails(), form.IBAN, form.BIC, form.Tags)
|
||||
conn.MustExec(r.Context(), "select add_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", company.Id, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Tags)
|
||||
htmxRedirect(w, r, companyURI(company, "/contacts"))
|
||||
}
|
||||
|
||||
|
@ -122,12 +115,7 @@ func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprout
|
|||
mustRenderEditContactForm(w, r, params[0].Value, form)
|
||||
return
|
||||
}
|
||||
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)
|
||||
slug := conn.MustGetText(r.Context(), "", "select edit_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", params[0].Value, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Tags)
|
||||
if slug == "" {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
@ -179,11 +167,6 @@ func (form *contactFilterForm) Parse(r *http.Request) error {
|
|||
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 {
|
||||
args := []interface{}{company.Id}
|
||||
where := []string{"contact.company_id = $1"}
|
||||
|
@ -194,7 +177,7 @@ func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company
|
|||
if filters != nil {
|
||||
name := strings.TrimSpace(filters.Name.String())
|
||||
if name != "" {
|
||||
appendWhere("contact.name ilike $%d", "%"+name+"%")
|
||||
appendWhere("contact.business_name ilike $%d", "%"+name+"%")
|
||||
}
|
||||
if len(filters.Tags.Tags) > 0 {
|
||||
if filters.TagsCondition.Selected == "and" {
|
||||
|
@ -206,15 +189,13 @@ func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company
|
|||
}
|
||||
rows := conn.MustQuery(ctx, fmt.Sprintf(`
|
||||
select slug
|
||||
, name
|
||||
, coalesce(email::text, '')
|
||||
, coalesce(phone::text, '')
|
||||
, business_name
|
||||
, email
|
||||
, phone
|
||||
, tags
|
||||
from contact
|
||||
left join contact_email using (contact_id)
|
||||
left join contact_phone using (contact_id)
|
||||
where (%s)
|
||||
order by name
|
||||
order by business_name
|
||||
`, strings.Join(where, ") AND (")), args...)
|
||||
defer rows.Close()
|
||||
|
||||
|
@ -234,61 +215,24 @@ func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company
|
|||
}
|
||||
|
||||
type contactForm struct {
|
||||
locale *Locale
|
||||
Name *InputField
|
||||
HasTaxDetails *CheckField
|
||||
BusinessName *InputField
|
||||
VATIN *InputField
|
||||
Phone *InputField
|
||||
Email *InputField
|
||||
Web *InputField
|
||||
Address *InputField
|
||||
City *InputField
|
||||
Province *InputField
|
||||
PostalCode *InputField
|
||||
Country *SelectField
|
||||
IBAN *InputField
|
||||
BIC *InputField
|
||||
Tags *TagsField
|
||||
locale *Locale
|
||||
BusinessName *InputField
|
||||
VATIN *InputField
|
||||
TradeName *InputField
|
||||
Phone *InputField
|
||||
Email *InputField
|
||||
Web *InputField
|
||||
Address *InputField
|
||||
City *InputField
|
||||
Province *InputField
|
||||
PostalCode *InputField
|
||||
Country *SelectField
|
||||
Tags *TagsField
|
||||
}
|
||||
|
||||
func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactForm {
|
||||
return &contactForm{
|
||||
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{
|
||||
Name: "business_name",
|
||||
Label: pgettext("input", "Business name", locale),
|
||||
|
@ -305,6 +249,37 @@ func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactFor
|
|||
Type: "text",
|
||||
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{
|
||||
Name: "address",
|
||||
Label: pgettext("input", "Address", locale),
|
||||
|
@ -345,16 +320,6 @@ func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactFor
|
|||
`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{
|
||||
Name: "tags",
|
||||
Label: pgettext("input", "Tags", locale),
|
||||
|
@ -366,10 +331,9 @@ func (form *contactForm) Parse(r *http.Request) error {
|
|||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
form.Name.FillValue(r)
|
||||
form.HasTaxDetails.FillValue(r)
|
||||
form.BusinessName.FillValue(r)
|
||||
form.VATIN.FillValue(r)
|
||||
form.TradeName.FillValue(r)
|
||||
form.Phone.FillValue(r)
|
||||
form.Email.FillValue(r)
|
||||
form.Web.FillValue(r)
|
||||
|
@ -378,8 +342,6 @@ func (form *contactForm) Parse(r *http.Request) error {
|
|||
form.Province.FillValue(r)
|
||||
form.PostalCode.FillValue(r)
|
||||
form.Country.FillValue(r)
|
||||
form.IBAN.FillValue(r)
|
||||
form.BIC.FillValue(r)
|
||||
form.Tags.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
@ -387,79 +349,54 @@ func (form *contactForm) Parse(r *http.Request) error {
|
|||
func (form *contactForm) Validate(ctx context.Context, conn *Conn) bool {
|
||||
validator := newFormValidator()
|
||||
|
||||
country := "ES"
|
||||
if form.HasTaxDetails.Checked {
|
||||
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))
|
||||
}
|
||||
country := ""
|
||||
if validator.CheckValidSelectOption(form.Country, gettext("Selected country is not valid.", form.locale)) {
|
||||
country = form.Country.Selected[0]
|
||||
}
|
||||
|
||||
if validator.CheckRequiredInput(form.Name, gettext("Name can not be empty.", form.locale)) {
|
||||
validator.CheckInputMinLength(form.Name, 2, gettext("Name must have at least two letters.", form.locale))
|
||||
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(form.VATIN, country, gettext("This value is not a valid VAT 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.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.Email.Val != "" {
|
||||
if validator.CheckRequiredInput(form.Email, gettext("Email can not be empty.", 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 != "" {
|
||||
validator.CheckValidURL(form.Web, gettext("This value is not a valid web address. It should be like https://domain.com/.", form.locale))
|
||||
}
|
||||
if form.IBAN.Val != "" {
|
||||
validator.CheckValidIBANInput(ctx, conn, form.IBAN, gettext("This values is not a valid IBAN.", 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))
|
||||
}
|
||||
if form.BIC.Val != "" {
|
||||
validator.CheckValidBICInput(ctx, conn, form.BIC, gettext("This values is not a valid BIC.", form.locale))
|
||||
}
|
||||
|
||||
return validator.AllOK()
|
||||
}
|
||||
|
||||
func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
|
||||
return !notFoundErrorOrPanic(conn.QueryRow(ctx, `
|
||||
select name
|
||||
, vatin is not null
|
||||
, business_name
|
||||
select business_name
|
||||
, substr(vatin::text, 3)
|
||||
, trade_name
|
||||
, phone
|
||||
, email
|
||||
, uri
|
||||
, web
|
||||
, address
|
||||
, city
|
||||
, province
|
||||
, postal_code
|
||||
, country_code
|
||||
, iban
|
||||
, bic
|
||||
, tags
|
||||
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
|
||||
`, slug).Scan(
|
||||
form.Name,
|
||||
form.HasTaxDetails,
|
||||
form.BusinessName,
|
||||
form.VATIN,
|
||||
form.TradeName,
|
||||
form.Phone,
|
||||
form.Email,
|
||||
form.Web,
|
||||
|
@ -468,72 +405,28 @@ func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
|
|||
form.Province,
|
||||
form.PostalCode,
|
||||
form.Country,
|
||||
form.IBAN,
|
||||
form.BIC,
|
||||
form.Tags))
|
||||
}
|
||||
|
||||
func (form *contactForm) TaxDetails() *CustomerTaxDetails {
|
||||
if !form.HasTaxDetails.Checked {
|
||||
return nil
|
||||
}
|
||||
return &CustomerTaxDetails{
|
||||
BusinessName: form.BusinessName.String(),
|
||||
VATIN: form.VATIN.String(),
|
||||
Address: form.Address.String(),
|
||||
City: form.City.String(),
|
||||
Province: form.Province.String(),
|
||||
PostalCode: form.PostalCode.String(),
|
||||
CountryCode: form.Country.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func ServeEditContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
serveTagsEditForm(w, r, params, "/contacts/", "select tags from contact where slug = $1")
|
||||
conn := getConn(r)
|
||||
locale := getLocale(r)
|
||||
company := getCompany(r)
|
||||
slug := params[0].Value
|
||||
form := newTagsForm(companyURI(company, "/contacts/"+slug+"/tags"), slug, locale)
|
||||
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from contact where slug = $1`, form.Slug).Scan(form.Tags)) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
|
||||
}
|
||||
|
||||
func HandleUpdateContactTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
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)
|
||||
company := mustGetCompany(r)
|
||||
form := newContactImportForm(locale, company)
|
||||
conn := getConn(r)
|
||||
company := getCompany(r)
|
||||
slug := params[0].Value
|
||||
form := newTagsForm(companyURI(company, "/contacts/"+slug+"/tags/edit"), slug, locale)
|
||||
if err := form.Parse(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
|
@ -542,45 +435,8 @@ func HandleImportContacts(w http.ResponseWriter, r *http.Request, _ httprouter.P
|
|||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
workbook, err := xlsx.OpenBinary(form.File.Content)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if conn.MustGetText(r.Context(), "", "update contact set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
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"))
|
||||
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
|
|||
rows := conn.MustQuery(r.Context(), fmt.Sprintf(`
|
||||
select to_price(0, decimal_digits) as sales
|
||||
, to_price(coalesce(invoice.total, 0), decimal_digits) as income
|
||||
, to_price(coalesce(expense.total, 0) + coalesce(expense_tax.vat, 0) + coalesce(expense_tax.irpf, 0), decimal_digits) as expenses
|
||||
, to_price(coalesce(expense.total, 0), decimal_digits) as expenses
|
||||
, to_price(coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0), decimal_digits) as vat
|
||||
, to_price(coalesce(invoice_tax.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
|
||||
|
@ -201,21 +201,9 @@ func buildDashboardChart(ctx context.Context, conn *Conn, locale *Locale, compan
|
|||
) as invoice
|
||||
left join (
|
||||
select to_char(date.invoice_date, '%[3]s')::integer as date
|
||||
, sum(subtotal + taxes)::integer as total
|
||||
, sum(amount)::integer as total
|
||||
from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date)
|
||||
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
|
||||
left join expense on expense.invoice_date = date.invoice_date and company_id = 1
|
||||
group by date
|
||||
) as expense using (date)
|
||||
order by date
|
||||
|
|
12
pkg/db.go
12
pkg/db.go
|
@ -143,14 +143,6 @@ func (tx *Tx) MustExec(ctx context.Context, sql string, args ...interface{}) {
|
|||
}
|
||||
}
|
||||
|
||||
func (tx *Tx) MustGetText(ctx context.Context, sql string, args ...interface{}) string {
|
||||
var result string
|
||||
if err := tx.QueryRow(ctx, sql, args...).Scan(&result); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (tx *Tx) MustGetInteger(ctx context.Context, sql string, args ...interface{}) int {
|
||||
var result int
|
||||
if err := tx.QueryRow(ctx, sql, args...).Scan(&result); err != nil {
|
||||
|
@ -167,8 +159,8 @@ func (tx *Tx) MustGetIntegerOrDefault(ctx context.Context, def int, sql string,
|
|||
return result
|
||||
}
|
||||
|
||||
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.CopyFromSlice(length, next))
|
||||
func (tx *Tx) MustCopyFrom(ctx context.Context, tableName string, columns []string, rows [][]interface{}) int64 {
|
||||
copied, err := tx.CopyFrom(ctx, pgx.Identifier{tableName}, columns, pgx.CopyFromRows(rows))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
528
pkg/expenses.go
528
pkg/expenses.go
|
@ -13,28 +13,18 @@ import (
|
|||
)
|
||||
|
||||
type ExpenseEntry struct {
|
||||
ID int
|
||||
Slug string
|
||||
InvoiceDate time.Time
|
||||
InvoiceNumber string
|
||||
Amount string
|
||||
Taxes map[string]string
|
||||
Total string
|
||||
InvoicerName string
|
||||
OriginalFileName string
|
||||
Tags []string
|
||||
Status string
|
||||
StatusLabel string
|
||||
}
|
||||
|
||||
type expensesIndexPage struct {
|
||||
Expenses []*ExpenseEntry
|
||||
SumAmount string
|
||||
SumTaxes map[string]string
|
||||
SumTotal string
|
||||
Filters *expenseFilterForm
|
||||
TaxClasses []string
|
||||
ExpenseStatuses map[string]string
|
||||
Expenses []*ExpenseEntry
|
||||
Filters *expenseFilterForm
|
||||
}
|
||||
|
||||
func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
|
@ -47,66 +37,65 @@ func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
|
|||
return
|
||||
}
|
||||
page := &expensesIndexPage{
|
||||
Expenses: mustCollectExpenseEntries(r.Context(), conn, locale, filters),
|
||||
ExpenseStatuses: mustCollectExpenseStatuses(r.Context(), conn, locale),
|
||||
TaxClasses: mustCollectTaxClasses(r.Context(), conn, company),
|
||||
Filters: filters,
|
||||
Expenses: mustCollectExpenseEntries(r.Context(), conn, company, filters),
|
||||
Filters: filters,
|
||||
}
|
||||
page.mustComputeExpensesTotalAmount(r.Context(), conn, filters)
|
||||
mustRenderMainTemplate(w, r, "expenses/index.gohtml", page)
|
||||
}
|
||||
|
||||
func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale, filters *expenseFilterForm) []*ExpenseEntry {
|
||||
where, args := filters.BuildQuery([]interface{}{locale.Language.String()})
|
||||
func mustCollectExpenseEntries(ctx context.Context, conn *Conn, company *Company, filters *expenseFilterForm) []*ExpenseEntry {
|
||||
args := []interface{}{company.Id}
|
||||
where := []string{"expense.company_id = $1"}
|
||||
appendWhere := func(expression string, value interface{}) {
|
||||
args = append(args, value)
|
||||
where = append(where, fmt.Sprintf(expression, len(args)))
|
||||
}
|
||||
maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) {
|
||||
if value != "" {
|
||||
if conv == nil {
|
||||
appendWhere(expression, value)
|
||||
} else {
|
||||
appendWhere(expression, conv(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
maybeAppendWhere("contact_id = $%d", filters.Customer.String(), func(v string) interface{} {
|
||||
customerId, _ := strconv.Atoi(filters.Customer.Selected[0])
|
||||
return customerId
|
||||
})
|
||||
maybeAppendWhere("invoice_number = $%d", filters.InvoiceNumber.String(), nil)
|
||||
maybeAppendWhere("invoice_date >= $%d", filters.FromDate.String(), nil)
|
||||
maybeAppendWhere("invoice_date <= $%d", filters.ToDate.String(), nil)
|
||||
if len(filters.Tags.Tags) > 0 {
|
||||
if filters.TagsCondition.Selected == "and" {
|
||||
appendWhere("expense.tags @> $%d", filters.Tags)
|
||||
} else {
|
||||
appendWhere("expense.tags && $%d", filters.Tags)
|
||||
}
|
||||
}
|
||||
rows := conn.MustQuery(ctx, fmt.Sprintf(`
|
||||
select expense_id
|
||||
, expense.slug
|
||||
select expense.slug
|
||||
, invoice_date
|
||||
, invoice_number
|
||||
, to_price(expense.amount, decimal_digits) as amount
|
||||
, 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
|
||||
, to_price(amount, decimal_digits)
|
||||
, contact.business_name
|
||||
, coalesce(attachment.original_filename, '')
|
||||
, expense.tags
|
||||
, expense.expense_status
|
||||
, esi18n.name
|
||||
from expense
|
||||
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 expense_status_i18n esi18n on expense.expense_status = esi18n.expense_status and esi18n.lang_tag = $1
|
||||
join currency using (currency_code)
|
||||
where (%s)
|
||||
group by expense_id
|
||||
, 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...)
|
||||
order by invoice_date
|
||||
`, strings.Join(where, ") AND (")), args...)
|
||||
defer rows.Close()
|
||||
|
||||
var entries []*ExpenseEntry
|
||||
for rows.Next() {
|
||||
entry := &ExpenseEntry{
|
||||
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 {
|
||||
entry := &ExpenseEntry{}
|
||||
if err := rows.Scan(&entry.Slug, &entry.InvoiceDate, &entry.InvoiceNumber, &entry.Amount, &entry.InvoicerName, &entry.OriginalFileName, &entry.Tags); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, tax := range taxes {
|
||||
entry.Taxes[tax[0]] = tax[1]
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
|
@ -115,99 +104,6 @@ func mustCollectExpenseEntries(ctx context.Context, conn *Conn, locale *Locale,
|
|||
|
||||
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) {
|
||||
locale := getLocale(r)
|
||||
conn := getConn(r)
|
||||
|
@ -220,10 +116,6 @@ func ServeExpenseForm(w http.ResponseWriter, r *http.Request, params httprouter.
|
|||
mustRenderNewExpenseForm(w, r, form)
|
||||
return
|
||||
}
|
||||
if !ValidUuid(slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
|
@ -235,40 +127,20 @@ func ServeExpenseForm(w http.ResponseWriter, r *http.Request, params httprouter.
|
|||
func mustRenderNewExpenseForm(w http.ResponseWriter, r *http.Request, form *expenseForm) {
|
||||
locale := getLocale(r)
|
||||
form.Invoicer.EmptyLabel = gettext("Select a contact.", locale)
|
||||
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
|
||||
mustRenderMainTemplate(w, r, "expenses/new.gohtml", form)
|
||||
}
|
||||
|
||||
func mustRenderEditExpenseForm(w http.ResponseWriter, r *http.Request, slug string, form *expenseForm) {
|
||||
page := &editExpensePage{
|
||||
newNewExpensePage(form, r),
|
||||
slug,
|
||||
Slug: slug,
|
||||
Form: form,
|
||||
}
|
||||
mustRenderMainTemplate(w, r, "expenses/edit.gohtml", page)
|
||||
}
|
||||
|
||||
type editExpensePage struct {
|
||||
*newExpensePage
|
||||
Slug string
|
||||
Form *expenseForm
|
||||
}
|
||||
|
||||
type expenseForm struct {
|
||||
|
@ -284,7 +156,6 @@ type expenseForm struct {
|
|||
}
|
||||
|
||||
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{
|
||||
locale: locale,
|
||||
company: company,
|
||||
|
@ -310,9 +181,6 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
|||
Label: pgettext("input", "Taxes", locale),
|
||||
Multiple: true,
|
||||
Options: mustGetTaxOptions(ctx, conn, company),
|
||||
Attributes: []template.HTMLAttr{
|
||||
triggerRecompute,
|
||||
},
|
||||
},
|
||||
Amount: &InputField{
|
||||
Name: "amount",
|
||||
|
@ -320,7 +188,7 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
|||
Type: "number",
|
||||
Required: true,
|
||||
Attributes: []template.HTMLAttr{
|
||||
triggerRecompute,
|
||||
`min="0"`,
|
||||
template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
|
||||
},
|
||||
},
|
||||
|
@ -336,16 +204,6 @@ func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
|||
}
|
||||
}
|
||||
|
||||
func mustGetExpenseStatusOptions(ctx context.Context, conn *Conn, locale *Locale) []*SelectOption {
|
||||
return MustGetOptions(ctx, conn, `
|
||||
select expense_status.expense_status
|
||||
, esi18n.name
|
||||
from expense_status
|
||||
join expense_status_i18n esi18n using(expense_status)
|
||||
where esi18n.lang_tag = $1
|
||||
order by expense_status`, locale.Language.String())
|
||||
}
|
||||
|
||||
func (form *expenseForm) Parse(r *http.Request) error {
|
||||
if err := r.ParseMultipartForm(form.File.MaxSize); err != nil {
|
||||
return err
|
||||
|
@ -369,18 +227,20 @@ func (form *expenseForm) Validate() bool {
|
|||
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))
|
||||
if validator.CheckRequiredInput(form.Amount, gettext("Amount can not be empty.", form.locale)) {
|
||||
validator.CheckValidDecimal(form.Amount, -math.MaxFloat64, math.MaxFloat64, gettext("Amount must be a decimal number.", form.locale))
|
||||
validator.CheckValidDecimal(form.Amount, form.company.MinCents(), math.MaxFloat64, gettext("Amount must be a number greater than zero.", form.locale))
|
||||
}
|
||||
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
|
||||
validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
|
||||
return validator.AllOK()
|
||||
}
|
||||
|
||||
func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
|
||||
if notFoundErrorOrPanic(conn.QueryRow(ctx, `
|
||||
return !notFoundErrorOrPanic(conn.QueryRow(ctx, `
|
||||
select contact_id
|
||||
, invoice_number
|
||||
, invoice_date
|
||||
, to_price(amount, decimal_digits)
|
||||
, array_agg(tax_id) filter ( where tax_id is not null )
|
||||
, array_agg(tax_id)
|
||||
, tags
|
||||
from expense
|
||||
left join expense_tax using (expense_id)
|
||||
|
@ -398,14 +258,36 @@ func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
|
|||
form.InvoiceDate,
|
||||
form.Amount,
|
||||
form.Tax,
|
||||
form.Tags)) {
|
||||
return false
|
||||
}
|
||||
if len(form.Tax.Selected) == 1 && form.Tax.Selected[0] == "" {
|
||||
form.Tax.Selected = nil
|
||||
}
|
||||
return true
|
||||
form.Tags))
|
||||
}
|
||||
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) {
|
||||
conn := getConn(r)
|
||||
locale := getLocale(r)
|
||||
|
@ -420,12 +302,10 @@ func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprout
|
|||
return
|
||||
}
|
||||
slug := params[0].Value
|
||||
if !ValidUuid(slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if !form.Validate() {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
if !IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
mustRenderEditExpenseForm(w, r, slug, form)
|
||||
return
|
||||
}
|
||||
|
@ -443,11 +323,10 @@ func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprout
|
|||
type expenseFilterForm struct {
|
||||
locale *Locale
|
||||
company *Company
|
||||
Contact *SelectField
|
||||
Customer *SelectField
|
||||
InvoiceNumber *InputField
|
||||
FromDate *InputField
|
||||
ToDate *InputField
|
||||
ExpenseStatus *SelectField
|
||||
Tags *TagsField
|
||||
TagsCondition *ToggleField
|
||||
}
|
||||
|
@ -456,10 +335,10 @@ func newExpenseFilterForm(ctx context.Context, conn *Conn, locale *Locale, compa
|
|||
return &expenseFilterForm{
|
||||
locale: locale,
|
||||
company: company,
|
||||
Contact: &SelectField{
|
||||
Name: "contact",
|
||||
Label: pgettext("input", "Contact", locale),
|
||||
EmptyLabel: gettext("All contacts", locale),
|
||||
Customer: &SelectField{
|
||||
Name: "customer",
|
||||
Label: pgettext("input", "Customer", locale),
|
||||
EmptyLabel: gettext("All customers", locale),
|
||||
Options: mustGetContactOptions(ctx, conn, company),
|
||||
},
|
||||
InvoiceNumber: &InputField{
|
||||
|
@ -481,12 +360,6 @@ func newExpenseFilterForm(ctx context.Context, conn *Conn, locale *Locale, compa
|
|||
Name: "tags",
|
||||
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{
|
||||
Name: "tags_condition",
|
||||
Label: pgettext("input", "Tags Condition", locale),
|
||||
|
@ -509,107 +382,34 @@ func (form *expenseFilterForm) Parse(r *http.Request) error {
|
|||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
form.Contact.FillValue(r)
|
||||
form.Customer.FillValue(r)
|
||||
form.InvoiceNumber.FillValue(r)
|
||||
form.FromDate.FillValue(r)
|
||||
form.ToDate.FillValue(r)
|
||||
form.ExpenseStatus.FillValue(r)
|
||||
form.Tags.FillValue(r)
|
||||
form.TagsCondition.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (form *expenseFilterForm) HasValue() bool {
|
||||
return form.Contact.HasValue() ||
|
||||
form.InvoiceNumber.HasValue() ||
|
||||
form.FromDate.HasValue() ||
|
||||
form.ToDate.HasValue() ||
|
||||
form.ExpenseStatus.HasValue() ||
|
||||
form.Tags.HasValue()
|
||||
}
|
||||
|
||||
func (form *expenseFilterForm) BuildQuery(args []interface{}) (string, []interface{}) {
|
||||
var where []string
|
||||
appendWhere := func(expression string, value interface{}) {
|
||||
args = append(args, value)
|
||||
where = append(where, fmt.Sprintf(expression, len(args)))
|
||||
}
|
||||
maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) {
|
||||
if value != "" {
|
||||
if conv == nil {
|
||||
appendWhere(expression, value)
|
||||
} else {
|
||||
appendWhere(expression, conv(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendWhere("expense.company_id = $%d", form.company.Id)
|
||||
maybeAppendWhere("contact_id = $%d", form.Contact.String(), func(v string) interface{} {
|
||||
customerId, _ := strconv.Atoi(form.Contact.Selected[0])
|
||||
return customerId
|
||||
})
|
||||
maybeAppendWhere("expense.expense_status = $%d", form.ExpenseStatus.String(), nil)
|
||||
maybeAppendWhere("invoice_number = $%d", form.InvoiceNumber.String(), nil)
|
||||
maybeAppendWhere("invoice_date >= $%d", form.FromDate.String(), nil)
|
||||
maybeAppendWhere("invoice_date <= $%d", form.ToDate.String(), nil)
|
||||
if len(form.Tags.Tags) > 0 {
|
||||
if form.TagsCondition.Selected == "and" {
|
||||
appendWhere("expense.tags @> $%d", form.Tags)
|
||||
} else {
|
||||
appendWhere("expense.tags && $%d", form.Tags)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(where, ") AND ("), args
|
||||
}
|
||||
|
||||
func ServeEditExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
serveTagsEditForm(w, r, params, "/expenses/", "select tags from expense where slug = $1")
|
||||
conn := getConn(r)
|
||||
locale := getLocale(r)
|
||||
company := getCompany(r)
|
||||
slug := params[0].Value
|
||||
form := newTagsForm(companyURI(company, "/expenses/"+slug+"/tags"), slug, locale)
|
||||
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from expense where slug = $1`, form.Slug).Scan(form.Tags)) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
|
||||
}
|
||||
|
||||
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)
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
form := newExpenseForm(r.Context(), conn, locale, company)
|
||||
company := getCompany(r)
|
||||
slug := params[0].Value
|
||||
form := newTagsForm(companyURI(company, "/expenses/"+slug+"/tags/edit"), slug, locale)
|
||||
if err := form.Parse(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
|
@ -618,117 +418,29 @@ func handleExpenseAction(w http.ResponseWriter, r *http.Request, action string,
|
|||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
actionField := r.Form.Get("action")
|
||||
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)
|
||||
if conn.MustGetText(r.Context(), "", "update expense set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
|
||||
}
|
||||
|
||||
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) {
|
||||
func ServeExpenseAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
slug := params[0].Value
|
||||
if !ValidUuid(slug) {
|
||||
conn := getConn(r)
|
||||
var contentType string
|
||||
var content []byte
|
||||
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `
|
||||
select mime_type
|
||||
, content
|
||||
from expense
|
||||
join expense_attachment using (expense_id)
|
||||
where slug = $1
|
||||
`, slug).Scan(&contentType, &content)) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := verifyCsrfTokenValid(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
conn := getConn(r)
|
||||
conn.MustExec(r.Context(), "select remove_expense($1)", slug)
|
||||
|
||||
company := mustGetCompany(r)
|
||||
htmxRedirect(w, r, companyURI(company, "/expenses"))
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(content)), 10))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(content)
|
||||
}
|
||||
|
|
113
pkg/form.go
113
pkg/form.go
|
@ -8,7 +8,7 @@ import (
|
|||
"fmt"
|
||||
"github.com/jackc/pgtype"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
|
@ -59,10 +59,6 @@ func (field *InputField) Value() (driver.Value, error) {
|
|||
return field.Val, nil
|
||||
}
|
||||
|
||||
func (field *InputField) HasValue() bool {
|
||||
return field.Val != ""
|
||||
}
|
||||
|
||||
func (field *InputField) FillValue(r *http.Request) {
|
||||
field.Val = strings.TrimSpace(r.FormValue(field.Name))
|
||||
}
|
||||
|
@ -188,10 +184,6 @@ func (field *SelectField) Clear() {
|
|||
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 {
|
||||
rows, err := conn.Query(ctx, sql, args...)
|
||||
if err != nil {
|
||||
|
@ -295,64 +287,6 @@ func (field *RadioField) isValidOption(selected string) bool {
|
|||
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 {
|
||||
Name string
|
||||
Label string
|
||||
|
@ -360,7 +294,6 @@ type FileField struct {
|
|||
OriginalFileName string
|
||||
ContentType string
|
||||
Content []byte
|
||||
Required bool
|
||||
Errors []error
|
||||
}
|
||||
|
||||
|
@ -373,7 +306,7 @@ func (field *FileField) FillValue(r *http.Request) error {
|
|||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
field.Content, err = io.ReadAll(file)
|
||||
field.Content, err = ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -401,10 +334,6 @@ func (field *TagsField) Value() (driver.Value, error) {
|
|||
return field.Tags, nil
|
||||
}
|
||||
|
||||
func (field *TagsField) HasValue() bool {
|
||||
return len(field.Tags) > 0 && field.Tags[0] != ""
|
||||
}
|
||||
|
||||
func (field *TagsField) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
|
@ -455,10 +384,6 @@ func (field *ToggleField) FillValue(r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func (field *ToggleField) String() string {
|
||||
return field.Selected
|
||||
}
|
||||
|
||||
type FormValidator struct {
|
||||
Valid bool
|
||||
}
|
||||
|
@ -479,29 +404,19 @@ func (v *FormValidator) CheckInputMinLength(field *InputField, min int, 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 {
|
||||
_, err := mail.ParseAddress(field.Val)
|
||||
return v.checkInput(field, err == nil, message)
|
||||
}
|
||||
|
||||
func (v *FormValidator) CheckValidVATINInput(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool {
|
||||
return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid($1 || $2, 'vatin')", country, field.Val), message)
|
||||
func (v *FormValidator) CheckValidVATINInput(field *InputField, country string, message string) bool {
|
||||
// TODO: actual VATIN validation
|
||||
return v.checkInput(field, true, message)
|
||||
}
|
||||
|
||||
func (v *FormValidator) CheckValidPhoneInput(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool {
|
||||
return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid_phone($1, $2)", field.Val, country), 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) CheckValidPhoneInput(field *InputField, country string, message string) bool {
|
||||
// TODO: actual phone validation
|
||||
return v.checkInput(field, true, message)
|
||||
}
|
||||
|
||||
func (v *FormValidator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool {
|
||||
|
@ -512,10 +427,6 @@ func (v *FormValidator) CheckValidSelectOption(field *SelectField, message strin
|
|||
return v.checkSelect(field, field.HasValidOptions(), message)
|
||||
}
|
||||
|
||||
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 {
|
||||
repeated := false
|
||||
groups := map[string]bool{}
|
||||
|
@ -574,11 +485,3 @@ func (v *FormValidator) checkSelect(field *SelectField, ok bool, message string)
|
|||
}
|
||||
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