Add payment collection

This is the same as a payment, but the user is the payee instead of the
payer.

I used a different relation than payment because i do not know any other
way to encode the constraint that only invoices can have a collection,
while expenses have only payments.

Besides the name and the fact that they are related to invoices, a
collection is pretty much the same as a payment.
This commit is contained in:
jordi fita mas 2024-08-21 03:36:12 +02:00
parent c6c550a036
commit 7f31b10cce
47 changed files with 1952 additions and 121 deletions

View File

@ -78,53 +78,84 @@ select add_product(123, 'Cavall Fort', 'Revista quinzenal en llengua catalana i
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, '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 daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.', '7.00', array[124], array['obsolet']); select add_product(123, 'Teia', 'Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.', '7.00', array[124], array['obsolet']);
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 with 123; alter sequence invoice_invoice_id_seq restart with 123;
alter sequence invoice_product_invoice_product_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_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_invoice(123, (current_date - '334 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}');
select add_collection(123, 124, (current_date - '330 days'::interval)::date, 123, 'Cobrament de FRA124', '1200.50', '{}');
select add_invoice(123, (current_date - '327 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); select add_invoice(123, (current_date - '327 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
select add_collection(123, 125, (current_date - '317 days'::interval)::date, 123, 'Cobrament de FRA125', '1200.50', '{}');
select add_invoice(123, (current_date - '317 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '317 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 126, (current_date - '316 days'::interval)::date, 124, 'Cobrament de FRA126', '12.87', '{}');
select add_invoice(123, (current_date - '314 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); select add_invoice(123, (current_date - '314 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
select add_collection(123, 127, (current_date - '310 days'::interval)::date, 123, 'Cobrament de FRA127', '1200.50', '{}');
select add_invoice(123, (current_date - '311 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '311 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 128, (current_date - '311 days'::interval)::date, 124, 'Cobrament de FRA128', '12.87', '{}');
select add_invoice(123, (current_date - '278 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}'); select add_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_invoice(123, (current_date - '274 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 130, (current_date - '272 days'::interval)::date, 123, 'Cobrament de FRA130', '691.90', '{}');
select add_invoice(123, (current_date - '267 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '267 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 131, (current_date - '265 days'::interval)::date, 123, 'Cobrament de FRA131', '87.50', '{}');
select add_invoice(123, (current_date - '257 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '257 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 132, (current_date - '250 days'::interval)::date, 124, 'Cobrament de FRA132', '87.50', '{}');
select add_invoice(123, (current_date - '254 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_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_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_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_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_invoice(123, (current_date - '197 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}');
select add_collection(123, 137, (current_date - '190 days'::interval)::date, 123, 'Cobrament de FRA137', '1200.50', '{}');
select add_invoice(123, (current_date - '187 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '187 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 138, (current_date - '186 days'::interval)::date, 124, 'Cobrament de FRA138', '12.87', '{}');
select add_invoice(123, (current_date - '184 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); select add_invoice(123, (current_date - '184 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
select add_collection(123, 139, (current_date - '181 days'::interval)::date, 123, 'Cobrament de FRA139', '1200.50', '{}');
select add_invoice(123, (current_date - '181 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}'); select add_invoice(123, (current_date - '181 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}');
select add_collection(123, 140, (current_date - '177 days'::interval)::date, 123, 'Cobrament de FRA140', '1200.50', '{}');
select add_invoice(123, (current_date - '148 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '148 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 141, (current_date - '140 days'::interval)::date, 123, 'Cobrament de FRA141', '87.50', '{}');
select add_invoice(123, (current_date - '144 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '144 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 142, (current_date - '143 days'::interval)::date, 123, 'Cobrament de FRA142', '87.50', '{}');
select add_invoice(123, (current_date - '137 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); select add_invoice(123, (current_date - '137 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
select add_collection(123, 143, (current_date - '137 days'::interval)::date, 123, 'Cobrament de FRA143', '1200.50', '{}');
select add_invoice(123, (current_date - '127 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}'); select add_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_invoice(123, (current_date - '124 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}');
select add_collection(123, 145, (current_date - '123 days'::interval)::date, 123, 'Cobrament de FRA145', '1200.50', '{}');
select add_invoice(123, (current_date - '121 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '121 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 146, (current_date - '101 days'::interval)::date, 124, 'Cobrament de FRA146', '12.87', '{}');
select add_invoice(123, (current_date - '78 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '78 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 147, (current_date - '60 days'::interval)::date, 123, 'Cobrament de FRA147', '87.50', '{}');
select add_invoice(123, (current_date - '74 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}'); select add_invoice(123, (current_date - '74 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}');
select add_collection(123, 148, (current_date - '61 days'::interval)::date, 123, 'Cobrament de FRA148', '1200.50', '{}');
select add_invoice(123, (current_date - '67 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '67 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 149, (current_date - '66 days'::interval)::date, 124, 'Cobrament de FRA149', '12.87', '{}');
select add_invoice(123, (current_date - '57 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); select add_invoice(123, (current_date - '57 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
select add_collection(123, 150, (current_date - '55 days'::interval)::date, 123, 'Cobrament de FRA150', '1200.50', '{}');
select add_invoice(123, (current_date - '54 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_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_invoice(123, (current_date - '51 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}');
select add_collection(123, 152, (current_date - '44 days'::interval)::date, 123, 'Cobrament de FRA152', '1200.50', '{}');
select add_invoice(123, (current_date - '28 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '28 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i daltres arbres, provinent sobretot del cor de larbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_collection(123, 153, (current_date - '28 days'::interval)::date, 124, 'Cobrament de FRA153', '12.87', '{}');
select add_invoice(123, (current_date - '24 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '24 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_invoice(123, (current_date - '17 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '17 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que susa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}');
select add_invoice(123, (current_date - '7 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}'); select add_invoice(123, (current_date - '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_invoice(123, (current_date - '4 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}');
select add_collection(123, 157, (current_date - '2 days'::interval)::date, 123, 'Primer cobrament de FRA157', '1000.00', '{}');
select add_invoice(123, (current_date - '1 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); select add_invoice(123, (current_date - '1 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
update invoice set invoice_status = 'paid' where invoice_id not in (154, 155, 156, 158);
update invoice set invoice_status = 'unpaid' where invoice_id = 155; update invoice set invoice_status = 'unpaid' where invoice_id = 155;
update invoice set invoice_status = 'sent' where invoice_id = 156; update invoice set invoice_status = 'sent' where invoice_id = 156;
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 expense_expense_id_seq restart with 123; 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_expense(123, (date_trunc('month', current_date) - '11 months + 14 day'::interval)::date, 130, 'ABC123', '256.12', '{124}', '{}');
select add_payment(123, 123, (date_trunc('month', current_date) - '11 months + 04 day'::interval)::date, 123, 'Pagament dABC123', '256.12', '{}'); select add_payment(123, 123, (date_trunc('month', current_date) - '11 months + 04 day'::interval)::date, 123, 'Pagament dABC123', '256.12', '{}');

68
deploy/add_collection.sql Normal file
View File

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

View File

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

View File

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

View File

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

45
deploy/collection.sql Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,11 @@ import (
"time" "time"
) )
const (
PaymentTypePayment = "P"
PaymentTypeCollection = "C"
)
func servePaymentIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func servePaymentIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r) conn := getConn(r)
company := mustGetCompany(r) company := mustGetCompany(r)
@ -42,14 +47,14 @@ type PaymentIndexPage struct {
func NewPaymentIndexPage(ctx context.Context, conn *Conn, company *Company, locale *Locale) *PaymentIndexPage { func NewPaymentIndexPage(ctx context.Context, conn *Conn, company *Company, locale *Locale) *PaymentIndexPage {
return &PaymentIndexPage{ return &PaymentIndexPage{
Payments: mustCollectPaymentEntries(ctx, conn, company, locale, 0), Payments: mustCollectPaymentEntries(ctx, conn, company, locale, "", 0),
BaseURI: companyURI(company, "/payments"), BaseURI: companyURI(company, "/payments"),
} }
} }
func NewPaymentIndexPageForExpense(ctx context.Context, conn *Conn, company *Company, locale *Locale, expense *PaymentExpense) *PaymentIndexPage { func NewPaymentIndexPageForExpense(ctx context.Context, conn *Conn, company *Company, locale *Locale, expense *PaymentExpense) *PaymentIndexPage {
return &PaymentIndexPage{ return &PaymentIndexPage{
Payments: mustCollectPaymentEntries(ctx, conn, company, locale, expense.Id), Payments: mustCollectPaymentEntries(ctx, conn, company, locale, PaymentTypePayment, expense.Id),
BaseURI: companyURI(company, "/expenses/"+expense.Slug+"/payments"), BaseURI: companyURI(company, "/expenses/"+expense.Slug+"/payments"),
Expense: expense, Expense: expense,
} }
@ -111,10 +116,11 @@ func (expense *PaymentExpense) calcRemainingPaymentAmount(ctx context.Context, c
type PaymentEntry struct { type PaymentEntry struct {
ID int ID int
Type string
Slug string Slug string
PaymentDate time.Time PaymentDate time.Time
Description string Description string
ExpenseSlug string DocumentSlug string
InvoiceNumber string InvoiceNumber string
Total string Total string
OriginalFileName string OriginalFileName string
@ -123,35 +129,57 @@ type PaymentEntry struct {
StatusLabel string StatusLabel string
} }
func mustCollectPaymentEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale, expenseId int) []*PaymentEntry { func mustCollectPaymentEntries(ctx context.Context, conn *Conn, company *Company, locale *Locale, paymentType string, documentId int) []*PaymentEntry {
rows := conn.MustQuery(ctx, ` rows := conn.MustQuery(ctx, `
select payment_id select $5 as type
, payment.slug , payment_id
, payment_date , payment.slug
, description , payment_date
, to_price(payment.amount, decimal_digits) as total , description
, payment.tags , to_price(payment.amount, decimal_digits) as total
, payment.payment_status , payment.tags
, psi18n.name , payment.payment_status
, coalesce(attachment.original_filename, '') , psi18n.name
, coalesce(expense.slug::text, '') , coalesce(attachment.original_filename, '')
, coalesce(expense.invoice_number, '') , coalesce(expense.slug::text, '')
from payment , coalesce(expense.invoice_number, '')
join payment_status_i18n psi18n on payment.payment_status = psi18n.payment_status and psi18n.lang_tag = $1 from payment
join currency using (currency_code) join payment_status_i18n psi18n on payment.payment_status = psi18n.payment_status and psi18n.lang_tag = $1
left join payment_attachment as attachment using (payment_id) join currency using (currency_code)
left join expense_payment using (payment_id) left join payment_attachment as attachment using (payment_id)
left join expense using (expense_id) left join expense_payment using (payment_id)
where payment.company_id = $2 left join expense using (expense_id)
and ($3 = 0 or expense_id = $3) where payment.company_id = $2
and ($3 = '' or ($3 = $5 and expense_id = $4))
union all
select $6 as type
, collection_id
, collection.slug
, collection_date as payment_date
, description
, to_price(collection.amount, decimal_digits) as total
, collection.tags
, collection.payment_status
, psi18n.name
, coalesce(attachment.original_filename, '')
, coalesce(invoice.slug::text, '')
, coalesce(invoice.invoice_number, '')
from collection
join payment_status_i18n psi18n on collection.payment_status = psi18n.payment_status and psi18n.lang_tag = $1
join currency using (currency_code)
left join collection_attachment as attachment using (collection_id)
left join invoice_collection using (collection_id)
left join invoice using (invoice_id)
where collection.company_id = $2
and ($3 = '' or ($3 = $6 and invoice_id = $4))
order by payment_date desc, total desc order by payment_date desc, total desc
`, locale.Language, company.Id, expenseId) `, locale.Language, company.Id, paymentType, documentId, PaymentTypePayment, PaymentTypeCollection)
defer rows.Close() defer rows.Close()
var entries []*PaymentEntry var entries []*PaymentEntry
for rows.Next() { for rows.Next() {
entry := &PaymentEntry{} entry := &PaymentEntry{}
if err := rows.Scan(&entry.ID, &entry.Slug, &entry.PaymentDate, &entry.Description, &entry.Total, &entry.Tags, &entry.Status, &entry.StatusLabel, &entry.OriginalFileName, &entry.ExpenseSlug, &entry.InvoiceNumber); err != nil { if err := rows.Scan(&entry.Type, &entry.ID, &entry.Slug, &entry.PaymentDate, &entry.Description, &entry.Total, &entry.Tags, &entry.Status, &entry.StatusLabel, &entry.OriginalFileName, &entry.DocumentSlug, &entry.InvoiceNumber); err != nil {
panic(err) panic(err)
} }
entries = append(entries, entry) entries = append(entries, entry)
@ -221,6 +249,7 @@ type PaymentForm struct {
Slug string Slug string
BaseURI string BaseURI string
Expense *PaymentExpense Expense *PaymentExpense
Type *SelectField
Description *InputField Description *InputField
PaymentDate *InputField PaymentDate *InputField
PaymentAccount *SelectField PaymentAccount *SelectField
@ -234,6 +263,15 @@ func newPaymentForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
locale: locale, locale: locale,
company: company, company: company,
BaseURI: companyURI(company, "/payments"), BaseURI: companyURI(company, "/payments"),
Type: &SelectField{
Name: "type",
Label: pgettext("input", "Type", locale),
Required: true,
Options: []*SelectOption{
{Value: PaymentTypePayment, Label: pgettext("payment type", "Payment", locale)},
{Value: PaymentTypeCollection, Label: pgettext("payment type", "Collection", locale)},
},
},
Description: &InputField{ Description: &InputField{
Name: "description", Name: "description",
Label: pgettext("input", "Description", locale), Label: pgettext("input", "Description", locale),
@ -276,6 +314,7 @@ func newPaymentForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
func newPaymentFormForExpense(ctx context.Context, conn *Conn, locale *Locale, company *Company, expense *PaymentExpense) *PaymentForm { func newPaymentFormForExpense(ctx context.Context, conn *Conn, locale *Locale, company *Company, expense *PaymentExpense) *PaymentForm {
form := newPaymentForm(ctx, conn, locale, company) form := newPaymentForm(ctx, conn, locale, company)
form.Type.Selected = []string{PaymentTypePayment}
form.BaseURI = companyURI(company, "/expenses/"+expense.Slug+"/payments") form.BaseURI = companyURI(company, "/expenses/"+expense.Slug+"/payments")
form.Expense = expense form.Expense = expense
return form return form
@ -283,6 +322,7 @@ func newPaymentFormForExpense(ctx context.Context, conn *Conn, locale *Locale, c
func (f *PaymentForm) MustRender(w http.ResponseWriter, r *http.Request) { func (f *PaymentForm) MustRender(w http.ResponseWriter, r *http.Request) {
if f.Slug == "" { if f.Slug == "" {
f.Type.EmptyLabel = gettext("Select a type.", f.locale)
f.PaymentAccount.EmptyLabel = gettext("Select an account.", f.locale) f.PaymentAccount.EmptyLabel = gettext("Select an account.", f.locale)
mustRenderMainTemplate(w, r, "payments/new.gohtml", f) mustRenderMainTemplate(w, r, "payments/new.gohtml", f)
} else { } else {
@ -291,23 +331,37 @@ func (f *PaymentForm) MustRender(w http.ResponseWriter, r *http.Request) {
} }
func (f *PaymentForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool { func (f *PaymentForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
selectedType := f.Type.Selected
selectedPaymentAccount := f.PaymentAccount.Selected selectedPaymentAccount := f.PaymentAccount.Selected
f.PaymentAccount.Clear() f.PaymentAccount.Clear()
if notFoundErrorOrPanic(conn.QueryRow(ctx, ` if notFoundErrorOrPanic(conn.QueryRow(ctx, `
select description select $2 as type
, payment_date , description
, payment_account_id::text , payment_date
, to_price(amount, decimal_digits) , payment_account_id::text
, tags , to_price(amount, decimal_digits)
from payment , tags
join currency using (currency_code) from payment
where payment.slug = $1 join currency using (currency_code)
`, slug).Scan( where payment.slug = $1
union all
select $3 as type
, description
, collection_date
, payment_account_id::text
, to_price(amount, decimal_digits)
, tags
from collection
join currency using (currency_code)
where collection.slug = $1
`, slug, PaymentTypePayment, PaymentTypeCollection).Scan(
f.Type,
f.Description, f.Description,
f.PaymentDate, f.PaymentDate,
f.PaymentAccount, f.PaymentAccount,
f.Amount, f.Amount,
f.Tags)) { f.Tags)) {
f.Type.Selected = selectedType
f.PaymentAccount.Selected = selectedPaymentAccount f.PaymentAccount.Selected = selectedPaymentAccount
return false return false
} }
@ -319,6 +373,7 @@ func (f *PaymentForm) Parse(r *http.Request) error {
if err := r.ParseMultipartForm(f.File.MaxSize); err != nil { if err := r.ParseMultipartForm(f.File.MaxSize); err != nil {
return err return err
} }
f.Type.FillValue(r)
f.Description.FillValue(r) f.Description.FillValue(r)
f.PaymentDate.FillValue(r) f.PaymentDate.FillValue(r)
f.PaymentAccount.FillValue(r) f.PaymentAccount.FillValue(r)
@ -332,6 +387,7 @@ func (f *PaymentForm) Parse(r *http.Request) error {
func (f *PaymentForm) Validate() bool { func (f *PaymentForm) Validate() bool {
validator := newFormValidator() validator := newFormValidator()
validator.CheckValidSelectOption(f.Type, gettext("Selected payment type is not valid.", f.locale))
validator.CheckRequiredInput(f.Description, gettext("Description can not be empty.", f.locale)) validator.CheckRequiredInput(f.Description, gettext("Description can not be empty.", f.locale))
validator.CheckValidSelectOption(f.PaymentAccount, gettext("Selected payment account is not valid.", f.locale)) validator.CheckValidSelectOption(f.PaymentAccount, gettext("Selected payment account is not valid.", f.locale))
validator.CheckValidDate(f.PaymentDate, gettext("Payment date must be a valid date.", f.locale)) validator.CheckValidDate(f.PaymentDate, gettext("Payment date must be a valid date.", f.locale))
@ -365,13 +421,20 @@ func handleAddPaymentForm(w http.ResponseWriter, r *http.Request, conn *Conn, co
form.MustRender(w, r) form.MustRender(w, r)
return return
} }
var paymentSlug any var documentId any
if form.Expense != nil { if form.Expense != nil {
paymentSlug = form.Expense.Id documentId = form.Expense.Id
} }
slug := conn.MustGetText(r.Context(), "", "select add_payment($1, $2, $3, $4, $5, $6, $7)", company.Id, paymentSlug, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags) if form.Type.String() == PaymentTypePayment {
if len(form.File.Content) > 0 { slug := conn.MustGetText(r.Context(), "", "select add_payment($1, $2, $3, $4, $5, $6, $7)", company.Id, documentId, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags)
conn.MustQuery(r.Context(), "select attach_to_payment($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content) if len(form.File.Content) > 0 {
conn.MustQuery(r.Context(), "select attach_to_payment($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
}
} else {
slug := conn.MustGetText(r.Context(), "", "select add_collection($1, $2, $3, $4, $5, $6, $7)", company.Id, documentId, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags)
if len(form.File.Content) > 0 {
conn.MustQuery(r.Context(), "select attach_to_collection($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
}
} }
htmxRedirect(w, r, form.BaseURI) htmxRedirect(w, r, form.BaseURI)
} }
@ -420,12 +483,22 @@ func handleEditPaymentForm(w http.ResponseWriter, r *http.Request, conn *Conn, f
form.MustRender(w, r) form.MustRender(w, r)
return return
} }
if found := conn.MustGetText(r.Context(), "", "select edit_payment($1, $2, $3, $4, $5, $6)", form.Slug, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags); found == "" { if form.Type.String() == PaymentTypePayment {
http.NotFound(w, r) if found := conn.MustGetText(r.Context(), "", "select edit_payment($1, $2, $3, $4, $5, $6)", form.Slug, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags); found == "" {
return http.NotFound(w, r)
} return
if len(form.File.Content) > 0 { }
conn.MustQuery(r.Context(), "select attach_to_payment($1, $2, $3, $4)", form.Slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content) if len(form.File.Content) > 0 {
conn.MustQuery(r.Context(), "select attach_to_payment($1, $2, $3, $4)", form.Slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
}
} else {
if found := conn.MustGetText(r.Context(), "", "select edit_collection($1, $2, $3, $4, $5, $6)", form.Slug, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags); found == "" {
http.NotFound(w, r)
return
}
if len(form.File.Content) > 0 {
conn.MustQuery(r.Context(), "select attach_to_collection($1, $2, $3, $4)", form.Slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
}
} }
htmxRedirect(w, r, form.BaseURI) htmxRedirect(w, r, form.BaseURI)
} }
@ -462,7 +535,7 @@ func removePayment(w http.ResponseWriter, r *http.Request, slug string, backURI
} }
conn := getConn(r) conn := getConn(r)
conn.MustExec(r.Context(), "select remove_payment($1)", slug) conn.MustExec(r.Context(), "select remove_payment($1), remove_collection($1)", slug)
htmxRedirect(w, r, backURI) htmxRedirect(w, r, backURI)
} }
@ -482,18 +555,24 @@ func handleRemoveExpensePayment(w http.ResponseWriter, r *http.Request, params h
func servePaymentAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func servePaymentAttachment(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
serveAttachment(w, r, params, ` serveAttachment(w, r, params, `
select mime_type select mime_type
, content , content
from payment from payment
join payment_attachment using (payment_id) join payment_attachment using (payment_id)
where slug = $1 where slug = $1
union all
select mime_type
, content
from collection
join collection_attachment using (collection_id)
where slug = $1
`) `)
} }
func servePaymentTagsEditForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func servePaymentTagsEditForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
serveTagsEditForm(w, r, params, "/payments/", "select tags from payment where slug = $1") serveTagsEditForm(w, r, params, "/payments/", "select tags from payment where slug = $1 union all select tags from collection where slug = $1")
} }
func handleUpdatePaymentTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func handleUpdatePaymentTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
handleUpdateTags(w, r, params, "/payments/", "update payment set tags = $1 where slug = $2 returning slug") handleUpdateTags(w, r, params, "/payments/", "with p as (update payment set tags = $1 where slug = $2 returning slug), c as (update collection set tags = $1 where slug = $2 returning slug) select p.slug from p union all select c.slug from c")
} }

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-08-17 05:26+0200\n" "POT-Creation-Date: 2024-08-21 03:28+0200\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -39,7 +39,7 @@ msgstr "Afegeix productes a la factura"
#: web/template/company/payment_methods.gohtml:10 #: web/template/company/payment_methods.gohtml:10
#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:9 #: web/template/products/new.gohtml:9 web/template/products/index.gohtml:9
#: web/template/products/edit.gohtml:10 web/template/payments/new.gohtml:10 #: web/template/products/edit.gohtml:10 web/template/payments/new.gohtml:10
#: web/template/payments/index.gohtml:10 web/template/payments/edit.gohtml:10 #: web/template/payments/index.gohtml:10 web/template/payments/edit.gohtml:14
#: web/template/payments/accounts/new.gohtml:10 #: web/template/payments/accounts/new.gohtml:10
#: web/template/payments/accounts/index.gohtml:10 #: web/template/payments/accounts/index.gohtml:10
#: web/template/payments/accounts/edit.gohtml:10 #: web/template/payments/accounts/edit.gohtml:10
@ -124,7 +124,7 @@ msgstr "Total"
#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93
#: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 #: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93
#: web/template/expenses/new.gohtml:56 web/template/expenses/edit.gohtml:58 #: web/template/expenses/new.gohtml:56 web/template/expenses/edit.gohtml:58
#: web/template/payments/edit.gohtml:41 #: web/template/payments/edit.gohtml:46
#: web/template/payments/accounts/edit.gohtml:38 #: web/template/payments/accounts/edit.gohtml:38
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
@ -135,7 +135,7 @@ msgstr "Actualitza"
#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53
#: web/template/expenses/new.gohtml:59 web/template/expenses/edit.gohtml:61 #: web/template/expenses/new.gohtml:59 web/template/expenses/edit.gohtml:61
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36
#: web/template/payments/new.gohtml:39 #: web/template/payments/new.gohtml:44
#: web/template/payments/accounts/new.gohtml:41 #: web/template/payments/accounts/new.gohtml:41
msgctxt "action" msgctxt "action"
msgid "Save" msgid "Save"
@ -242,7 +242,7 @@ msgstr "Accions per la factura %s"
#: web/template/invoices/index.gohtml:139 web/template/invoices/view.gohtml:19 #: web/template/invoices/index.gohtml:139 web/template/invoices/view.gohtml:19
#: web/template/quotes/index.gohtml:137 web/template/quotes/view.gohtml:22 #: web/template/quotes/index.gohtml:137 web/template/quotes/view.gohtml:22
#: web/template/contacts/index.gohtml:82 web/template/expenses/index.gohtml:125 #: web/template/contacts/index.gohtml:82 web/template/expenses/index.gohtml:125
#: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:81 #: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:85
msgctxt "action" msgctxt "action"
msgid "Edit" msgid "Edit"
msgstr "Edita" msgstr "Edita"
@ -634,7 +634,7 @@ msgstr "Nova despesa"
#: web/template/expenses/new.gohtml:11 web/template/expenses/index.gohtml:3 #: web/template/expenses/new.gohtml:11 web/template/expenses/index.gohtml:3
#: web/template/expenses/index.gohtml:11 web/template/expenses/edit.gohtml:11 #: web/template/expenses/index.gohtml:11 web/template/expenses/edit.gohtml:11
#: web/template/payments/new.gohtml:12 web/template/payments/index.gohtml:12 #: web/template/payments/new.gohtml:12 web/template/payments/index.gohtml:12
#: web/template/payments/edit.gohtml:12 #: web/template/payments/edit.gohtml:16
msgctxt "title" msgctxt "title"
msgid "Expenses" msgid "Expenses"
msgstr "Despeses" msgstr "Despeses"
@ -781,7 +781,7 @@ msgid "New Payment"
msgstr "Nou pagament" msgstr "Nou pagament"
#: web/template/payments/new.gohtml:15 web/template/payments/index.gohtml:3 #: web/template/payments/new.gohtml:15 web/template/payments/index.gohtml:3
#: web/template/payments/index.gohtml:15 web/template/payments/edit.gohtml:15 #: web/template/payments/index.gohtml:15 web/template/payments/edit.gohtml:19
msgctxt "title" msgctxt "title"
msgid "Payments" msgid "Payments"
msgstr "Pagaments" msgstr "Pagaments"
@ -810,24 +810,29 @@ msgstr "Document"
msgid "Are you sure you wish to delete this payment?" msgid "Are you sure you wish to delete this payment?"
msgstr "Esteu segur de voler esborrar aquest pagament?" msgstr "Esteu segur de voler esborrar aquest pagament?"
#: web/template/payments/index.gohtml:73 #: web/template/payments/index.gohtml:77
msgid "Actions for payment %s" msgid "Actions for payment %s"
msgstr "Accions pel pagament %s" msgstr "Accions pel pagament %s"
#: web/template/payments/index.gohtml:92 #: web/template/payments/index.gohtml:96
msgctxt "action" msgctxt "action"
msgid "Remove" msgid "Remove"
msgstr "Esborra" msgstr "Esborra"
#: web/template/payments/index.gohtml:102 #: web/template/payments/index.gohtml:106
msgid "No payments added yet." msgid "No payments added yet."
msgstr "No hi ha cap pagament." msgstr "No hi ha cap pagament."
#: web/template/payments/edit.gohtml:3 #: web/template/payments/edit.gohtml:4
msgctxt "title" msgctxt "title"
msgid "Edit Payment “%s”" msgid "Edit Payment “%s”"
msgstr "Edició del pagament «%s»" msgstr "Edició del pagament «%s»"
#: web/template/payments/edit.gohtml:6
msgctxt "title"
msgid "Edit Collection “%s”"
msgstr "Edició del cobrament «%s»"
#: web/template/payments/accounts/new.gohtml:3 #: web/template/payments/accounts/new.gohtml:3
#: web/template/payments/accounts/new.gohtml:12 #: web/template/payments/accounts/new.gohtml:12
msgctxt "title" msgctxt "title"
@ -901,7 +906,7 @@ msgid "Name"
msgstr "Nom" msgstr "Nom"
#: pkg/products.go:177 pkg/products.go:303 pkg/tags.go:37 pkg/quote.go:174 #: pkg/products.go:177 pkg/products.go:303 pkg/tags.go:37 pkg/quote.go:174
#: pkg/quote.go:708 pkg/payments.go:272 pkg/expenses.go:335 pkg/expenses.go:485 #: pkg/quote.go:708 pkg/payments.go:310 pkg/expenses.go:335 pkg/expenses.go:485
#: pkg/invoices.go:177 pkg/invoices.go:877 pkg/contacts.go:154 #: pkg/invoices.go:177 pkg/invoices.go:877 pkg/contacts.go:154
#: pkg/contacts.go:362 #: pkg/contacts.go:362
msgctxt "input" msgctxt "input"
@ -936,7 +941,7 @@ msgstr "Qualsevol"
msgid "Invoices must have at least one of the specified labels." msgid "Invoices must have at least one of the specified labels."
msgstr "Les factures han de tenir com a mínim una de les etiquetes." msgstr "Les factures han de tenir com a mínim una de les etiquetes."
#: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:239 #: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:277
#: pkg/invoices.go:1161 #: pkg/invoices.go:1161
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
@ -1351,60 +1356,78 @@ msgstr "La confirmació no és igual a la contrasenya."
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid." msgstr "Heu seleccionat un idioma que no és vàlid."
#: pkg/payments.go:202 #: pkg/payments.go:230
#, c-format #, c-format
msgid "Payment of %s" msgid "Payment of %s"
msgstr "Pagament de %s" msgstr "Pagament de %s"
#: pkg/payments.go:245 #: pkg/payments.go:268 pkg/accounts.go:131
msgctxt "input"
msgid "Type"
msgstr "Tipus"
#: pkg/payments.go:271
msgctxt "payment type"
msgid "Payment"
msgstr "Pagament"
#: pkg/payments.go:272
msgctxt "payment type"
msgid "Collection"
msgstr "Cobrament"
#: pkg/payments.go:283
msgctxt "input" msgctxt "input"
msgid "Payment Date" msgid "Payment Date"
msgstr "Data del pagament" msgstr "Data del pagament"
#: pkg/payments.go:251 #: pkg/payments.go:289
msgctxt "input" msgctxt "input"
msgid "Account" msgid "Account"
msgstr "Compte" msgstr "Compte"
#: pkg/payments.go:257 pkg/expenses.go:319 #: pkg/payments.go:295 pkg/expenses.go:319
msgctxt "input" msgctxt "input"
msgid "Amount" msgid "Amount"
msgstr "Import" msgstr "Import"
#: pkg/payments.go:267 pkg/expenses.go:330 pkg/invoices.go:888 #: pkg/payments.go:305 pkg/expenses.go:330 pkg/invoices.go:888
msgctxt "input" msgctxt "input"
msgid "File" msgid "File"
msgstr "Fitxer" msgstr "Fitxer"
#: pkg/payments.go:286 #: pkg/payments.go:325
msgid "Select a type."
msgstr "Escolliu un tipus."
#: pkg/payments.go:326
msgid "Select an account." msgid "Select an account."
msgstr "Escolliu un compte." msgstr "Escolliu un compte."
#: pkg/payments.go:335 #: pkg/payments.go:390
msgid "Selected payment type is not valid."
msgstr "Heu seleccionat un tipus de pagament que no és vàlid."
#: pkg/payments.go:391
msgid "Description can not be empty." msgid "Description can not be empty."
msgstr "No podeu deixar la descripció en blanc." msgstr "No podeu deixar la descripció en blanc."
#: pkg/payments.go:336 #: pkg/payments.go:392
msgid "Selected payment account is not valid." msgid "Selected payment account is not valid."
msgstr "Heu seleccionat un compte de pagament que no és vàlid." msgstr "Heu seleccionat un compte de pagament que no és vàlid."
#: pkg/payments.go:337 #: pkg/payments.go:393
msgid "Payment date must be a valid date." msgid "Payment date must be a valid date."
msgstr "La data de pagament ha de ser vàlida." msgstr "La data de pagament ha de ser vàlida."
#: pkg/payments.go:338 pkg/expenses.go:372 #: pkg/payments.go:394 pkg/expenses.go:372
msgid "Amount can not be empty." msgid "Amount can not be empty."
msgstr "No podeu deixar limport en blanc." msgstr "No podeu deixar limport en blanc."
#: pkg/payments.go:339 pkg/expenses.go:373 #: pkg/payments.go:395 pkg/expenses.go:373
msgid "Amount must be a number greater than zero." msgid "Amount must be a number greater than zero."
msgstr "Limport ha de ser un número major a zero." msgstr "Limport ha de ser un número major a zero."
#: pkg/accounts.go:131
msgctxt "input"
msgid "Type"
msgstr "Tipus"
#: pkg/accounts.go:146 pkg/contacts.go:352 #: pkg/accounts.go:146 pkg/contacts.go:352
msgctxt "input" msgctxt "input"
msgid "IBAN" msgid "IBAN"

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-08-17 05:26+0200\n" "POT-Creation-Date: 2024-08-21 03:28+0200\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -39,7 +39,7 @@ msgstr "Añadir productos a la factura"
#: web/template/company/payment_methods.gohtml:10 #: web/template/company/payment_methods.gohtml:10
#: web/template/products/new.gohtml:9 web/template/products/index.gohtml:9 #: web/template/products/new.gohtml:9 web/template/products/index.gohtml:9
#: web/template/products/edit.gohtml:10 web/template/payments/new.gohtml:10 #: web/template/products/edit.gohtml:10 web/template/payments/new.gohtml:10
#: web/template/payments/index.gohtml:10 web/template/payments/edit.gohtml:10 #: web/template/payments/index.gohtml:10 web/template/payments/edit.gohtml:14
#: web/template/payments/accounts/new.gohtml:10 #: web/template/payments/accounts/new.gohtml:10
#: web/template/payments/accounts/index.gohtml:10 #: web/template/payments/accounts/index.gohtml:10
#: web/template/payments/accounts/edit.gohtml:10 #: web/template/payments/accounts/edit.gohtml:10
@ -124,7 +124,7 @@ msgstr "Total"
#: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93
#: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 #: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93
#: web/template/expenses/new.gohtml:56 web/template/expenses/edit.gohtml:58 #: web/template/expenses/new.gohtml:56 web/template/expenses/edit.gohtml:58
#: web/template/payments/edit.gohtml:41 #: web/template/payments/edit.gohtml:46
#: web/template/payments/accounts/edit.gohtml:38 #: web/template/payments/accounts/edit.gohtml:38
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
@ -135,7 +135,7 @@ msgstr "Actualizar"
#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53
#: web/template/expenses/new.gohtml:59 web/template/expenses/edit.gohtml:61 #: web/template/expenses/new.gohtml:59 web/template/expenses/edit.gohtml:61
#: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36
#: web/template/payments/new.gohtml:39 #: web/template/payments/new.gohtml:44
#: web/template/payments/accounts/new.gohtml:41 #: web/template/payments/accounts/new.gohtml:41
msgctxt "action" msgctxt "action"
msgid "Save" msgid "Save"
@ -242,7 +242,7 @@ msgstr "Acciones para la factura %s"
#: web/template/invoices/index.gohtml:139 web/template/invoices/view.gohtml:19 #: web/template/invoices/index.gohtml:139 web/template/invoices/view.gohtml:19
#: web/template/quotes/index.gohtml:137 web/template/quotes/view.gohtml:22 #: web/template/quotes/index.gohtml:137 web/template/quotes/view.gohtml:22
#: web/template/contacts/index.gohtml:82 web/template/expenses/index.gohtml:125 #: web/template/contacts/index.gohtml:82 web/template/expenses/index.gohtml:125
#: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:81 #: web/template/products/index.gohtml:78 web/template/payments/index.gohtml:85
msgctxt "action" msgctxt "action"
msgid "Edit" msgid "Edit"
msgstr "Editar" msgstr "Editar"
@ -634,7 +634,7 @@ msgstr "Nuevo gasto"
#: web/template/expenses/new.gohtml:11 web/template/expenses/index.gohtml:3 #: web/template/expenses/new.gohtml:11 web/template/expenses/index.gohtml:3
#: web/template/expenses/index.gohtml:11 web/template/expenses/edit.gohtml:11 #: web/template/expenses/index.gohtml:11 web/template/expenses/edit.gohtml:11
#: web/template/payments/new.gohtml:12 web/template/payments/index.gohtml:12 #: web/template/payments/new.gohtml:12 web/template/payments/index.gohtml:12
#: web/template/payments/edit.gohtml:12 #: web/template/payments/edit.gohtml:16
msgctxt "title" msgctxt "title"
msgid "Expenses" msgid "Expenses"
msgstr "Gastos" msgstr "Gastos"
@ -781,7 +781,7 @@ msgid "New Payment"
msgstr "Nuevo pago" msgstr "Nuevo pago"
#: web/template/payments/new.gohtml:15 web/template/payments/index.gohtml:3 #: web/template/payments/new.gohtml:15 web/template/payments/index.gohtml:3
#: web/template/payments/index.gohtml:15 web/template/payments/edit.gohtml:15 #: web/template/payments/index.gohtml:15 web/template/payments/edit.gohtml:19
msgctxt "title" msgctxt "title"
msgid "Payments" msgid "Payments"
msgstr "Pagos" msgstr "Pagos"
@ -810,24 +810,29 @@ msgstr "Documento"
msgid "Are you sure you wish to delete this payment?" msgid "Are you sure you wish to delete this payment?"
msgstr "¿Estáis seguro de querer borrar este pago?" msgstr "¿Estáis seguro de querer borrar este pago?"
#: web/template/payments/index.gohtml:73 #: web/template/payments/index.gohtml:77
msgid "Actions for payment %s" msgid "Actions for payment %s"
msgstr "Acciones para el pago %s" msgstr "Acciones para el pago %s"
#: web/template/payments/index.gohtml:92 #: web/template/payments/index.gohtml:96
msgctxt "action" msgctxt "action"
msgid "Remove" msgid "Remove"
msgstr "Borrar" msgstr "Borrar"
#: web/template/payments/index.gohtml:102 #: web/template/payments/index.gohtml:106
msgid "No payments added yet." msgid "No payments added yet."
msgstr "No hay pagos." msgstr "No hay pagos."
#: web/template/payments/edit.gohtml:3 #: web/template/payments/edit.gohtml:4
msgctxt "title" msgctxt "title"
msgid "Edit Payment “%s”" msgid "Edit Payment “%s”"
msgstr "Edición del pago «%s»" msgstr "Edición del pago «%s»"
#: web/template/payments/edit.gohtml:6
msgctxt "title"
msgid "Edit Collection “%s”"
msgstr "Edición del cobro «%s»"
#: web/template/payments/accounts/new.gohtml:3 #: web/template/payments/accounts/new.gohtml:3
#: web/template/payments/accounts/new.gohtml:12 #: web/template/payments/accounts/new.gohtml:12
msgctxt "title" msgctxt "title"
@ -901,7 +906,7 @@ msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: pkg/products.go:177 pkg/products.go:303 pkg/tags.go:37 pkg/quote.go:174 #: pkg/products.go:177 pkg/products.go:303 pkg/tags.go:37 pkg/quote.go:174
#: pkg/quote.go:708 pkg/payments.go:272 pkg/expenses.go:335 pkg/expenses.go:485 #: pkg/quote.go:708 pkg/payments.go:310 pkg/expenses.go:335 pkg/expenses.go:485
#: pkg/invoices.go:177 pkg/invoices.go:877 pkg/contacts.go:154 #: pkg/invoices.go:177 pkg/invoices.go:877 pkg/contacts.go:154
#: pkg/contacts.go:362 #: pkg/contacts.go:362
msgctxt "input" msgctxt "input"
@ -936,7 +941,7 @@ msgstr "Cualquiera"
msgid "Invoices must have at least one of the specified labels." msgid "Invoices must have at least one of the specified labels."
msgstr "Las facturas deben tener como mínimo una de las etiquetas." msgstr "Las facturas deben tener como mínimo una de las etiquetas."
#: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:239 #: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:277
#: pkg/invoices.go:1161 #: pkg/invoices.go:1161
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
@ -1351,60 +1356,78 @@ msgstr "La confirmación no corresponde con la contraseña."
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido." msgstr "Habéis escogido un idioma que no es válido."
#: pkg/payments.go:202 #: pkg/payments.go:230
#, c-format #, c-format
msgid "Payment of %s" msgid "Payment of %s"
msgstr "Pago de %s" msgstr "Pago de %s"
#: pkg/payments.go:245 #: pkg/payments.go:268 pkg/accounts.go:131
msgctxt "input"
msgid "Type"
msgstr "Tipo"
#: pkg/payments.go:271
msgctxt "payment type"
msgid "Payment"
msgstr "Pago"
#: pkg/payments.go:272
msgctxt "payment type"
msgid "Collection"
msgstr "Cobro"
#: pkg/payments.go:283
msgctxt "input" msgctxt "input"
msgid "Payment Date" msgid "Payment Date"
msgstr "Fecha del pago" msgstr "Fecha del pago"
#: pkg/payments.go:251 #: pkg/payments.go:289
msgctxt "input" msgctxt "input"
msgid "Account" msgid "Account"
msgstr "Cuenta" msgstr "Cuenta"
#: pkg/payments.go:257 pkg/expenses.go:319 #: pkg/payments.go:295 pkg/expenses.go:319
msgctxt "input" msgctxt "input"
msgid "Amount" msgid "Amount"
msgstr "Importe" msgstr "Importe"
#: pkg/payments.go:267 pkg/expenses.go:330 pkg/invoices.go:888 #: pkg/payments.go:305 pkg/expenses.go:330 pkg/invoices.go:888
msgctxt "input" msgctxt "input"
msgid "File" msgid "File"
msgstr "Archivo" msgstr "Archivo"
#: pkg/payments.go:286 #: pkg/payments.go:325
msgid "Select a type."
msgstr "Escoged un tipo."
#: pkg/payments.go:326
msgid "Select an account." msgid "Select an account."
msgstr "Escoged una cuenta." msgstr "Escoged una cuenta."
#: pkg/payments.go:335 #: pkg/payments.go:390
msgid "Selected payment type is not valid."
msgstr "Habéis escogido un tipo de pago que no es válido."
#: pkg/payments.go:391
msgid "Description can not be empty." msgid "Description can not be empty."
msgstr "No podéis dejar la descripción en blanco." msgstr "No podéis dejar la descripción en blanco."
#: pkg/payments.go:336 #: pkg/payments.go:392
msgid "Selected payment account is not valid." msgid "Selected payment account is not valid."
msgstr "Habéis escogido una cuenta de pago que no es válida." msgstr "Habéis escogido una cuenta de pago que no es válida."
#: pkg/payments.go:337 #: pkg/payments.go:393
msgid "Payment date must be a valid date." msgid "Payment date must be a valid date."
msgstr "La fecha de pago debe ser válida." msgstr "La fecha de pago debe ser válida."
#: pkg/payments.go:338 pkg/expenses.go:372 #: pkg/payments.go:394 pkg/expenses.go:372
msgid "Amount can not be empty." msgid "Amount can not be empty."
msgstr "No podéis dejar el importe en blanco." msgstr "No podéis dejar el importe en blanco."
#: pkg/payments.go:339 pkg/expenses.go:373 #: pkg/payments.go:395 pkg/expenses.go:373
msgid "Amount must be a number greater than zero." msgid "Amount must be a number greater than zero."
msgstr "El importe tiene que ser un número mayor a cero." msgstr "El importe tiene que ser un número mayor a cero."
#: pkg/accounts.go:131
msgctxt "input"
msgid "Type"
msgstr "Tipo"
#: pkg/accounts.go:146 pkg/contacts.go:352 #: pkg/accounts.go:146 pkg/contacts.go:352
msgctxt "input" msgctxt "input"
msgid "IBAN" msgid "IBAN"

View File

@ -0,0 +1,7 @@
-- Revert numerus:add_collection from pg
begin;
drop function if exists numerus.add_collection(integer, integer, date, integer, text, text, numerus.tag_name[]);
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:attach_to_collection from pg
begin;
drop function if exists numerus.attach_to_collection(uuid, text, text, bytea);
commit;

View File

@ -1,10 +1,14 @@
-- Revert numerus:available_invoice_status from pg -- Deploy numerus:available_invoice_status to pg
-- requires: schema_numerus
-- requires: invoice_status
-- requires: invoice_status_i18n
begin; begin;
set search_path to numerus; set search_path to numerus;
delete from invoice_status_i18n; update invoice set invoice_status = 'created' where invoice_status = 'partial';
delete from invoice_status; delete from invoice_status_i18n where invoice_status = 'partial';
delete from invoice_status where invoice_status = 'partial';
commit; commit;

View File

@ -0,0 +1,10 @@
-- Revert numerus:available_invoice_status from pg
begin;
set search_path to numerus;
delete from invoice_status_i18n;
delete from invoice_status;
commit;

7
revert/collection.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert numerus:collection from pg
begin;
drop table if exists numerus.collection;
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:collection_attachment from pg
begin;
drop table if exists numerus.collection_attachment;
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:edit_collection from pg
begin;
drop function if exists numerus.edit_collection(uuid, date, integer, text, text, numerus.tag_name[]);
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:invoice_collection from pg
begin;
drop table if exists numerus.invoice_collection;
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:remove_collection from pg
begin;
drop function if exists numerus.remove_collection(uuid);
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:update_invoice_collection_status from pg
begin;
drop function if exists numerus.update_invoice_collection_status(integer, integer, integer);
commit;

View File

@ -157,3 +157,12 @@ attach_to_payment [roles schema_numerus payment payment_attachment] 2024-08-11T2
remove_payment [roles schema_numerus expense_payment payment payment_attachment update_expense_payment_status] 2024-08-11T00:30:58Z jordi fita mas <jordi@tandem.blog> # Add function to remove payments remove_payment [roles schema_numerus expense_payment payment payment_attachment update_expense_payment_status] 2024-08-11T00:30:58Z jordi fita mas <jordi@tandem.blog> # Add function to remove payments
add_expense [add_expense@v2] 2024-08-12T23:48:07Z jordi fita mas <jordi@tandem.blog> # Remove the status parameter from add_expense add_expense [add_expense@v2] 2024-08-12T23:48:07Z jordi fita mas <jordi@tandem.blog> # Remove the status parameter from add_expense
edit_expense [edit_expense@v2] 2024-08-12T23:53:48Z jordi fita mas <jordi@tandem.blog> # Remove the expense_status from edit_expense edit_expense [edit_expense@v2] 2024-08-12T23:53:48Z jordi fita mas <jordi@tandem.blog> # Remove the expense_status from edit_expense
collection [roles schema_numerus company payment_account currency tag_name payment_status extension_pgcrypto] 2024-08-18T02:54:28Z jordi fita mas <jordi@tandem.blog> # Add relation of cash (payment) collection
invoice_collection [roles schema_numerus invoice collection] 2024-08-18T03:22:09Z jordi fita mas <jordi@tandem.blog> # Add relation of invoice collections
available_invoice_status [available_invoice_status@v2] 2024-08-18T03:34:13Z jordi fita mas <jordi@tandem.blog> # Add “partial” invoice status
update_invoice_collection_status [roles schema_numerus invoice collection invoice_collection invoice_amount available_invoice_status available_payment_status] 2024-08-19T01:20:56Z jordi fita mas <jordi@tandem.blog> # Add function to update invoice and collection status
add_collection [roles schema_numerus collection invoice_collection company currency parse_price tag_name update_invoice_collection_status] 2024-08-19T01:26:01Z jordi fita mas <jordi@tandem.blog> # Add function to insert new collections
edit_collection [roles schema_numerus collection invoice_collection currency parse_price tag_name update_invoice_collection_status] 2024-08-19T23:58:36Z jordi fita mas <jordi@tandem.blog> # Add function to update collections
collection_attachment [roles schema_numerus collection] 2024-08-20T00:34:09Z jordi fita mas <jordi@tandem.blog> # Add relation of collection attachments
attach_to_collection [roles schema_numerus collection collection_attachment] 2024-08-20T00:41:53Z jordi fita mas <jordi@tandem.blog> # Add function to attach files to collections
remove_collection [roles schema_numerus invoice_collection collection collection_attachment update_invoice_collection_status] 2024-08-20T00:47:27Z jordi fita mas <jordi@tandem.blog> # Add function to remove collections

194
test/add_collection.sql Normal file
View File

@ -0,0 +1,194 @@
-- Test add_collection
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(20);
set search_path to numerus, auth, public;
select has_function('numerus', 'add_collection', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]']);
select function_lang_is('numerus', 'add_collection', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'plpgsql');
select function_returns('numerus', 'add_collection', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'uuid');
select isnt_definer('numerus', 'add_collection', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]']);
select volatility_is('numerus', 'add_collection', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'volatile');
select function_privs_are('numerus', 'add_collection', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_collection', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_collection', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_collection', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate invoice_collection cascade;
truncate collection cascade;
truncate payment_account cascade;
truncate invoice_product_tax cascade;
truncate invoice_product cascade;
truncate invoice cascade;
truncate contact_tax_details cascade;
truncate contact cascade;
truncate tax cascade;
truncate tax_class cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into tax_class (tax_class_id, company_id, name)
values (11, 1, 'tax')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (2, 1, 11, 'IRPF -15 %', -0.15)
, (3, 1, 11, 'IVA 4 %', 0.04)
, (4, 1, 11, 'IVA 10 %', 0.10)
;
insert into contact (contact_id, company_id, name)
values ( 9, 1, 'Customer 1')
, (10, 2, 'Customer 2')
;
insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code)
values (9, 'Customer 1', 'XX555', '', '', '', '', 'ES')
, (10, 'Customer 2', 'XX666', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, contact_id, invoice_date, payment_method_id, currency_code)
values (12, 1, 'REF123', 9, '2011-01-11', 111, 'EUR')
, (13, 2, 'INV001', 10, '2011-01-11', 111, 'USD')
, (14, 2, 'INV002', 10, '2022-02-22', 222, 'USD')
, (15, 2, 'INV003', 10, '2022-02-22', 222, 'USD')
, (16, 1, 'REF001', 9, '2023-03-03', 111, 'EUR')
, (17, 1, 'REF002', 9, '2023-03-03', 111, 'EUR')
, (18, 1, 'REF003', 9, '2023-03-03', 111, 'EUR')
;
insert into invoice_product (invoice_product_id, invoice_id, name, price)
values (19, 12, 'P1', 100)
, (20, 12, 'P2', 11)
, (21, 13, 'P1', 50)
, (22, 13, 'P2', 61)
, (23, 14, 'P1', 100)
, (24, 14, 'P2', 100)
, (25, 14, 'P3', 11)
, (26, 14, 'P4', 11)
, (27, 15, 'P1', 222)
, (28, 16, 'P*', 10000)
, (29, 17, 'P*', 10000)
, (30, 18, 'P*', 10000)
;
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
values (28, 3, 0.04)
, (29, 2, -0.15)
, (30, 2, -0.15)
, (30, 4, 0.10)
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values (11, 1, 'other', 'Other 1')
, (22, 2, 'cash', 'Cash 2')
;
select lives_ok(
$$ select add_collection(1, null, '2023-05-02', 11, '“Protection”', '11.11', array['tag1', 'tag2']) $$,
'Should be able to insert a collection, unrelated to any invoice, for the first company'
);
select lives_ok(
$$ select add_collection(2, 13, '2023-05-03', 22, 'Collection of INV001', '1.11', array[]::tag_name[]) $$,
'Should be able to insert a complete collection for the first invoice'
);
select lives_ok(
$$ select add_collection(2, 14, '2023-05-04', 22, 'First collection of INV002', '1.00', array[]::tag_name[]) $$,
'Should be able to insert a partial collection for the second invoice'
);
select lives_ok(
$$ select add_collection(2, 14, '2023-05-05', 22, 'Second collection of INV002', '1.22', array[]::tag_name[]) $$,
'Should be able to insert a partial, and final, collection for the second invoice'
);
select lives_ok(
$$ select add_collection(2, 15, '2023-05-06', 22, 'Partial collection of INV003', '1.11', array[]::tag_name[]) $$,
'Should be able to insert a partial collection for the third invoice'
);
select lives_ok(
$$ select add_collection(1, 16, '2023-03-06', 11, 'Re: REF001', '103.99', array[]::tag_name[]) $$,
'Should be able to collect an invoice with taxes'
);
select lives_ok(
$$ select add_collection(1, 17, '2023-03-06', 11, 'Re: REF002', '85', array[]::tag_name[]) $$,
'Should be able to collect an invoice with negative taxes'
);
select lives_ok(
$$ select add_collection(1, 18, '2023-03-06', 11, 'Re: REF003', '95', array[]::tag_name[]) $$,
'Should be able to collect an invoice with multiple taxes'
);
select bag_eq(
$$ select company_id, description, collection_date::text, payment_account_id, amount, currency_code, payment_status, tags::text, created_at from collection $$,
$$ values (1, '“Protection”', '2023-05-02', 11, 1111, 'EUR', 'complete', '{tag1,tag2}', current_timestamp)
, (2, 'Collection of INV001', '2023-05-03', 22, 111, 'USD', 'complete', '{}', current_timestamp)
, (2, 'First collection of INV002', '2023-05-04', 22, 100, 'USD', 'partial', '{}', current_timestamp)
, (2, 'Second collection of INV002', '2023-05-05', 22, 122, 'USD', 'partial', '{}', current_timestamp)
, (2, 'Partial collection of INV003', '2023-05-06', 22, 111, 'USD', 'partial', '{}', current_timestamp)
, (1, 'Re: REF001', '2023-03-06', 11, 10399, 'EUR', 'partial', '{}', current_timestamp)
, (1, 'Re: REF002', '2023-03-06', 11, 8500, 'EUR', 'complete', '{}', current_timestamp)
, (1, 'Re: REF003', '2023-03-06', 11, 9500, 'EUR', 'complete', '{}', current_timestamp)
$$,
'Should have created all collections'
);
select bag_eq(
$$ select invoice_id, description from invoice_collection join collection using (collection_id) $$,
$$ values (13, 'Collection of INV001')
, (14, 'First collection of INV002')
, (14, 'Second collection of INV002')
, (15, 'Partial collection of INV003')
, (16, 'Re: REF001')
, (17, 'Re: REF002')
, (18, 'Re: REF003')
$$,
'Should have linked all invoices to collections'
);
select bag_eq(
$$ select invoice_id, invoice_status from invoice $$,
$$ values (12, 'created')
, (13, 'paid')
, (14, 'paid')
, (15, 'partial')
, (16, 'partial')
, (17, 'paid')
, (18, 'paid')
$$,
'Should have updated the status of invoices'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,80 @@
-- Test attach_to_collection
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(12);
set search_path to numerus, public;
select has_function('numerus', 'attach_to_collection', array['uuid', 'text', 'text', 'bytea']);
select function_lang_is('numerus', 'attach_to_collection', array['uuid', 'text', 'text', 'bytea'], 'sql');
select function_returns('numerus', 'attach_to_collection', array['uuid', 'text', 'text', 'bytea'], 'void');
select isnt_definer('numerus', 'attach_to_collection', array['uuid', 'text', 'text', 'bytea']);
select volatility_is('numerus', 'attach_to_collection', array['uuid', 'text', 'text', 'bytea'], 'volatile');
select function_privs_are('numerus', 'attach_to_collection', array ['uuid', 'text', 'text', 'bytea'], 'guest', array []::text[]);
select function_privs_are('numerus', 'attach_to_collection', array ['uuid', 'text', 'text', 'bytea'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'attach_to_collection', array ['uuid', 'text', 'text', 'bytea'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'attach_to_collection', array ['uuid', 'text', 'text', 'bytea'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate collection_attachment cascade;
truncate collection cascade;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values (11, 1, 'cash', 'Cash 1')
, (12, 1, 'cash', 'Cash 2')
, (13, 1, 'other', 'Other')
;
insert into collection (collection_id, company_id, slug, description, collection_date, payment_account_id, amount, currency_code, payment_status)
values (16, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Collection 1', '2023-05-04', 12, 111, 'EUR', 'complete')
, (17, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Collection 2', '2023-05-05', 13, 100, 'EUR', 'partial')
;
insert into collection_attachment (collection_id, original_filename, mime_type, content)
values (17, 'something.txt', 'text/plain', convert_to('Once upon a time…', 'UTF-8'))
;
select lives_ok(
$$ select attach_to_collection('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'collection.txt', 'text/plain', convert_to('To receive 42 ', 'UTF-8')) $$,
'Should be able to attach a document to the first collection'
);
select lives_ok(
$$ select attach_to_collection('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'collection.html', 'text/html', convert_to('<html><p>To receive 42 €</p></html>', 'UTF-8')) $$,
'Should be able to replate the second collections attachment with a new document'
);
select bag_eq(
$$ select collection_id, original_filename, mime_type, convert_from(content, 'UTF-8') from collection_attachment $$,
$$ values (16, 'collection.txt', 'text/plain', 'To receive 42 ')
, (17, 'collection.html', 'text/html', '<html><p>To receive 42 €</p></html>')
$$,
'Should have attached all documents'
);
select *
from finish();
rollback;

187
test/collection.sql Normal file
View File

@ -0,0 +1,187 @@
-- Test collection
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(71);
set search_path to numerus, auth, public;
select has_table('collection');
select has_pk('collection');
select table_privs_are('collection', 'guest', array []::text[]);
select table_privs_are('collection', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('collection', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('collection', 'authenticator', array []::text[]);
select has_column('collection', 'collection_id');
select col_is_pk('collection', 'collection_id');
select col_type_is('collection', 'collection_id', 'integer');
select col_not_null('collection', 'collection_id');
select col_hasnt_default('collection', 'collection_id');
select has_column('collection', 'company_id');
select col_is_fk('collection', 'company_id');
select fk_ok('collection', 'company_id', 'company', 'company_id');
select col_type_is('collection', 'company_id', 'integer');
select col_not_null('collection', 'company_id');
select col_hasnt_default('collection', 'company_id');
select has_column('collection', 'slug');
select col_is_unique('collection', 'slug');
select col_type_is('collection', 'slug', 'uuid');
select col_not_null('collection', 'slug');
select col_has_default('collection', 'slug');
select col_default_is('collection', 'slug', 'gen_random_uuid()');
select has_column('collection', 'description');
select col_type_is('collection', 'description', 'text');
select col_not_null('collection', 'description');
select col_hasnt_default('collection', 'description');
select has_column('collection', 'collection_date');
select col_type_is('collection', 'collection_date', 'date');
select col_not_null('collection', 'collection_date');
select col_has_default('collection', 'collection_date');
select col_default_is('collection', 'collection_date', 'CURRENT_DATE');
select has_column('collection', 'payment_account_id');
select col_is_fk('collection', 'payment_account_id');
select fk_ok('collection', 'payment_account_id', 'payment_account', 'payment_account_id');
select col_type_is('collection', 'payment_account_id', 'integer');
select col_not_null('collection', 'payment_account_id');
select col_hasnt_default('collection', 'payment_account_id');
select has_column('collection', 'amount');
select col_type_is('collection', 'amount', 'integer');
select col_not_null('collection', 'amount');
select col_hasnt_default('collection', 'amount');
select has_column('collection', 'currency_code');
select col_is_fk('collection', 'currency_code');
select fk_ok('collection', 'currency_code', 'currency', 'currency_code');
select col_type_is('collection', 'currency_code', 'text');
select col_not_null('collection', 'currency_code');
select col_hasnt_default('collection', 'currency_code');
select has_column('collection', 'tags');
select col_type_is('collection', 'tags', 'tag_name[]');
select col_not_null('collection', 'tags');
select col_has_default('collection', 'tags');
select col_default_is('collection', 'tags', '{}');
select has_column('collection', 'payment_status');
select col_is_fk('collection', 'payment_status');
select fk_ok('collection', 'payment_status', 'payment_status', 'payment_status');
select col_type_is('collection', 'payment_status', 'text');
select col_not_null('collection', 'payment_status');
select col_has_default('collection', 'payment_status');
select col_default_is('collection', 'payment_status', 'complete');
select has_column('collection', 'created_at');
select col_type_is('collection', 'created_at', 'timestamp with time zone');
select col_not_null('collection', 'created_at');
select col_has_default('collection', 'created_at');
select col_default_is('collection', 'created_at', 'CURRENT_TIMESTAMP');
set client_min_messages to warning;
truncate collection cascade;
truncate payment_account cascade;
truncate company_user cascade;
truncate company cascade;
truncate payment_method cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values (221, 2, 'other', 'Other 2')
, (441, 4, 'other', 'Other 4')
;
insert into collection (company_id, description, payment_account_id, amount, currency_code)
values (2, 'Collection 20001', 221, 333, 'EUR')
, (4, 'Collection 40001', 441, 555, 'EUR')
;
prepare collection_data as
select company_id, description
from collection
order by company_id, description;
set role invoicer;
select is_empty('collection_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'collection_data',
$$ values (2, 'Collection 20001')
$$,
'Should only list collections from the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'collection_data',
$$ values (4, 'Collection 40001')
$$,
'Should only list collections from the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'collection_data',
'42501', 'permission denied for table collection',
'Should not allow select to guest users'
);
reset role;
select throws_ok(
$$ insert into collection (company_id, description, payment_account_id, amount, currency_code) values (2, 'Nope', 221, 0, 'EUR') $$,
'23514', 'new row for relation "collection" violates check constraint "collection_amount_positive"',
'Should not allow empty collections'
);
select throws_ok(
$$ insert into collection (company_id, description, payment_account_id, amount, currency_code) values (2, 'Nope', 221, -1, 'EUR') $$,
'23514', 'new row for relation "collection" violates check constraint "collection_amount_positive"',
'Should not allow negative collections'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,131 @@
-- Test collection_attachment
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(29);
set search_path to numerus, auth, public;
select has_table('collection_attachment');
select has_pk('collection_attachment');
select table_privs_are('collection_attachment', 'guest', array []::text[]);
select table_privs_are('collection_attachment', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('collection_attachment', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('collection_attachment', 'authenticator', array []::text[]);
select has_column('collection_attachment', 'collection_id');
select col_is_pk('collection_attachment', 'collection_id');
select col_is_fk('collection_attachment', 'collection_id');
select fk_ok('collection_attachment', 'collection_id', 'collection', 'collection_id');
select col_type_is('collection_attachment', 'collection_id', 'integer');
select col_not_null('collection_attachment', 'collection_id');
select col_hasnt_default('collection_attachment', 'collection_id');
select has_column('collection_attachment', 'original_filename');
select col_type_is('collection_attachment', 'original_filename', 'text');
select col_not_null('collection_attachment', 'original_filename');
select col_hasnt_default('collection_attachment', 'original_filename');
select has_column('collection_attachment', 'mime_type');
select col_type_is('collection_attachment', 'mime_type', 'text');
select col_not_null('collection_attachment', 'mime_type');
select col_hasnt_default('collection_attachment', 'mime_type');
select has_column('collection_attachment', 'content');
select col_type_is('collection_attachment', 'content', 'bytea');
select col_not_null('collection_attachment', 'content');
select col_hasnt_default('collection_attachment', 'content');
set client_min_messages to warning;
truncate collection_attachment cascade;
truncate collection cascade;
truncate company_user cascade;
truncate payment_method cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into payment_account(payment_account_id, company_id, payment_account_type, name)
values (8, 2, 'other', 'Other 2')
, (9, 4, 'other', 'Other 4')
;
insert into collection (collection_id, company_id, description, payment_account_id, collection_date, amount, currency_code)
values (13, 2, 'Payment 2', 8, '2011-01-11', 111, 'EUR')
, (14, 4, 'Payment 4', 9, '2022-02-22', 222, 'EUR')
;
insert into collection_attachment (collection_id, original_filename, mime_type, content)
values (13, 'collection.txt', 'text/plain', convert_to('Collection 42', 'UTF8'))
, (14, 'collection.html', 'text/html', convert_to('<html>Collection <em>42</em></html>', 'UTF8'))
;
prepare collection_attachment_data as
select collection_id, original_filename
from collection_attachment
order by collection_id, original_filename;
set role invoicer;
select is_empty('collection_attachment_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'collection_attachment_data',
$$ values (13, 'collection.txt')
$$,
'Should only list collection attachmements of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'collection_attachment_data',
$$ values (14, 'collection.html')
$$,
'Should only list collection attachmements of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'collection_attachment_data',
'42501', 'permission denied for table collection_attachment',
'Should not allow select to guest users'
);
reset role;
select *
from finish();
rollback;

183
test/edit_collection.sql Normal file
View File

@ -0,0 +1,183 @@
-- Test edit_collection
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(17);
set search_path to numerus, public;
select has_function('numerus', 'edit_collection', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]']);
select function_lang_is('numerus', 'edit_collection', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'plpgsql');
select function_returns('numerus', 'edit_collection', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'uuid');
select isnt_definer('numerus', 'edit_collection', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]']);
select volatility_is('numerus', 'edit_collection', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'volatile');
select function_privs_are('numerus', 'edit_collection', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'edit_collection', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_collection', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_collection', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate invoice_collection cascade;
truncate collection cascade;
truncate invoice_product_tax cascade;
truncate invoice_product cascade;
truncate invoice cascade;
truncate contact_tax_details cascade;
truncate contact cascade;
truncate tax cascade;
truncate tax_class cascade;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into tax_class (tax_class_id, company_id, name)
values (11, 1, 'tax')
;
insert into tax (tax_id, company_id, tax_class_id, name, rate)
values (2, 1, 11, 'IRPF -15 %', -0.15)
, (3, 1, 11, 'IVA 4 %', 0.04)
, (4, 1, 11, 'IVA 10 %', 0.10)
;
insert into contact (contact_id, company_id, name)
values ( 9, 1, 'Customer 1')
;
insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code)
values (9, 'Customer 1', 'XX555', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, contact_id, invoice_date, payment_method_id, currency_code, invoice_status)
values (13, 1, 'INV001', 9, '2011-01-11', 111, 'EUR', 'paid')
, (14, 1, 'INV002', 9, '2022-02-22', 111, 'EUR', 'paid')
, (15, 1, 'INV003', 9, '2022-02-22', 111, 'EUR', 'partial')
, (16, 1, 'REF001', 9, '2023-03-03', 111, 'EUR', 'paid')
, (17, 1, 'REF002', 9, '2023-03-03', 111, 'EUR', 'paid')
, (18, 1, 'REF003', 9, '2023-03-03', 111, 'EUR', 'paid')
;
insert into invoice_product (invoice_product_id, invoice_id, name, price)
values (19, 13, 'P1', 111)
, (20, 14, 'P1', 111)
, (21, 14, 'P2', 111)
, (22, 15, 'P2', 111)
, (23, 15, 'P2', 111)
, (24, 15, 'P2', 111)
, (25, 16, 'P1', 10000)
, (26, 17, 'P1', 10000)
, (27, 18, 'P1', 10000)
;
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
values (25, 3, 0.04)
, (26, 2, -0.15)
, (27, 2, -0.15)
, (27, 4, 0.10)
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values (11, 1, 'cash', 'Cash 1')
, (12, 1, 'cash', 'Cash 2')
, (13, 1, 'other', 'Other')
;
insert into collection (collection_id, company_id, slug, description, collection_date, payment_account_id, amount, currency_code, payment_status, tags)
values (16, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Payment INV001', '2023-05-04', 12, 111, 'EUR', 'complete', '{tag1}')
, (17, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'First INV002', '2023-05-05', 13, 100, 'EUR', 'partial', '{tag2}')
, (18, 1, '3bdad7a8-4a1e-4ae0-b5c6-015e51ee0502', 'Second INV002', '2023-05-06', 13, 122, 'EUR', 'partial', '{tag1,tag3}')
, (19, 1, '5a524bee-8311-4d13-9adf-ef6310b26990', 'Partial INV003', '2023-05-07', 11, 123, 'EUR', 'partial', '{}')
, (20, 1, '65222c3b-4faa-4be4-b39c-5bd170a943cf', 'Re: REF001', '2023-03-07', 11, 10400, 'EUR', 'complete', '{}')
, (21, 1, 'dbb699cf-d1f4-40ff-96cb-8f29e238d51d', 'Re: REF002', '2023-03-07', 11, 8500, 'EUR', 'complete', '{}')
, (22, 1, '0756a50f-2957-4661-abd2-e422a848af4e', 'Re: REF003', '2023-03-07', 11, 9500, 'EUR', 'complete', '{}')
;
insert into invoice_collection (invoice_id, collection_id)
values (13, 16)
, (14, 17)
, (14, 18)
, (15, 19)
, (16, 20)
, (17, 21)
, (18, 22)
;
select lives_ok(
$$ select edit_collection('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', '2023-05-06', 13, 'Partial INV001', '1.00', array['tag1']) $$,
'Should be able to change a complete collection to partial'
);
select lives_ok(
$$ select edit_collection('b57b980b-247b-4be4-a0b7-03a7819c53ae', '2023-05-07', 12, 'First INV002', '0.50', array['tag1', 'tag3']) $$,
'Should be able to adjust a partial collection, that is still partial, and the invoice now becomes partial'
);
select lives_ok(
$$ select edit_collection('5a524bee-8311-4d13-9adf-ef6310b26990', '2023-05-01', 11, 'Complete INV003', '3.33', array[]::tag_name[]) $$,
'Should be able to complete a previously partial collection'
);
select lives_ok(
$$ select edit_collection('65222c3b-4faa-4be4-b39c-5bd170a943cf', '2023-03-10', 11, 'Re: REF001', '103.99', array[]::tag_name[]) $$,
'Should be able to make partial a collection with tax.'
);
select lives_ok(
$$ select edit_collection('dbb699cf-d1f4-40ff-96cb-8f29e238d51d', '2023-03-10', 11, 'Re: REF002', '84.99', array[]::tag_name[]) $$,
'Should be able to make partial a collection with negative tax.'
);
select lives_ok(
$$ select edit_collection('0756a50f-2957-4661-abd2-e422a848af4e', '2023-03-10', 11, 'Re: REF003', '94.99', array[]::tag_name[]) $$,
'Should be able to make partial a collection with multiple taxe.'
);
select bag_eq(
$$ select description, collection_date::text, payment_account_id, amount, payment_status, tags::text from collection $$,
$$ values ('Partial INV001', '2023-05-06', 13, 100, 'partial', '{tag1}')
, ('First INV002', '2023-05-07', 12, 50, 'partial', '{tag1,tag3}')
, ('Second INV002', '2023-05-06', 13, 122, 'partial', '{tag1,tag3}')
, ('Complete INV003', '2023-05-01', 11, 333, 'complete', '{}')
, ('Re: REF001', '2023-03-10', 11, 10399, 'partial', '{}')
, ('Re: REF002', '2023-03-10', 11, 8499, 'partial', '{}')
, ('Re: REF003', '2023-03-10', 11, 9499, 'partial', '{}')
$$,
'Should have updated all collections'
);
select bag_eq(
$$ select invoice_id, invoice_status from invoice $$,
$$ values (13, 'partial')
, (14, 'partial')
, (15, 'paid')
, (16, 'partial')
, (17, 'partial')
, (18, 'partial')
$$,
'Should have updated invoices too'
);
select *
from finish();
rollback;

142
test/invoice_collection.sql Normal file
View File

@ -0,0 +1,142 @@
-- Test invoice_collection
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(23);
set search_path to numerus, auth, public;
select has_table('invoice_collection');
select has_pk('invoice_collection');
select col_is_pk('invoice_collection', array['invoice_id', 'collection_id']);
select table_privs_are('invoice_collection', 'guest', array []::text[]);
select table_privs_are('invoice_collection', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('invoice_collection', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('invoice_collection', 'authenticator', array []::text[]);
select has_column('invoice_collection', 'invoice_id');
select col_is_fk('invoice_collection', 'invoice_id');
select fk_ok('invoice_collection', 'invoice_id', 'invoice', 'invoice_id');
select col_type_is('invoice_collection', 'invoice_id', 'integer');
select col_not_null('invoice_collection', 'invoice_id');
select col_hasnt_default('invoice_collection', 'invoice_id');
select has_column('invoice_collection', 'collection_id');
select col_is_fk('invoice_collection', 'collection_id');
select fk_ok('invoice_collection', 'collection_id', 'collection', 'collection_id');
select col_type_is('invoice_collection', 'collection_id', 'integer');
select col_not_null('invoice_collection', 'collection_id');
select col_hasnt_default('invoice_collection', 'collection_id');
set client_min_messages to warning;
truncate invoice_collection cascade;
truncate collection cascade;
truncate payment_account cascade;
truncate invoice cascade;
truncate contact_tax_details cascade;
truncate contact cascade;
truncate company_user cascade;
truncate payment_method cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (444, 4, 'cash', 'cash')
, (222, 2, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into company_user (company_id, user_id)
values (2, 1)
, (4, 5)
;
insert into contact (contact_id, company_id, name)
values ( 9, 2, 'Customer 1')
, (10, 4, 'Customer 2')
;
insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code)
values (9, 'Customer 1', 'XX555', '', '', '', '', 'ES')
, (10, 'Customer 2', 'XX666', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, contact_id, invoice_date, payment_method_id, currency_code)
values (13, 2, 'INV001', 9, '2011-01-11', 222, 'EUR')
, (14, 4, 'INV002', 10, '2022-02-22', 444, 'EUR')
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values (17, 2, 'cash', 'Cash 2')
, (18, 4, 'cash', 'Cash 4')
;
insert into collection (collection_id, company_id, description, collection_date, payment_account_id, amount, currency_code)
values (21, 2, 'Collection INV001', '2022-01-11', 17, 111, 'EUR')
, (22, 4, 'Collection INV002', '2022-02-23', 18, 222, 'EUR')
;
insert into invoice_collection (invoice_id, collection_id)
values (13, 21)
, (14, 22)
;
prepare invoice_collection_data as
select invoice_id, collection_id
from invoice_collection
order by invoice_id, collection_id;
set role invoicer;
select is_empty('invoice_collection_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select bag_eq(
'invoice_collection_data',
$$ values (13, 21)
$$,
'Should only list tax of products of the companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select bag_eq(
'invoice_collection_data',
$$ values (14, 22)
$$,
'Should only list tax of products of the companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'invoice_collection_data',
'42501', 'permission denied for table invoice_collection',
'Should not allow select to guest users'
);
reset role;
select *
from finish();
rollback;

146
test/remove_collection.sql Normal file
View File

@ -0,0 +1,146 @@
-- Test remove_collection
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(16);
set search_path to numerus, public;
select has_function('numerus', 'remove_collection', array['uuid']);
select function_lang_is('numerus', 'remove_collection', array['uuid'], 'plpgsql');
select function_returns('numerus', 'remove_collection', array['uuid'], 'void');
select isnt_definer('numerus', 'remove_collection', array['uuid']);
select volatility_is('numerus', 'remove_collection', array['uuid'], 'volatile');
select function_privs_are('numerus', 'remove_collection', array ['uuid'], 'guest', array []::text[]);
select function_privs_are('numerus', 'remove_collection', array ['uuid'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'remove_collection', array ['uuid'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'remove_collection', array ['uuid'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate invoice_collection;
truncate collection_attachment;
truncate collection cascade;
truncate invoice_product cascade;
truncate invoice cascade;
truncate contact_tax_details cascade;
truncate contact cascade;
truncate payment_account cascade;
truncate payment_method cascade;
truncate company cascade;
reset client_min_messages;
set constraints "company_default_payment_method_id_fkey" deferred;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
;
insert into payment_method (payment_method_id, company_id, name, instructions)
values (111, 1, 'cash', 'cash')
;
set constraints "company_default_payment_method_id_fkey" immediate;
insert into contact (contact_id, company_id, name)
values ( 9, 1, 'Customer 1')
;
insert into contact_tax_details (contact_id, business_name, vatin, address, city, province, postal_code, country_code)
values (9, 'Customer 1', 'XX555', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, contact_id, invoice_date, payment_method_id, currency_code, invoice_status)
values (13, 1, 'INV001', 9, '2011-01-11', 111, 'EUR', 'paid')
, (14, 1, 'INV002', 9, '2022-02-22', 111, 'EUR', 'paid')
, (15, 1, 'INV003', 9, '2022-02-22', 111, 'EUR', 'partial')
;
insert into invoice_product (invoice_product_id, invoice_id, name, price)
values (16, 13, 'P1', 111)
, (17, 14, 'P1', 111)
, (18, 14, 'P2', 111)
, (19, 15, 'P1', 111)
, (20, 15, 'P2', 111)
, (21, 15, 'P3', 111)
;
insert into payment_account (payment_account_id, company_id, payment_account_type, name)
values (11, 1, 'cash', 'Cash 1')
, (12, 1, 'cash', 'Cash 2')
, (13, 1, 'other', 'Other')
;
insert into collection (collection_id, company_id, slug, description, collection_date, payment_account_id, amount, currency_code, payment_status, tags)
values (16, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Collection INV001', '2023-05-04', 12, 111, 'EUR', 'complete', '{tag1}')
, (17, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'First INV002', '2023-05-05', 13, 100, 'EUR', 'partial', '{tag2}')
, (18, 1, '3bdad7a8-4a1e-4ae0-b5c6-015e51ee0502', 'Second INV002', '2023-05-06', 13, 122, 'EUR', 'partial', '{tag1,tag3}')
, (19, 1, '5a524bee-8311-4d13-9adf-ef6310b26990', 'Partial INV003', '2023-05-07', 11, 123, 'EUR', 'partial', '{}')
;
insert into invoice_collection (invoice_id, collection_id)
values (13, 16)
, (14, 17)
, (14, 18)
, (15, 19)
;
insert into collection_attachment (collection_id, original_fileName, mime_type, content)
values (16, 'collection.txt', 'text/plain', convert_to('Pay 42', 'UTF-8'))
, (18, 'empty.html', 'text/html', convert_to('empty', 'UTF-8'))
, (19, 'collection.html', 'text/html', convert_to('<html> PAY <em>42</em></html>', 'UTF-8'))
;
select lives_ok(
$$ select remove_collection('7ac3ae0e-b0c1-4206-a19b-0be20835edd4') $$,
'Should be able to remove a complete collection'
);
select lives_ok(
$$ select remove_collection('5a524bee-8311-4d13-9adf-ef6310b26990') $$,
'Should be able to remove a partial collection, '
);
select lives_ok(
$$ select remove_collection('b57b980b-247b-4be4-a0b7-03a7819c53ae') $$,
'Should be able to remove a partial collection, leaving the invoices other partial collection'
);
select bag_eq(
$$ select description, collection_date::text, payment_account_id, amount, payment_status, tags::text from collection $$,
$$ values ('Second INV002', '2023-05-06', 13, 122, 'partial', '{tag1,tag3}')
$$,
'Should have deleted all given collections'
);
select bag_eq(
$$ select invoice_id, collection_id from invoice_collection$$,
$$ values (14, 18)
$$,
'Should have deleted all related invoices collections'
);
select bag_eq(
$$ select collection_id, original_filename from collection_attachment $$,
$$ values (18, 'empty.html') $$,
'Should have deleted all related attachments'
);
select bag_eq(
$$ select invoice_id, invoice_status from invoice $$,
$$ values (13, 'created')
, (14, 'partial')
, (15, 'created')
$$,
'Should have updated invoices too'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,27 @@
-- Test update_invoice_collection_status
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(9);
set search_path to numerus, public;
select has_function('numerus', 'update_invoice_collection_status', array['integer', 'integer', 'integer']);
select function_lang_is('numerus', 'update_invoice_collection_status', array['integer', 'integer', 'integer'], 'sql');
select function_returns('numerus', 'update_invoice_collection_status', array['integer', 'integer', 'integer'], 'void');
select isnt_definer('numerus', 'update_invoice_collection_status', array['integer', 'integer', 'integer']);
select volatility_is('numerus', 'update_invoice_collection_status', array['integer', 'integer', 'integer'], 'volatile');
select function_privs_are('numerus', 'update_invoice_collection_status', array ['integer', 'integer', 'integer'], 'guest', array []::text[]);
select function_privs_are('numerus', 'update_invoice_collection_status', array ['integer', 'integer', 'integer'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'update_invoice_collection_status', array ['integer', 'integer', 'integer'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'update_invoice_collection_status', array ['integer', 'integer', 'integer'], 'authenticator', array []::text[]);
select *
from finish();
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:add_collection on pg
begin;
select has_function_privilege('numerus.add_collection(integer, integer, date, integer, text, text, numerus.tag_name[])', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:attach_to_collection on pg
begin;
select has_function_privilege('numerus.attach_to_collection(uuid, text, text, bytea)', 'execute');
rollback;

View File

@ -6,6 +6,7 @@ set search_path to numerus;
select 1 / count(*) from invoice_status where invoice_status = 'created' and name ='Created'; select 1 / count(*) from invoice_status where invoice_status = 'created' and name ='Created';
select 1 / count(*) from invoice_status where invoice_status = 'sent' and name ='Sent'; select 1 / count(*) from invoice_status where invoice_status = 'sent' and name ='Sent';
select 1 / count(*) from invoice_status where invoice_status = 'partial' and name ='Partial';
select 1 / count(*) from invoice_status where invoice_status = 'paid' and name ='Paid'; select 1 / count(*) from invoice_status where invoice_status = 'paid' and name ='Paid';
select 1 / count(*) from invoice_status where invoice_status = 'unpaid' and name ='Unpaid'; select 1 / count(*) from invoice_status where invoice_status = 'unpaid' and name ='Unpaid';
@ -13,6 +14,8 @@ select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' an
select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' and name ='Creada' and lang_tag = 'es'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' and name ='Creada' and lang_tag = 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'sent' and name ='Enviada' and lang_tag= 'ca'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'sent' and name ='Enviada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'sent' and name ='Enviada' and lang_tag= 'es'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'sent' and name ='Enviada' and lang_tag= 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'partial' and name ='Parcial' and lang_tag = 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'partial' and name ='Parcial' and lang_tag = 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'ca'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'es'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'unpaid' and name ='No cobrada' and lang_tag= 'ca'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'unpaid' and name ='No cobrada' and lang_tag= 'ca';

View File

@ -0,0 +1,21 @@
-- Verify numerus:available_invoice_status on pg
begin;
set search_path to numerus;
select 1 / count(*) from invoice_status where invoice_status = 'created' and name ='Created';
select 1 / count(*) from invoice_status where invoice_status = 'sent' and name ='Sent';
select 1 / count(*) from invoice_status where invoice_status = 'paid' and name ='Paid';
select 1 / count(*) from invoice_status where invoice_status = 'unpaid' and name ='Unpaid';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' and name ='Creada' and lang_tag = 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' and name ='Creada' and lang_tag = 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'sent' and name ='Enviada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'sent' and name ='Enviada' and lang_tag= 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'unpaid' and name ='No cobrada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'unpaid' and name ='No cobrada' and lang_tag= 'es';
rollback;

22
verify/collection.sql Normal file
View File

@ -0,0 +1,22 @@
-- Verify numerus:collection on pg
begin;
select collection_id
, company_id
, slug
, description
, collection_date
, payment_account_id
, amount
, currency_code
, tags
, payment_status
, created_at
from numerus.collection
where false;
select 1 / count(*) from pg_class where oid = 'numerus.collection'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.collection'::regclass;
rollback;

View File

@ -0,0 +1,15 @@
-- Verify numerus:collection_attachment on pg
begin;
select collection_id
, original_filename
, mime_type
, content
from numerus.collection_attachment
where false;
select 1 / count(*) from pg_class where oid = 'numerus.collection_attachment'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.collection_attachment'::regclass;
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:edit_collection on pg
begin;
select has_function_privilege('numerus.edit_collection(uuid, date, integer, text, text, numerus.tag_name[])', 'execute');
rollback;

View File

@ -0,0 +1,13 @@
-- Verify numerus:invoice_collection on pg
begin;
select invoice_id
, collection_id
from numerus.invoice_collection
where false;
select 1 / count(*) from pg_class where oid = 'numerus.invoice_collection'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.invoice_collection'::regclass;
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:remove_collection on pg
begin;
select has_function_privilege('numerus.remove_collection(uuid)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:update_invoice_collection_status on pg
begin;
select has_function_privilege('numerus.update_invoice_collection_status(integer, integer, integer)', 'execute');
rollback;

View File

@ -696,6 +696,7 @@ main > nav {
.payment-status-partial, .payment-status-partial,
.expense-status-partial, .expense-status-partial,
.quote-status-sent, .quote-status-sent,
.invoice-status-partial,
.invoice-status-sent { .invoice-status-sent {
background-color: var(--numerus--color--hay); background-color: var(--numerus--color--hay);
} }

View File

@ -1,6 +1,10 @@
{{ define "title" -}} {{ define "title" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentForm*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentForm*/ -}}
{{ printf ( pgettext "Edit Payment “%s”" "title" ) .Description }} {{- if eq .Type.Value "P" -}}
{{ printf ( pgettext "Edit Payment “%s”" "title" ) .Description }}
{{- else -}}
{{ printf ( pgettext "Edit Collection “%s”" "title" ) .Description }}
{{- end -}}
{{- end }} {{- end }}
{{ define "breadcrumbs" -}} {{ define "breadcrumbs" -}}
@ -30,6 +34,7 @@
{{ csrfToken }} {{ csrfToken }}
{{ putMethod }} {{ putMethod }}
{{ template "hidden-select-field" .Type }}
{{ template "select-field" .PaymentAccount }} {{ template "select-field" .PaymentAccount }}
{{ template "input-field" .Description }} {{ template "input-field" .Description }}
{{ template "input-field" .PaymentDate }} {{ template "input-field" .PaymentDate }}

View File

@ -45,7 +45,11 @@
<td><a href="{{ $.BaseURI }}/{{ .Slug }}">{{ .Description }}</a></td> <td><a href="{{ $.BaseURI }}/{{ .Slug }}">{{ .Description }}</a></td>
<td> <td>
{{- if .InvoiceNumber -}} {{- if .InvoiceNumber -}}
<a href="{{ companyURI "/expenses/"}}{{ .ExpenseSlug }}">{{ .InvoiceNumber }}</a> {{- if eq .Type "P" -}}
<a href="{{ companyURI "/expenses/"}}{{ .DocumentSlug }}">{{ .InvoiceNumber }}</a>
{{- else -}}
<a href="{{ companyURI "/invoice/"}}{{ .DocumentSlug }}">{{ .InvoiceNumber }}</a>
{{- end -}}
{{- end -}} {{- end -}}
</td> </td>
<td class="payment-status-{{ .Status }}">{{ .StatusLabel }}</td> <td class="payment-status-{{ .Status }}">{{ .StatusLabel }}</td>

View File

@ -28,6 +28,11 @@
data-hx-boost="true"> data-hx-boost="true">
{{ csrfToken }} {{ csrfToken }}
{{- if .Expense -}}
{{ template "hidden-select-field" .Type }}
{{- else -}}
{{ template "select-field" .Type }}
{{- end -}}
{{ template "select-field" .PaymentAccount }} {{ template "select-field" .PaymentAccount }}
{{ template "input-field" .Description }} {{ template "input-field" .Description }}
{{ template "input-field" .PaymentDate }} {{ template "input-field" .PaymentDate }}