diff --git a/demo/demo.sql b/demo/demo.sql index 424bb83..846c271 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -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, 'Teia', 'Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.', '7.00', array[124], array['obsolet']); +alter 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_product_invoice_product_id_seq restart with 123; select add_invoice(123, (current_date - '338 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}'); +select add_collection(123, 123, (current_date - '337 days'::interval)::date, 123, 'Cobrament de FRA123', '1200.50', '{}'); select add_invoice(123, (current_date - '334 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}'); +select add_collection(123, 124, (current_date - '330 days'::interval)::date, 123, 'Cobrament de FRA124', '1200.50', '{}'); select add_invoice(123, (current_date - '327 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a l’aigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); +select add_collection(123, 125, (current_date - '317 days'::interval)::date, 123, 'Cobrament de FRA125', '1200.50', '{}'); select add_invoice(123, (current_date - '317 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 126, (current_date - '316 days'::interval)::date, 124, 'Cobrament de FRA126', '12.87', '{}'); select add_invoice(123, (current_date - '314 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a l’aigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); +select add_collection(123, 127, (current_date - '310 days'::interval)::date, 123, 'Cobrament de FRA127', '1200.50', '{}'); select add_invoice(123, (current_date - '311 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 128, (current_date - '311 days'::interval)::date, 124, 'Cobrament de FRA128', '12.87', '{}'); select add_invoice(123, (current_date - '278 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}'); +select add_collection(123, 129, (current_date - '270 days'::interval)::date, 123, 'Cobrament de FRA129', '1200.50', '{}'); select add_invoice(123, (current_date - '274 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 130, (current_date - '272 days'::interval)::date, 123, 'Cobrament de FRA130', '691.90', '{}'); select add_invoice(123, (current_date - '267 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que s’usa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 131, (current_date - '265 days'::interval)::date, 123, 'Cobrament de FRA131', '87.50', '{}'); select add_invoice(123, (current_date - '257 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que s’usa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 132, (current_date - '250 days'::interval)::date, 124, 'Cobrament de FRA132', '87.50', '{}'); select add_invoice(123, (current_date - '254 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 133, (current_date - '220 days'::interval)::date, 123, 'Cobrament de FRA133', '691.90', '{}'); select add_invoice(123, (current_date - '251 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}'); +select add_collection(123, 134, (current_date - '245 days'::interval)::date, 123, 'Cobrament de FRA134', '1200.50', '{}'); select add_invoice(123, (current_date - '208 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 135, (current_date - '190 days'::interval)::date, 123, 'Cobrament de FRA135', '691.90', '{}'); select add_invoice(123, (current_date - '204 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 136, (current_date - '200 days'::interval)::date, 123, 'Cobrament de FRA136', '691.90', '{}'); select add_invoice(123, (current_date - '197 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}'); +select add_collection(123, 137, (current_date - '190 days'::interval)::date, 123, 'Cobrament de FRA137', '1200.50', '{}'); select add_invoice(123, (current_date - '187 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 138, (current_date - '186 days'::interval)::date, 124, 'Cobrament de FRA138', '12.87', '{}'); select add_invoice(123, (current_date - '184 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a l’aigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); +select add_collection(123, 139, (current_date - '181 days'::interval)::date, 123, 'Cobrament de FRA139', '1200.50', '{}'); select add_invoice(123, (current_date - '181 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}'); +select add_collection(123, 140, (current_date - '177 days'::interval)::date, 123, 'Cobrament de FRA140', '1200.50', '{}'); select add_invoice(123, (current_date - '148 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que s’usa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 141, (current_date - '140 days'::interval)::date, 123, 'Cobrament de FRA141', '87.50', '{}'); select add_invoice(123, (current_date - '144 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que s’usa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 142, (current_date - '143 days'::interval)::date, 123, 'Cobrament de FRA142', '87.50', '{}'); select add_invoice(123, (current_date - '137 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a l’aigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); +select add_collection(123, 143, (current_date - '137 days'::interval)::date, 123, 'Cobrament de FRA143', '1200.50', '{}'); select add_invoice(123, (current_date - '127 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}'); +select add_collection(123, 144, (current_date - '120 days'::interval)::date, 123, 'Cobrament de FRA144', '1200.50', '{}'); select add_invoice(123, (current_date - '124 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}'); +select add_collection(123, 145, (current_date - '123 days'::interval)::date, 123, 'Cobrament de FRA145', '1200.50', '{}'); select add_invoice(123, (current_date - '121 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 146, (current_date - '101 days'::interval)::date, 124, 'Cobrament de FRA146', '12.87', '{}'); select add_invoice(123, (current_date - '78 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que s’usa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 147, (current_date - '60 days'::interval)::date, 123, 'Cobrament de FRA147', '87.50', '{}'); select add_invoice(123, (current_date - '74 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}'); +select add_collection(123, 148, (current_date - '61 days'::interval)::date, 123, 'Cobrament de FRA148', '1200.50', '{}'); select add_invoice(123, (current_date - '67 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 149, (current_date - '66 days'::interval)::date, 124, 'Cobrament de FRA149', '12.87', '{}'); select add_invoice(123, (current_date - '57 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a l’aigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); +select add_collection(123, 150, (current_date - '55 days'::interval)::date, 123, 'Cobrament de FRA150', '1200.50', '{}'); select add_invoice(123, (current_date - '54 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 151, (current_date - '50 days'::interval)::date, 124, 'Cobrament de FRA151', '691.90', '{}'); select add_invoice(123, (current_date - '51 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}'); +select add_collection(123, 152, (current_date - '44 days'::interval)::date, 123, 'Cobrament de FRA152', '1200.50', '{}'); select add_invoice(123, (current_date - '28 days'::interval)::date, 128, 'Vol esmorzar!', 123, '{producte}','{"(129,Teia,\"Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.\",7.00,1,0.0,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); +select add_collection(123, 153, (current_date - '28 days'::interval)::date, 124, 'Cobrament de FRA153', '12.87', '{}'); select add_invoice(123, (current_date - '24 days'::interval)::date, 127, '', 123, '{producte,bestia}','{"(128,Palla,Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.,25.00,25,0.0,{125})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '17 days'::interval)::date, 126, '', 123, '{producte,higiene}','{"(126,\"Paper higiènic (pack de 32 U)\",Paper que s’usa per mantenir la higiene personal després de defecar o orinar.,7.99,10,0.0,{126})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{124})"}'); select add_invoice(123, (current_date - '7 days'::interval)::date, 125, '', 123, '{producte,mag}','{"(125,Mirra,Goma resinosa aromàtica de color gris groguenc i gust amargant.,7.22,144,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.45,1,0.0,{124})"}'); select add_invoice(123, (current_date - '4 days'::interval)::date, 124, '', 123, '{producte,mag}','{"(124,Encens,Goma resina fragrant que desprèn una olor característica quan es crema.,2.26,460,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",4.53,1,0.0,{124})"}'); +select add_collection(123, 157, (current_date - '2 days'::interval)::date, 123, 'Primer cobrament de FRA157', '1000.00', '{}'); select add_invoice(123, (current_date - '1 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a l’aigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); -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 = '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; select add_expense(123, (date_trunc('month', current_date) - '11 months + 14 day'::interval)::date, 130, 'ABC123', '256.12', '{124}', '{}'); select add_payment(123, 123, (date_trunc('month', current_date) - '11 months + 04 day'::interval)::date, 123, 'Pagament d’ABC123', '256.12', '{}'); diff --git a/deploy/add_collection.sql b/deploy/add_collection.sql new file mode 100644 index 0000000..b0a3cb6 --- /dev/null +++ b/deploy/add_collection.sql @@ -0,0 +1,68 @@ +-- Deploy numerus:add_collection to pg +-- requires: roles +-- requires: schema_numerus +-- requires: collection +-- requires: invoice_collection +-- requires: company +-- requires: currency +-- requires: parse_price +-- requires: tag_name +-- requires: update_invoice_collection_status + +begin; + +set search_path to numerus, public; + +create or replace function add_collection(company integer, invoice_id integer, collection_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as +$$ +declare + cid integer; + cslug uuid; + amount_cents integer; +begin + insert into collection + ( company_id + , payment_account_id + , description + , collection_date + , amount + , currency_code + , payment_status + , tags + ) + select company_id + , payment_account_id + , description + , collection_date + , parse_price(amount, currency.decimal_digits) + , currency_code + , 'complete' + , tags + from company + join currency using (currency_code) + where company.company_id = add_collection.company + returning collection_id, slug, collection.amount + into cid, cslug, amount_cents + ; + + if invoice_id is not null then + -- must be inserted before updating statuses, so that it can see this + -- collection’s amount too. + insert into invoice_collection (invoice_id, collection_id) + values (invoice_id, cid) + ; + + perform update_invoice_collection_status(cid, invoice_id, amount_cents); + end if; + + return cslug; +end +$$ + language plpgsql +; + +revoke execute on function add_collection(integer, integer, date, integer, text, text, tag_name[]) from public; +grant execute on function add_collection(integer, integer, date, integer, text, text, tag_name[]) to invoicer; +grant execute on function add_collection(integer, integer, date, integer, text, text, tag_name[]) to admin; + +commit; diff --git a/deploy/attach_to_collection.sql b/deploy/attach_to_collection.sql new file mode 100644 index 0000000..3d97236 --- /dev/null +++ b/deploy/attach_to_collection.sql @@ -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; diff --git a/deploy/available_invoice_status.sql b/deploy/available_invoice_status.sql index c11a59f..1df6628 100644 --- a/deploy/available_invoice_status.sql +++ b/deploy/available_invoice_status.sql @@ -10,19 +10,24 @@ set search_path to numerus; insert into invoice_status (invoice_status, name) values ('created', 'Created') , ('sent', 'Sent') + , ('partial', 'Partial') , ('paid', 'Paid') , ('unpaid', 'Unpaid') +on conflict (invoice_status) do nothing ; insert into invoice_status_i18n (invoice_status, lang_tag, name) values ('created', 'ca', 'Creada') , ('sent', 'ca', 'Enviada') + , ('partial', 'ca', 'Parcial') , ('paid', 'ca', 'Cobrada') , ('unpaid', 'ca', 'No cobrada') , ('created', 'es', 'Creada') , ('sent', 'es', 'Enviada') + , ('partial', 'es', 'Parcial') , ('paid', 'es', 'Cobrada') , ('unpaid', 'es', 'No cobrada') +on conflict (invoice_status, lang_tag) do nothing ; commit; diff --git a/deploy/available_invoice_status@v2.sql b/deploy/available_invoice_status@v2.sql new file mode 100644 index 0000000..c11a59f --- /dev/null +++ b/deploy/available_invoice_status@v2.sql @@ -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; diff --git a/deploy/collection.sql b/deploy/collection.sql new file mode 100644 index 0000000..9bc4932 --- /dev/null +++ b/deploy/collection.sql @@ -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; diff --git a/deploy/collection_attachment.sql b/deploy/collection_attachment.sql new file mode 100644 index 0000000..0b4f118 --- /dev/null +++ b/deploy/collection_attachment.sql @@ -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; diff --git a/deploy/edit_collection.sql b/deploy/edit_collection.sql new file mode 100644 index 0000000..5438f40 --- /dev/null +++ b/deploy/edit_collection.sql @@ -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; diff --git a/deploy/invoice_collection.sql b/deploy/invoice_collection.sql new file mode 100644 index 0000000..19a8c97 --- /dev/null +++ b/deploy/invoice_collection.sql @@ -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; diff --git a/deploy/remove_collection.sql b/deploy/remove_collection.sql new file mode 100644 index 0000000..92225a0 --- /dev/null +++ b/deploy/remove_collection.sql @@ -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; diff --git a/deploy/update_invoice_collection_status.sql b/deploy/update_invoice_collection_status.sql new file mode 100644 index 0000000..ddd5ec1 --- /dev/null +++ b/deploy/update_invoice_collection_status.sql @@ -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; diff --git a/pkg/payments.go b/pkg/payments.go index 0997cbe..40b5ce8 100644 --- a/pkg/payments.go +++ b/pkg/payments.go @@ -10,6 +10,11 @@ import ( "time" ) +const ( + PaymentTypePayment = "P" + PaymentTypeCollection = "C" +) + func servePaymentIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { conn := getConn(r) company := mustGetCompany(r) @@ -42,14 +47,14 @@ type PaymentIndexPage struct { func NewPaymentIndexPage(ctx context.Context, conn *Conn, company *Company, locale *Locale) *PaymentIndexPage { return &PaymentIndexPage{ - Payments: mustCollectPaymentEntries(ctx, conn, company, locale, 0), + Payments: mustCollectPaymentEntries(ctx, conn, company, locale, "", 0), BaseURI: companyURI(company, "/payments"), } } func NewPaymentIndexPageForExpense(ctx context.Context, conn *Conn, company *Company, locale *Locale, expense *PaymentExpense) *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"), Expense: expense, } @@ -111,10 +116,11 @@ func (expense *PaymentExpense) calcRemainingPaymentAmount(ctx context.Context, c type PaymentEntry struct { ID int + Type string Slug string PaymentDate time.Time Description string - ExpenseSlug string + DocumentSlug string InvoiceNumber string Total string OriginalFileName string @@ -123,35 +129,57 @@ type PaymentEntry struct { 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, ` - select payment_id - , payment.slug - , payment_date - , description - , to_price(payment.amount, decimal_digits) as total - , payment.tags - , payment.payment_status - , psi18n.name - , coalesce(attachment.original_filename, '') - , coalesce(expense.slug::text, '') - , coalesce(expense.invoice_number, '') - from payment - join payment_status_i18n psi18n on payment.payment_status = psi18n.payment_status and psi18n.lang_tag = $1 - join currency using (currency_code) - left join payment_attachment as attachment using (payment_id) - left join expense_payment using (payment_id) - left join expense using (expense_id) - where payment.company_id = $2 - and ($3 = 0 or expense_id = $3) + select $5 as type + , payment_id + , payment.slug + , payment_date + , description + , to_price(payment.amount, decimal_digits) as total + , payment.tags + , payment.payment_status + , psi18n.name + , coalesce(attachment.original_filename, '') + , coalesce(expense.slug::text, '') + , coalesce(expense.invoice_number, '') + from payment + join payment_status_i18n psi18n on payment.payment_status = psi18n.payment_status and psi18n.lang_tag = $1 + join currency using (currency_code) + left join payment_attachment as attachment using (payment_id) + left join expense_payment using (payment_id) + left join expense using (expense_id) + 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 - `, locale.Language, company.Id, expenseId) + `, locale.Language, company.Id, paymentType, documentId, PaymentTypePayment, PaymentTypeCollection) defer rows.Close() var entries []*PaymentEntry for rows.Next() { 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) } entries = append(entries, entry) @@ -221,6 +249,7 @@ type PaymentForm struct { Slug string BaseURI string Expense *PaymentExpense + Type *SelectField Description *InputField PaymentDate *InputField PaymentAccount *SelectField @@ -234,6 +263,15 @@ func newPaymentForm(ctx context.Context, conn *Conn, locale *Locale, company *Co locale: locale, company: company, 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{ Name: "description", 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 { form := newPaymentForm(ctx, conn, locale, company) + form.Type.Selected = []string{PaymentTypePayment} form.BaseURI = companyURI(company, "/expenses/"+expense.Slug+"/payments") form.Expense = expense 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) { if f.Slug == "" { + f.Type.EmptyLabel = gettext("Select a type.", f.locale) f.PaymentAccount.EmptyLabel = gettext("Select an account.", f.locale) mustRenderMainTemplate(w, r, "payments/new.gohtml", f) } 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 { + selectedType := f.Type.Selected selectedPaymentAccount := f.PaymentAccount.Selected f.PaymentAccount.Clear() if notFoundErrorOrPanic(conn.QueryRow(ctx, ` - select description - , payment_date - , payment_account_id::text - , to_price(amount, decimal_digits) - , tags - from payment - join currency using (currency_code) - where payment.slug = $1 - `, slug).Scan( + select $2 as type + , description + , payment_date + , payment_account_id::text + , to_price(amount, decimal_digits) + , tags + from payment + join currency using (currency_code) + 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.PaymentDate, f.PaymentAccount, f.Amount, f.Tags)) { + f.Type.Selected = selectedType f.PaymentAccount.Selected = selectedPaymentAccount return false } @@ -319,6 +373,7 @@ func (f *PaymentForm) Parse(r *http.Request) error { if err := r.ParseMultipartForm(f.File.MaxSize); err != nil { return err } + f.Type.FillValue(r) f.Description.FillValue(r) f.PaymentDate.FillValue(r) f.PaymentAccount.FillValue(r) @@ -332,6 +387,7 @@ func (f *PaymentForm) Parse(r *http.Request) error { func (f *PaymentForm) Validate() bool { 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.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)) @@ -365,13 +421,20 @@ func handleAddPaymentForm(w http.ResponseWriter, r *http.Request, conn *Conn, co form.MustRender(w, r) return } - var paymentSlug any + var documentId any 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 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) + if form.Type.String() == PaymentTypePayment { + 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) + 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) } @@ -420,12 +483,22 @@ func handleEditPaymentForm(w http.ResponseWriter, r *http.Request, conn *Conn, f form.MustRender(w, r) 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 == "" { - 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 form.Type.String() == PaymentTypePayment { + 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 == "" { + 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) + } + } 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) } @@ -462,7 +535,7 @@ func removePayment(w http.ResponseWriter, r *http.Request, slug string, backURI } 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) } @@ -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) { serveAttachment(w, r, params, ` - select mime_type - , content - from payment - join payment_attachment using (payment_id) - where slug = $1 + select mime_type + , content + from payment + join payment_attachment using (payment_id) + 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) { - 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) { - 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") } diff --git a/po/ca.po b/po/ca.po index d681066..3da765e 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: numerus\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 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" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -39,7 +39,7 @@ msgstr "Afegeix productes a la factura" #: web/template/company/payment_methods.gohtml:10 #: 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/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/index.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/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 #: 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 msgctxt "action" msgid "Update" @@ -135,7 +135,7 @@ msgstr "Actualitza" #: 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/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 msgctxt "action" 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/quotes/index.gohtml:137 web/template/quotes/view.gohtml:22 #: 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" msgid "Edit" msgstr "Edita" @@ -634,7 +634,7 @@ msgstr "Nova despesa" #: 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/payments/new.gohtml:12 web/template/payments/index.gohtml:12 -#: web/template/payments/edit.gohtml:12 +#: web/template/payments/edit.gohtml:16 msgctxt "title" msgid "Expenses" msgstr "Despeses" @@ -781,7 +781,7 @@ msgid "New Payment" msgstr "Nou pagament" #: 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" msgid "Payments" msgstr "Pagaments" @@ -810,24 +810,29 @@ msgstr "Document" msgid "Are you sure you wish to delete this payment?" 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" msgstr "Accions pel pagament %s" -#: web/template/payments/index.gohtml:92 +#: web/template/payments/index.gohtml:96 msgctxt "action" msgid "Remove" msgstr "Esborra" -#: web/template/payments/index.gohtml:102 +#: web/template/payments/index.gohtml:106 msgid "No payments added yet." msgstr "No hi ha cap pagament." -#: web/template/payments/edit.gohtml:3 +#: web/template/payments/edit.gohtml:4 msgctxt "title" msgid "Edit Payment “%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:12 msgctxt "title" @@ -901,7 +906,7 @@ msgid "Name" msgstr "Nom" #: 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/contacts.go:362 msgctxt "input" @@ -936,7 +941,7 @@ msgstr "Qualsevol" 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." -#: 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 msgctxt "input" msgid "Description" @@ -1351,60 +1356,78 @@ msgstr "La confirmació no és igual a la contrasenya." msgid "Selected language is not valid." msgstr "Heu seleccionat un idioma que no és vàlid." -#: pkg/payments.go:202 +#: pkg/payments.go:230 #, c-format msgid "Payment of %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" msgid "Payment Date" msgstr "Data del pagament" -#: pkg/payments.go:251 +#: pkg/payments.go:289 msgctxt "input" msgid "Account" msgstr "Compte" -#: pkg/payments.go:257 pkg/expenses.go:319 +#: pkg/payments.go:295 pkg/expenses.go:319 msgctxt "input" msgid "Amount" 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" msgid "File" 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." 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." msgstr "No podeu deixar la descripció en blanc." -#: pkg/payments.go:336 +#: pkg/payments.go:392 msgid "Selected payment account is not valid." 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." 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." msgstr "No podeu deixar l’import 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." msgstr "L’import 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 msgctxt "input" msgid "IBAN" diff --git a/po/es.po b/po/es.po index 230b4cc..9758016 100644 --- a/po/es.po +++ b/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: numerus\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 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" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -39,7 +39,7 @@ msgstr "Añadir productos a la factura" #: web/template/company/payment_methods.gohtml:10 #: 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/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/index.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/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 #: 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 msgctxt "action" msgid "Update" @@ -135,7 +135,7 @@ msgstr "Actualizar" #: 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/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 msgctxt "action" 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/quotes/index.gohtml:137 web/template/quotes/view.gohtml:22 #: 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" msgid "Edit" msgstr "Editar" @@ -634,7 +634,7 @@ msgstr "Nuevo gasto" #: 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/payments/new.gohtml:12 web/template/payments/index.gohtml:12 -#: web/template/payments/edit.gohtml:12 +#: web/template/payments/edit.gohtml:16 msgctxt "title" msgid "Expenses" msgstr "Gastos" @@ -781,7 +781,7 @@ msgid "New Payment" msgstr "Nuevo pago" #: 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" msgid "Payments" msgstr "Pagos" @@ -810,24 +810,29 @@ msgstr "Documento" msgid "Are you sure you wish to delete this payment?" 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" msgstr "Acciones para el pago %s" -#: web/template/payments/index.gohtml:92 +#: web/template/payments/index.gohtml:96 msgctxt "action" msgid "Remove" msgstr "Borrar" -#: web/template/payments/index.gohtml:102 +#: web/template/payments/index.gohtml:106 msgid "No payments added yet." msgstr "No hay pagos." -#: web/template/payments/edit.gohtml:3 +#: web/template/payments/edit.gohtml:4 msgctxt "title" msgid "Edit Payment “%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:12 msgctxt "title" @@ -901,7 +906,7 @@ msgid "Name" msgstr "Nombre" #: 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/contacts.go:362 msgctxt "input" @@ -936,7 +941,7 @@ msgstr "Cualquiera" msgid "Invoices must have at least one of the specified labels." 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 msgctxt "input" msgid "Description" @@ -1351,60 +1356,78 @@ msgstr "La confirmación no corresponde con la contraseña." msgid "Selected language is not valid." msgstr "Habéis escogido un idioma que no es válido." -#: pkg/payments.go:202 +#: pkg/payments.go:230 #, c-format msgid "Payment of %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" msgid "Payment Date" msgstr "Fecha del pago" -#: pkg/payments.go:251 +#: pkg/payments.go:289 msgctxt "input" msgid "Account" msgstr "Cuenta" -#: pkg/payments.go:257 pkg/expenses.go:319 +#: pkg/payments.go:295 pkg/expenses.go:319 msgctxt "input" msgid "Amount" 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" msgid "File" 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." 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." 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." 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." 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." 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." 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 msgctxt "input" msgid "IBAN" diff --git a/revert/add_collection.sql b/revert/add_collection.sql new file mode 100644 index 0000000..888f056 --- /dev/null +++ b/revert/add_collection.sql @@ -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; diff --git a/revert/attach_to_collection.sql b/revert/attach_to_collection.sql new file mode 100644 index 0000000..531e0a7 --- /dev/null +++ b/revert/attach_to_collection.sql @@ -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; diff --git a/revert/available_invoice_status.sql b/revert/available_invoice_status.sql index 47121a6..30b6e05 100644 --- a/revert/available_invoice_status.sql +++ b/revert/available_invoice_status.sql @@ -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; set search_path to numerus; -delete from invoice_status_i18n; -delete from invoice_status; +update invoice set invoice_status = 'created' where invoice_status = 'partial'; +delete from invoice_status_i18n where invoice_status = 'partial'; +delete from invoice_status where invoice_status = 'partial'; commit; diff --git a/revert/available_invoice_status@v2.sql b/revert/available_invoice_status@v2.sql new file mode 100644 index 0000000..47121a6 --- /dev/null +++ b/revert/available_invoice_status@v2.sql @@ -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; diff --git a/revert/collection.sql b/revert/collection.sql new file mode 100644 index 0000000..d07c08e --- /dev/null +++ b/revert/collection.sql @@ -0,0 +1,7 @@ +-- Revert numerus:collection from pg + +begin; + +drop table if exists numerus.collection; + +commit; diff --git a/revert/collection_attachment.sql b/revert/collection_attachment.sql new file mode 100644 index 0000000..0de2a89 --- /dev/null +++ b/revert/collection_attachment.sql @@ -0,0 +1,7 @@ +-- Revert numerus:collection_attachment from pg + +begin; + +drop table if exists numerus.collection_attachment; + +commit; diff --git a/revert/edit_collection.sql b/revert/edit_collection.sql new file mode 100644 index 0000000..79487d1 --- /dev/null +++ b/revert/edit_collection.sql @@ -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; diff --git a/revert/invoice_collection.sql b/revert/invoice_collection.sql new file mode 100644 index 0000000..fbc75dd --- /dev/null +++ b/revert/invoice_collection.sql @@ -0,0 +1,7 @@ +-- Revert numerus:invoice_collection from pg + +begin; + +drop table if exists numerus.invoice_collection; + +commit; diff --git a/revert/remove_collection.sql b/revert/remove_collection.sql new file mode 100644 index 0000000..aeff0bb --- /dev/null +++ b/revert/remove_collection.sql @@ -0,0 +1,7 @@ +-- Revert numerus:remove_collection from pg + +begin; + +drop function if exists numerus.remove_collection(uuid); + +commit; diff --git a/revert/update_invoice_collection_status.sql b/revert/update_invoice_collection_status.sql new file mode 100644 index 0000000..054243b --- /dev/null +++ b/revert/update_invoice_collection_status.sql @@ -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; diff --git a/sqitch.plan b/sqitch.plan index f948eb3..28f7313 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -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 # Add function to remove payments add_expense [add_expense@v2] 2024-08-12T23:48:07Z jordi fita mas # Remove the status parameter from add_expense edit_expense [edit_expense@v2] 2024-08-12T23:53:48Z jordi fita mas # 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 # Add relation of cash (payment) collection +invoice_collection [roles schema_numerus invoice collection] 2024-08-18T03:22:09Z jordi fita mas # Add relation of invoice collections +available_invoice_status [available_invoice_status@v2] 2024-08-18T03:34:13Z jordi fita mas # 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 # 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 # 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 # Add function to update collections +collection_attachment [roles schema_numerus collection] 2024-08-20T00:34:09Z jordi fita mas # Add relation of collection attachments +attach_to_collection [roles schema_numerus collection collection_attachment] 2024-08-20T00:41:53Z jordi fita mas # 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 # Add function to remove collections diff --git a/test/add_collection.sql b/test/add_collection.sql new file mode 100644 index 0000000..73226d2 --- /dev/null +++ b/test/add_collection.sql @@ -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; diff --git a/test/attach_to_collection.sql b/test/attach_to_collection.sql new file mode 100644 index 0000000..042485b --- /dev/null +++ b/test/attach_to_collection.sql @@ -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('

To receive 42 €

', 'UTF-8')) $$, + 'Should be able to replate the second collection’s 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', '

To receive 42 €

') + $$, + 'Should have attached all documents' +); + +select * +from finish(); + +rollback; diff --git a/test/collection.sql b/test/collection.sql new file mode 100644 index 0000000..454ec55 --- /dev/null +++ b/test/collection.sql @@ -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; + diff --git a/test/collection_attachment.sql b/test/collection_attachment.sql new file mode 100644 index 0000000..32fc2d1 --- /dev/null +++ b/test/collection_attachment.sql @@ -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('Collection 42', '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; + diff --git a/test/edit_collection.sql b/test/edit_collection.sql new file mode 100644 index 0000000..811985f --- /dev/null +++ b/test/edit_collection.sql @@ -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; diff --git a/test/invoice_collection.sql b/test/invoice_collection.sql new file mode 100644 index 0000000..0bd6378 --- /dev/null +++ b/test/invoice_collection.sql @@ -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; + diff --git a/test/remove_collection.sql b/test/remove_collection.sql new file mode 100644 index 0000000..4c49d59 --- /dev/null +++ b/test/remove_collection.sql @@ -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(' PAY 42', '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 invoice’s 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; diff --git a/test/update_invoice_collection_status.sql b/test/update_invoice_collection_status.sql new file mode 100644 index 0000000..7175bd6 --- /dev/null +++ b/test/update_invoice_collection_status.sql @@ -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; diff --git a/verify/add_collection.sql b/verify/add_collection.sql new file mode 100644 index 0000000..373b38f --- /dev/null +++ b/verify/add_collection.sql @@ -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; diff --git a/verify/attach_to_collection.sql b/verify/attach_to_collection.sql new file mode 100644 index 0000000..9a491e4 --- /dev/null +++ b/verify/attach_to_collection.sql @@ -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; diff --git a/verify/available_invoice_status.sql b/verify/available_invoice_status.sql index cdaafee..c02be12 100644 --- a/verify/available_invoice_status.sql +++ b/verify/available_invoice_status.sql @@ -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 = '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 = '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 = '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 = '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= 'es'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'unpaid' and name ='No cobrada' and lang_tag= 'ca'; diff --git a/verify/available_invoice_status@v2.sql b/verify/available_invoice_status@v2.sql new file mode 100644 index 0000000..cdaafee --- /dev/null +++ b/verify/available_invoice_status@v2.sql @@ -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; diff --git a/verify/collection.sql b/verify/collection.sql new file mode 100644 index 0000000..47234bc --- /dev/null +++ b/verify/collection.sql @@ -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; diff --git a/verify/collection_attachment.sql b/verify/collection_attachment.sql new file mode 100644 index 0000000..f03ad57 --- /dev/null +++ b/verify/collection_attachment.sql @@ -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; diff --git a/verify/edit_collection.sql b/verify/edit_collection.sql new file mode 100644 index 0000000..19976aa --- /dev/null +++ b/verify/edit_collection.sql @@ -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; diff --git a/verify/invoice_collection.sql b/verify/invoice_collection.sql new file mode 100644 index 0000000..39efe46 --- /dev/null +++ b/verify/invoice_collection.sql @@ -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; diff --git a/verify/remove_collection.sql b/verify/remove_collection.sql new file mode 100644 index 0000000..e468d57 --- /dev/null +++ b/verify/remove_collection.sql @@ -0,0 +1,7 @@ +-- Verify numerus:remove_collection on pg + +begin; + +select has_function_privilege('numerus.remove_collection(uuid)', 'execute'); + +rollback; diff --git a/verify/update_invoice_collection_status.sql b/verify/update_invoice_collection_status.sql new file mode 100644 index 0000000..91d57d7 --- /dev/null +++ b/verify/update_invoice_collection_status.sql @@ -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; diff --git a/web/static/numerus.css b/web/static/numerus.css index 5575346..91f8205 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -696,6 +696,7 @@ main > nav { .payment-status-partial, .expense-status-partial, .quote-status-sent, +.invoice-status-partial, .invoice-status-sent { background-color: var(--numerus--color--hay); } diff --git a/web/template/payments/edit.gohtml b/web/template/payments/edit.gohtml index b41423f..f59a821 100644 --- a/web/template/payments/edit.gohtml +++ b/web/template/payments/edit.gohtml @@ -1,6 +1,10 @@ {{ define "title" -}} {{- /*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 }} {{ define "breadcrumbs" -}} @@ -30,6 +34,7 @@ {{ csrfToken }} {{ putMethod }} + {{ template "hidden-select-field" .Type }} {{ template "select-field" .PaymentAccount }} {{ template "input-field" .Description }} {{ template "input-field" .PaymentDate }} diff --git a/web/template/payments/index.gohtml b/web/template/payments/index.gohtml index ee55cae..faf5160 100644 --- a/web/template/payments/index.gohtml +++ b/web/template/payments/index.gohtml @@ -45,7 +45,11 @@ {{ .Description }} {{- if .InvoiceNumber -}} - {{ .InvoiceNumber }} + {{- if eq .Type "P" -}} + {{ .InvoiceNumber }} + {{- else -}} + {{ .InvoiceNumber }} + {{- end -}} {{- end -}} {{ .StatusLabel }} diff --git a/web/template/payments/new.gohtml b/web/template/payments/new.gohtml index 67f7d36..c57c741 100644 --- a/web/template/payments/new.gohtml +++ b/web/template/payments/new.gohtml @@ -28,6 +28,11 @@ data-hx-boost="true"> {{ csrfToken }} + {{- if .Expense -}} + {{ template "hidden-select-field" .Type }} + {{- else -}} + {{ template "select-field" .Type }} + {{- end -}} {{ template "select-field" .PaymentAccount }} {{ template "input-field" .Description }} {{ template "input-field" .PaymentDate }}