ekwo-os 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +873 -0
- package/dist/args.d.ts +57 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +134 -0
- package/dist/args.js.map +1 -0
- package/dist/assets/expected-objects.json +9432 -0
- package/dist/assets/migrations/20260911120000_core_companies.sql +260 -0
- package/dist/assets/migrations/20260911120100_accounts_journals.sql +211 -0
- package/dist/assets/migrations/20260911120200_contacts.sql +90 -0
- package/dist/assets/migrations/20260911120300_taxes.sql +146 -0
- package/dist/assets/migrations/20260911120400_entries.sql +338 -0
- package/dist/assets/migrations/20260911120500_documents.sql +279 -0
- package/dist/assets/migrations/20260911120600_payments_reconciliation.sql +251 -0
- package/dist/assets/migrations/20260911120700_bank.sql +178 -0
- package/dist/assets/migrations/20260911120800_analytics_attachments.sql +128 -0
- package/dist/assets/migrations/20260911120900_post_document.sql +279 -0
- package/dist/assets/migrations/20260911121000_reporting.sql +235 -0
- package/dist/assets/migrations/20260911121100_country_templates.sql +226 -0
- package/dist/assets/migrations/20260911121200_fec.sql +71 -0
- package/dist/assets/migrations/20260911130000_instance.sql +77 -0
- package/dist/assets/migrations/20260911130100_instance_members.sql +224 -0
- package/dist/assets/migrations/20260911140000_instance_admins.sql +112 -0
- package/dist/assets/migrations/20260911160000_tax_posting_templates_unique.sql +17 -0
- package/dist/assets/migrations/20260911170000_document_amount_paid.sql +82 -0
- package/dist/assets/migrations/20260911173000_post_payment.sql +122 -0
- package/dist/assets/migrations/20260911173100_sequence_counters_under_rls.sql +84 -0
- package/dist/assets/migrations/20260911183000_country_journal_defaults.sql +143 -0
- package/dist/assets/migrations/20260911193853_line_account_defaults.sql +274 -0
- package/dist/assets/migrations/20260911195054_products.sql +221 -0
- package/dist/assets/migrations/20260911210131_anon_surface.sql +85 -0
- package/dist/assets/migrations/20260912074712_country_packs.sql +318 -0
- package/dist/assets/migrations/20260912080311_report_code_and_region.sql +198 -0
- package/dist/assets/migrations/20260912081014_pack_certification_maintained.sql +31 -0
- package/dist/assets/migrations/20260912081015_pack_certification_backfill.sql +16 -0
- package/dist/assets/migrations/20260912090407_tax_report_boxes.sql +329 -0
- package/dist/assets/migrations/20260912091917_tax_on_base_value.sql +25 -0
- package/dist/assets/migrations/20260912091918_tax_engine_columns.sql +776 -0
- package/dist/assets/migrations/20260912094412_opening_and_closing.sql +759 -0
- package/dist/assets/migrations/20260912095825_charts_of_accounts.sql +326 -0
- package/dist/assets/migrations/20260912100412_financial_statements.sql +632 -0
- package/dist/assets/migrations/20260912104719_one_formula_evaluator.sql +209 -0
- package/dist/assets/migrations/20260912105720_entry_kind_appropriation.sql +26 -0
- package/dist/assets/migrations/20260912105721_appropriation_entry_kind.sql +444 -0
- package/dist/assets/migrations/20260912111751_document_rules.sql +274 -0
- package/dist/assets/migrations/20260912112132_cash_basis_vat_and_fx.sql +1189 -0
- package/dist/assets/migrations/20260913074512_modules.sql +493 -0
- package/dist/assets/migrations/20260913075903_asset_disposal_roles.sql +47 -0
- package/dist/assets/migrations/20260913083216_capabilities.sql +637 -0
- package/dist/assets/migrations/20260913083901_company_invitations.sql +262 -0
- package/dist/assets/migrations/20260913084402_user_preferences.sql +376 -0
- package/dist/assets/migrations/20260913084847_company_profile.sql +261 -0
- package/dist/assets/migrations/20260913085436_numbering_reads_the_pack.sql +235 -0
- package/dist/assets/migrations/20260913085932_api_keys.sql +303 -0
- package/dist/assets/migrations/20260913090216_fiscal_year_bounds.sql +159 -0
- package/dist/assets/migrations/20260913092527_entries_import.sql +219 -0
- package/dist/assets/migrations/20260913101536_null_safe_guards.sql +188 -0
- package/dist/assets/migrations/20260913102115_installer_is_named.sql +462 -0
- package/dist/assets/migrations/20260913102758_no_currency_default.sql +138 -0
- package/dist/assets/migrations/20260913103355_cash_basis_needs_a_box.sql +453 -0
- package/dist/assets/migrations/20260913104014_aged_balance_named_group.sql +79 -0
- package/dist/assets/migrations/20260913104232_foreign_key_indexes.sql +144 -0
- package/dist/assets/migrations/20260913105120_declared_no_reader.sql +41 -0
- package/dist/assets/migrations/20260913111407_pack_languages.sql +223 -0
- package/dist/assets/migrations/20260913112233_schema_comments_for_a_reader.sql +27 -0
- package/dist/assets/migrations/20260913114535_fec_opening_balances.sql +307 -0
- package/dist/assets/migrations/20260914103412_audit_log.sql +509 -0
- package/dist/assets/migrations/20260914111907_pack_upgrade.sql +428 -0
- package/dist/assets/migrations/20260914120500_rounding_reads_the_currency.sql +206 -0
- package/dist/assets/migrations/20260914121200_amounts_round_at_the_currency.sql +2403 -0
- package/dist/assets/migrations/20260914134325_schema_version_0_2_0.sql +36 -0
- package/dist/assets/migrations/20260914143915_accounts_in_use.sql +428 -0
- package/dist/assets/migrations/20260914144731_account_code_frozen.sql +99 -0
- package/dist/assets/migrations/20260914151207_schema_grants_its_own_rights.sql +284 -0
- package/dist/assets/migrations/20260914152840_pack_upgrade_records_its_own_line.sql +237 -0
- package/dist/assets/migrations/20260914163943_declaration_periodicity.sql +349 -0
- package/dist/assets/migrations/20260915094000_foreign_services_received.sql +37 -0
- package/dist/assets/migrations/20260915094500_mentions_read_a_foreign_service.sql +72 -0
- package/dist/assets/migrations/20260915153000_document_shares.sql +568 -0
- package/dist/assets/migrations/20260915160000_ec_sales_list.sql +181 -0
- package/dist/assets/migrations/20260915161842_pack_sources.sql +56 -0
- package/dist/assets/migrations/20260915170500_schema_version_0_3_0.sql +45 -0
- package/dist/assets/migrations/20260915174500_a_vat_category_is_a_code.sql +177 -0
- package/dist/assets/migrations/20260915180000_document_rule_references.sql +85 -0
- package/dist/assets/migrations/20260915181000_territories.sql +342 -0
- package/dist/assets/migrations/20260915181500_ec_sales_list_reads_the_territories.sql +222 -0
- package/dist/assets/migrations/20260915182000_intracom_triangular.sql +53 -0
- package/dist/assets/migrations/20260915182500_a_triangular_supply_says_reverse_charge.sql +99 -0
- package/dist/assets/migrations/20260915191200_a_document_knows_its_language.sql +633 -0
- package/dist/assets/migrations/20260915195000_a_price_that_holds_its_tax.sql +649 -0
- package/dist/assets/migrations/20260915200000_a_posting_names_its_boxes.sql +1004 -0
- package/dist/assets/migrations/20260916094500_a_company_files_more_than_one_declaration.sql +795 -0
- package/dist/assets/migrations/20260916103000_a_ledger_line_names_its_posting.sql +902 -0
- package/dist/assets/migrations/20260916123000_a_vocabulary_for_a_tax_that_is_not_a_vat.sql +144 -0
- package/dist/assets/migrations/20260916124000_a_box_can_be_a_rate_of_a_box.sql +525 -0
- package/dist/assets/migrations/20260916125000_a_tax_follows_the_territory.sql +1318 -0
- package/dist/assets/migrations/20260916126000_a_tax_point_and_its_exception.sql +1211 -0
- package/dist/assets/migrations/20260917090000_a_counterparty_that_learns.sql +561 -0
- package/dist/assets/migrations/20260917120000_what_the_money_pays.sql +545 -0
- package/dist/assets/migrations/20260917150000_a_filing_is_a_row.sql +488 -0
- package/dist/assets/migrations/20260917170000_when_it_is_due.sql +194 -0
- package/dist/assets/migrations/20260917180000_a_box_is_a_number_and_a_kind.sql +213 -0
- package/dist/assets/migrations/20260917190000_what_a_declaration_owes.sql +486 -0
- package/dist/assets/migrations/20260917200000_what_moved_after_it_went.sql +227 -0
- package/dist/assets/migrations/20260918090000_a_form_names_its_file.sql +27 -0
- package/dist/assets/migrations/20260918110000_a_deposit_and_what_came_back.sql +286 -0
- package/dist/assets/migrations/20260918113741_a_fourth_preset_is_named.sql +9 -0
- package/dist/assets/migrations/20260918113807_a_client_reads_and_hands_over.sql +96 -0
- package/dist/assets/migrations/20260918114322_two_definer_functions_check_their_caller.sql +75 -0
- package/dist/assets/migrations/20260918130000_every_company_somebody_keeps.sql +177 -0
- package/dist/assets/migrations/20260918140000_a_session_nobody_prepared.sql +40 -0
- package/dist/assets/migrations/20260918141107_a_line_keeps_the_tax_it_was_posted_with.sql +256 -0
- package/dist/assets/migrations/20260918141342_a_company_has_an_electronic_address.sql +66 -0
- package/dist/assets/migrations/20260918141605_an_invoice_reads_whole_from_the_views.sql +313 -0
- package/dist/assets/migrations/20260918141627_a_policy_asks_once.sql +132 -0
- package/dist/assets/migrations/20260918143352_a_suggestion_reads_the_words_once.sql +204 -0
- package/dist/assets/migrations/20260918143417_a_reference_of_the_caller_and_a_rehearsal.sql +115 -0
- package/dist/assets/migrations/20260918150712_a_company_leaves_with_its_books.sql +1119 -0
- package/dist/assets/migrations/20260918150931_a_statement_is_imported_once.sql +654 -0
- package/dist/assets/migrations/20260918161204_a_posted_document_does_not_move.sql +437 -0
- package/dist/assets/migrations/20260918161538_a_posted_entry_does_not_move.sql +252 -0
- package/dist/assets/migrations/20260918171946_an_entry_is_posted_by_post_entry.sql +289 -0
- package/dist/assets/migrations/20260918174312_schema_version_0_4_0.sql +53 -0
- package/dist/assets/modules/assets/module.json +13 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913081447_assets.sql +1190 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104234_foreign_key_indexes.sql +47 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104500_assets_capabilities.sql +145 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914122100_assets_round_at_the_currency.sql +564 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914145012_assets_accounts_in_use.sql +37 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914151530_assets_grants_its_own_rights.sql +63 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918113912_assets_client_reads.sql +20 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918150801_assets_leave_with_the_company.sql +41 -0
- package/dist/assets/modules/budgets/module.json +12 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913083012_budgets.sql +210 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104233_foreign_key_indexes.sql +37 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104501_budgets_capabilities.sql +72 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914122400_budgets_round_at_the_currency.sql +68 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914145048_budgets_accounts_in_use.sql +30 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914151812_budgets_grants_its_own_rights.sql +31 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918113954_budgets_client_reads.sql +20 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918150823_budgets_leave_with_the_company.sql +37 -0
- package/dist/assets/modules/schema/module.1.json +69 -0
- package/dist/assets/seed/00_currencies.sql +15 -0
- package/dist/assets/seed/00_territories.sql +280 -0
- package/dist/assets/seed/05_framework_generic.sql +110 -0
- package/dist/assets/seed/10_pack_be.sql +1284 -0
- package/dist/assets/seed/11_pack_fr.sql +1112 -0
- package/dist/assets/seed/12_pack_lu.sql +2484 -0
- package/dist/assets/seed/13_pack_ee.sql +733 -0
- package/dist/assets/seed/14_pack_gb.sql +796 -0
- package/dist/assets/seed/15_pack_us.sql +751 -0
- package/dist/assets/seed/90_demo_company.sql +368 -0
- package/dist/assets/seed/modules/assets/10_pack_be.sql +58 -0
- package/dist/assets/seed/modules/assets/11_pack_fr.sql +57 -0
- package/dist/assets/seed/modules/assets/14_pack_gb.sql +58 -0
- package/dist/assets/seed/modules/assets/15_pack_us.sql +60 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +134 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +8 -0
- package/dist/bin.js.map +1 -0
- package/dist/books.d.ts +60 -0
- package/dist/books.d.ts.map +1 -0
- package/dist/books.js +168 -0
- package/dist/books.js.map +1 -0
- package/dist/bootstrap.d.ts +236 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +459 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bundle.d.ts +27 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +47 -0
- package/dist/bundle.js.map +1 -0
- package/dist/checklist.d.ts +35 -0
- package/dist/checklist.d.ts.map +1 -0
- package/dist/checklist.js +64 -0
- package/dist/checklist.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +318 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/company.d.ts +59 -0
- package/dist/commands/company.d.ts.map +1 -0
- package/dist/commands/company.js +287 -0
- package/dist/commands/company.js.map +1 -0
- package/dist/commands/contact.d.ts +9 -0
- package/dist/commands/contact.d.ts.map +1 -0
- package/dist/commands/contact.js +82 -0
- package/dist/commands/contact.js.map +1 -0
- package/dist/commands/demo.d.ts +19 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +72 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +68 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/document.d.ts +17 -0
- package/dist/commands/document.d.ts.map +1 -0
- package/dist/commands/document.js +217 -0
- package/dist/commands/document.js.map +1 -0
- package/dist/commands/init.d.ts +31 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +535 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +24 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +128 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +115 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/module.d.ts +32 -0
- package/dist/commands/module.d.ts.map +1 -0
- package/dist/commands/module.js +285 -0
- package/dist/commands/module.js.map +1 -0
- package/dist/commands/pack.d.ts +22 -0
- package/dist/commands/pack.d.ts.map +1 -0
- package/dist/commands/pack.js +481 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/payment.d.ts +15 -0
- package/dist/commands/payment.d.ts.map +1 -0
- package/dist/commands/payment.js +97 -0
- package/dist/commands/payment.js.map +1 -0
- package/dist/commands/register.d.ts +20 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +134 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +134 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/whoami.d.ts +21 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +133 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/company.d.ts +16 -0
- package/dist/company.d.ts.map +1 -0
- package/dist/company.js +26 -0
- package/dist/company.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/connection.d.ts +79 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +116 -0
- package/dist/connection.js.map +1 -0
- package/dist/context.d.ts +47 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +93 -0
- package/dist/context.js.map +1 -0
- package/dist/doctor.d.ts +45 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +448 -0
- package/dist/doctor.js.map +1 -0
- package/dist/grants.d.ts +88 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +174 -0
- package/dist/grants.js.map +1 -0
- package/dist/identity.d.ts +54 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +132 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +155 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +303 -0
- package/dist/inventory.js.map +1 -0
- package/dist/migrations.d.ts +76 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +226 -0
- package/dist/migrations.js.map +1 -0
- package/dist/module/read.d.ts +91 -0
- package/dist/module/read.d.ts.map +1 -0
- package/dist/module/read.js +172 -0
- package/dist/module/read.js.map +1 -0
- package/dist/output.d.ts +110 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +173 -0
- package/dist/output.js.map +1 -0
- package/dist/pack/certification.d.ts +27 -0
- package/dist/pack/certification.d.ts.map +1 -0
- package/dist/pack/certification.js +38 -0
- package/dist/pack/certification.js.map +1 -0
- package/dist/pack/compile.d.ts +75 -0
- package/dist/pack/compile.d.ts.map +1 -0
- package/dist/pack/compile.js +801 -0
- package/dist/pack/compile.js.map +1 -0
- package/dist/pack/filing.d.ts +48 -0
- package/dist/pack/filing.d.ts.map +1 -0
- package/dist/pack/filing.js +56 -0
- package/dist/pack/filing.js.map +1 -0
- package/dist/pack/read.d.ts +766 -0
- package/dist/pack/read.d.ts.map +1 -0
- package/dist/pack/read.js +2336 -0
- package/dist/pack/read.js.map +1 -0
- package/dist/pack/schema.d.ts +26 -0
- package/dist/pack/schema.d.ts.map +1 -0
- package/dist/pack/schema.js +181 -0
- package/dist/pack/schema.js.map +1 -0
- package/dist/pack/territories.d.ts +98 -0
- package/dist/pack/territories.d.ts.map +1 -0
- package/dist/pack/territories.js +0 -0
- package/dist/pack/territories.js.map +1 -0
- package/dist/pack/upgrade.d.ts +86 -0
- package/dist/pack/upgrade.d.ts.map +1 -0
- package/dist/pack/upgrade.js +76 -0
- package/dist/pack/upgrade.js.map +1 -0
- package/dist/pack/vat-codes.d.ts +273 -0
- package/dist/pack/vat-codes.d.ts.map +1 -0
- package/dist/pack/vat-codes.js +513 -0
- package/dist/pack/vat-codes.js.map +1 -0
- package/dist/profiles.d.ts +67 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +187 -0
- package/dist/profiles.js.map +1 -0
- package/dist/prompt.d.ts +40 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +120 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +84 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +104 -0
- package/dist/registry.js.map +1 -0
- package/dist/rest.d.ts +81 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +245 -0
- package/dist/rest.js.map +1 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +15 -0
- package/dist/schema.js.map +1 -0
- package/dist/seeds.d.ts +27 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +47 -0
- package/dist/seeds.js.map +1 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +92 -0
- package/dist/session.js.map +1 -0
- package/dist/sql.d.ts +48 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +112 -0
- package/dist/sql.js.map +1 -0
- package/dist/status.d.ts +94 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +120 -0
- package/dist/status.js.map +1 -0
- package/dist/ui.d.ts +55 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +114 -0
- package/dist/ui.js.map +1 -0
- package/package.json +29 -0
- package/schema/output.1.json +613 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
-- Ekwo OS — inviting somebody who does not have an account yet.
|
|
2
|
+
--
|
|
3
|
+
-- `company_members.user_id` deliberately carries no foreign key to
|
|
4
|
+
-- `auth.users`, precisely so that a membership can be written before the
|
|
5
|
+
-- person has signed up. What was missing is the act that produces it: until
|
|
6
|
+
-- now somebody had to know the invitee's `auth.users.id` — which means the
|
|
7
|
+
-- invitee had to exist, and somebody had to go and read it out of the Auth
|
|
8
|
+
-- dashboard.
|
|
9
|
+
--
|
|
10
|
+
-- An invitation is the missing half. It names an address, a preset and any
|
|
11
|
+
-- capability granted on top of it, and it carries a secret that is handed over
|
|
12
|
+
-- once and stored only as a hash. The person signs up with their own address
|
|
13
|
+
-- in the customer's Supabase Auth, and `accept_invitation()` turns the
|
|
14
|
+
-- invitation into a membership — after checking that the address they signed
|
|
15
|
+
-- in with is the one that was invited.
|
|
16
|
+
--
|
|
17
|
+
-- **Three rules and their reasons.**
|
|
18
|
+
--
|
|
19
|
+
-- The token is never stored. `token_hash` is a sha256 of it, computed by
|
|
20
|
+
-- Postgres' own `sha256()` — core since PostgreSQL 11, so no extension, which
|
|
21
|
+
-- `pgcrypto` would have been and PGlite does not carry. A database backup
|
|
22
|
+
-- therefore hands nobody an invitation.
|
|
23
|
+
--
|
|
24
|
+
-- The address is matched, not trusted. A token alone would let whoever
|
|
25
|
+
-- receives a forwarded e-mail join the books; `auth.email()` has to equal the
|
|
26
|
+
-- address the invitation was written for, case-insensitively.
|
|
27
|
+
--
|
|
28
|
+
-- An invitation is used once and expires. Accepting sets `accepted_at`, and
|
|
29
|
+
-- every later attempt is refused rather than silently re-adding a member
|
|
30
|
+
-- somebody may have removed on purpose.
|
|
31
|
+
|
|
32
|
+
create table company_invitations (
|
|
33
|
+
id uuid primary key default gen_random_uuid(),
|
|
34
|
+
company_id uuid not null references companies(id) on delete cascade,
|
|
35
|
+
-- Lower-cased on the way in: an address is not case-sensitive in its domain
|
|
36
|
+
-- and, in practice, not in its local part either. `citext` would have said
|
|
37
|
+
-- the same thing and is an extension.
|
|
38
|
+
email text not null,
|
|
39
|
+
role member_role not null default 'viewer',
|
|
40
|
+
capabilities_granted text[] not null default '{}',
|
|
41
|
+
token_hash text not null unique,
|
|
42
|
+
invited_by uuid,
|
|
43
|
+
created_at timestamptz not null default now(),
|
|
44
|
+
expires_at timestamptz not null,
|
|
45
|
+
accepted_at timestamptz,
|
|
46
|
+
accepted_by uuid,
|
|
47
|
+
revoked_at timestamptz,
|
|
48
|
+
constraint company_invitations_email_lowercase check (email = lower(email)),
|
|
49
|
+
constraint company_invitations_email_shape check (email like '%_@_%'),
|
|
50
|
+
constraint company_invitations_expiry check (expires_at > created_at),
|
|
51
|
+
constraint company_invitations_accepted_together
|
|
52
|
+
check ((accepted_at is null) = (accepted_by is null))
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
comment on table company_invitations is
|
|
56
|
+
'Pending and past invitations into a company. The token is handed over once and kept only as a sha256 hash.';
|
|
57
|
+
comment on column company_invitations.email is
|
|
58
|
+
'The address the invitation is for, lower-cased. accept_invitation() refuses anyone signed in with another.';
|
|
59
|
+
comment on column company_invitations.capabilities_granted is
|
|
60
|
+
'Capabilities the new member holds on top of their preset, written onto company_members when the invitation is accepted.';
|
|
61
|
+
comment on column company_invitations.invited_by is
|
|
62
|
+
'auth.users.id of whoever issued it. No foreign key, for the same reason company_members has none.';
|
|
63
|
+
|
|
64
|
+
create index company_invitations_company_idx on company_invitations (company_id, created_at desc);
|
|
65
|
+
create index company_invitations_email_idx on company_invitations (lower(email));
|
|
66
|
+
|
|
67
|
+
-- ---------------------------------------------------------------------------
|
|
68
|
+
-- Issuing one
|
|
69
|
+
--
|
|
70
|
+
-- The secret is two `gen_random_uuid()` with their dashes removed: 256 bits
|
|
71
|
+
-- from the same source the primary keys come from, and no extension. It is
|
|
72
|
+
-- returned by this call and by nothing else, ever.
|
|
73
|
+
-- ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
create or replace function invite_member(
|
|
76
|
+
p_company_id uuid,
|
|
77
|
+
p_email text,
|
|
78
|
+
p_role member_role default 'viewer',
|
|
79
|
+
-- A list crosses as JSON, the way `opening_balance` takes its lines: over
|
|
80
|
+
-- PostgREST an argument is a JSON body, and a Postgres driver handed a
|
|
81
|
+
-- JavaScript array builds an array literal instead. One spelling that both
|
|
82
|
+
-- routes read the same way.
|
|
83
|
+
p_capabilities jsonb default '[]'::jsonb,
|
|
84
|
+
p_valid_for interval default interval '14 days'
|
|
85
|
+
)
|
|
86
|
+
returns table (invitation_id uuid, token text, expires_at timestamptz)
|
|
87
|
+
language plpgsql
|
|
88
|
+
security definer
|
|
89
|
+
set search_path = public, pg_temp
|
|
90
|
+
as $$
|
|
91
|
+
declare
|
|
92
|
+
v_email text := lower(trim(p_email));
|
|
93
|
+
v_token text;
|
|
94
|
+
v_unknown text;
|
|
95
|
+
v_caps text[] := coalesce(
|
|
96
|
+
(select array_agg(value) from jsonb_array_elements_text(coalesce(p_capabilities, '[]'::jsonb)) as value),
|
|
97
|
+
'{}');
|
|
98
|
+
v_row company_invitations%rowtype;
|
|
99
|
+
begin
|
|
100
|
+
if auth.uid() is not null and not has_capability(p_company_id, 'members.manage')
|
|
101
|
+
and not is_instance_admin() then
|
|
102
|
+
raise exception 'not_allowed: inviting somebody into this company needs members.manage'
|
|
103
|
+
using errcode = '42501';
|
|
104
|
+
end if;
|
|
105
|
+
|
|
106
|
+
if v_email is null or v_email not like '%_@_%' then
|
|
107
|
+
raise exception 'bad_email: % is not an address an invitation can be sent to', p_email;
|
|
108
|
+
end if;
|
|
109
|
+
|
|
110
|
+
select c into v_unknown
|
|
111
|
+
from unnest(v_caps) as c
|
|
112
|
+
where c not in (select code from capabilities)
|
|
113
|
+
limit 1;
|
|
114
|
+
if v_unknown is not null then
|
|
115
|
+
raise exception 'unknown_capability: % is not a capability of this installation', v_unknown;
|
|
116
|
+
end if;
|
|
117
|
+
|
|
118
|
+
-- Re-inviting the same address replaces the pending invitation rather than
|
|
119
|
+
-- leaving two tokens alive for one seat.
|
|
120
|
+
update company_invitations
|
|
121
|
+
set revoked_at = now()
|
|
122
|
+
where company_id = p_company_id
|
|
123
|
+
and email = v_email
|
|
124
|
+
and accepted_at is null
|
|
125
|
+
and revoked_at is null;
|
|
126
|
+
|
|
127
|
+
v_token := replace(gen_random_uuid()::text, '-', '') ||
|
|
128
|
+
replace(gen_random_uuid()::text, '-', '');
|
|
129
|
+
|
|
130
|
+
insert into company_invitations (company_id, email, role, capabilities_granted,
|
|
131
|
+
token_hash, invited_by, expires_at)
|
|
132
|
+
values (p_company_id, v_email, p_role, v_caps,
|
|
133
|
+
encode(sha256(convert_to(v_token, 'UTF8')), 'hex'), auth.uid(), now() + p_valid_for)
|
|
134
|
+
returning * into v_row;
|
|
135
|
+
|
|
136
|
+
return query select v_row.id, v_token, v_row.expires_at;
|
|
137
|
+
end;
|
|
138
|
+
$$;
|
|
139
|
+
|
|
140
|
+
comment on function invite_member(uuid, text, member_role, jsonb, interval) is
|
|
141
|
+
'Invites an address into a company and returns the token once. Only the hash is stored; re-inviting the same address revokes the pending invitation.';
|
|
142
|
+
|
|
143
|
+
-- ---------------------------------------------------------------------------
|
|
144
|
+
-- Accepting one
|
|
145
|
+
--
|
|
146
|
+
-- Definer, because the invitee is nobody yet: they cannot read
|
|
147
|
+
-- `company_invitations` and cannot write `company_members`. What the function
|
|
148
|
+
-- checks is everything the policies would have: a live invitation, and the
|
|
149
|
+
-- address it was written for.
|
|
150
|
+
-- ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
create or replace function accept_invitation(p_token text)
|
|
153
|
+
returns company_members
|
|
154
|
+
language plpgsql
|
|
155
|
+
security definer
|
|
156
|
+
set search_path = public, pg_temp
|
|
157
|
+
as $$
|
|
158
|
+
declare
|
|
159
|
+
v_invitation company_invitations%rowtype;
|
|
160
|
+
v_member company_members%rowtype;
|
|
161
|
+
begin
|
|
162
|
+
if auth.uid() is null then
|
|
163
|
+
raise exception 'no_user: accepting an invitation is something a signed-in user does';
|
|
164
|
+
end if;
|
|
165
|
+
|
|
166
|
+
select * into v_invitation
|
|
167
|
+
from company_invitations
|
|
168
|
+
where token_hash = encode(sha256(convert_to(coalesce(p_token, ''), 'UTF8')), 'hex');
|
|
169
|
+
|
|
170
|
+
if v_invitation.id is null then
|
|
171
|
+
raise exception 'unknown_invitation: that is not an invitation of this installation';
|
|
172
|
+
end if;
|
|
173
|
+
if v_invitation.revoked_at is not null then
|
|
174
|
+
raise exception 'invitation_revoked: that invitation was withdrawn on %', v_invitation.revoked_at;
|
|
175
|
+
end if;
|
|
176
|
+
if v_invitation.accepted_at is not null then
|
|
177
|
+
raise exception 'invitation_already_accepted: that invitation was used on %', v_invitation.accepted_at;
|
|
178
|
+
end if;
|
|
179
|
+
if v_invitation.expires_at <= now() then
|
|
180
|
+
raise exception 'invitation_expired: that invitation expired on %', v_invitation.expires_at;
|
|
181
|
+
end if;
|
|
182
|
+
if lower(coalesce(auth.email(), '')) <> v_invitation.email then
|
|
183
|
+
raise exception 'invitation_not_yours: that invitation was sent to another address'
|
|
184
|
+
using errcode = '42501';
|
|
185
|
+
end if;
|
|
186
|
+
|
|
187
|
+
insert into company_members (company_id, user_id, role, capabilities_granted)
|
|
188
|
+
values (v_invitation.company_id, auth.uid(), v_invitation.role,
|
|
189
|
+
v_invitation.capabilities_granted)
|
|
190
|
+
on conflict (company_id, user_id) do nothing
|
|
191
|
+
returning * into v_member;
|
|
192
|
+
|
|
193
|
+
if v_member.user_id is null then
|
|
194
|
+
select * into v_member
|
|
195
|
+
from company_members
|
|
196
|
+
where company_id = v_invitation.company_id and user_id = auth.uid();
|
|
197
|
+
end if;
|
|
198
|
+
|
|
199
|
+
update company_invitations
|
|
200
|
+
set accepted_at = now(), accepted_by = auth.uid()
|
|
201
|
+
where id = v_invitation.id;
|
|
202
|
+
|
|
203
|
+
return v_member;
|
|
204
|
+
end;
|
|
205
|
+
$$;
|
|
206
|
+
|
|
207
|
+
comment on function accept_invitation(text) is
|
|
208
|
+
'Turns an invitation into a membership for the signed-in user, whose address has to be the one invited. Single use, and refused once expired.';
|
|
209
|
+
|
|
210
|
+
create or replace function revoke_invitation(p_invitation_id uuid)
|
|
211
|
+
returns company_invitations
|
|
212
|
+
language plpgsql
|
|
213
|
+
security definer
|
|
214
|
+
set search_path = public, pg_temp
|
|
215
|
+
as $$
|
|
216
|
+
declare
|
|
217
|
+
v_row company_invitations%rowtype;
|
|
218
|
+
begin
|
|
219
|
+
select * into v_row from company_invitations where id = p_invitation_id;
|
|
220
|
+
if v_row.id is null then
|
|
221
|
+
raise exception 'unknown_invitation: no invitation with that id';
|
|
222
|
+
end if;
|
|
223
|
+
|
|
224
|
+
if auth.uid() is not null and not has_capability(v_row.company_id, 'members.manage')
|
|
225
|
+
and not is_instance_admin() then
|
|
226
|
+
raise exception 'not_allowed: withdrawing an invitation needs members.manage'
|
|
227
|
+
using errcode = '42501';
|
|
228
|
+
end if;
|
|
229
|
+
|
|
230
|
+
if v_row.accepted_at is not null then
|
|
231
|
+
raise exception 'invitation_already_accepted: it became a membership on %; remove the member instead',
|
|
232
|
+
v_row.accepted_at;
|
|
233
|
+
end if;
|
|
234
|
+
|
|
235
|
+
update company_invitations set revoked_at = coalesce(revoked_at, now())
|
|
236
|
+
where id = p_invitation_id
|
|
237
|
+
returning * into v_row;
|
|
238
|
+
|
|
239
|
+
return v_row;
|
|
240
|
+
end;
|
|
241
|
+
$$;
|
|
242
|
+
|
|
243
|
+
comment on function revoke_invitation(uuid) is
|
|
244
|
+
'Withdraws an invitation that has not been accepted. An accepted one is a member, and members are removed from company_members.';
|
|
245
|
+
|
|
246
|
+
-- ---------------------------------------------------------------------------
|
|
247
|
+
-- Row level security
|
|
248
|
+
--
|
|
249
|
+
-- Reading an invitation is administering members, so it is `members.manage`
|
|
250
|
+
-- and the instance administrator. Nothing writes through the API: the three
|
|
251
|
+
-- functions above are the only way in, and each one checks what a policy
|
|
252
|
+
-- would have.
|
|
253
|
+
-- ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
alter table company_invitations enable row level security;
|
|
256
|
+
|
|
257
|
+
create policy company_invitations_select on company_invitations
|
|
258
|
+
for select using (
|
|
259
|
+
has_capability(company_id, 'members.manage') or is_instance_admin()
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
revoke execute on all functions in schema public from public;
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
-- Ekwo OS — what a person prefers, and the one way a label is chosen.
|
|
2
|
+
--
|
|
3
|
+
-- Two things that turned out to be the same thing.
|
|
4
|
+
--
|
|
5
|
+
-- **A preference belongs to a person, not to a company.** Which company they
|
|
6
|
+
-- work in by default, the language they read in, their timezone, how they
|
|
7
|
+
-- like a date and a number written. None of it has a default in the schema,
|
|
8
|
+
-- for the reason P0-7 and P0-8 gave before: a default language is one
|
|
9
|
+
-- country's answer handed to everybody who has not spoken, and null means
|
|
10
|
+
-- "ask the company, then the pack, then the client" rather than "English".
|
|
11
|
+
--
|
|
12
|
+
-- **A label is chosen in one place.** `name_i18n` and `text_i18n` sit on the
|
|
13
|
+
-- chart, the journals, the declaration boxes, the statement lines and the
|
|
14
|
+
-- legal mentions, and the way to read one was written out as
|
|
15
|
+
-- `coalesce(nullif(x.name_i18n ->> lang, ''), x.name)` wherever it was
|
|
16
|
+
-- needed. That is a formula, and a formula written twice is a formula that
|
|
17
|
+
-- will one day disagree with itself. `label_for(name, name_i18n, languages)`
|
|
18
|
+
-- is now the only spelling of it, it takes a **list** of languages rather
|
|
19
|
+
-- than one, and `preferred_languages(company)` builds that list: the user's
|
|
20
|
+
-- own, then the company's, then the pack's.
|
|
21
|
+
--
|
|
22
|
+
-- `install_country_template()` is republished on top of it. It keeps passing
|
|
23
|
+
-- one language and the company's, deliberately: a chart of accounts is copied
|
|
24
|
+
-- in the language the books are kept in, not in the language of whoever
|
|
25
|
+
-- happened to run the installer.
|
|
26
|
+
|
|
27
|
+
-- ---------------------------------------------------------------------------
|
|
28
|
+
-- label_for — the one way a translated label is picked
|
|
29
|
+
-- ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
create or replace function label_for(p_name text, p_i18n jsonb, p_languages text[])
|
|
32
|
+
returns text
|
|
33
|
+
language sql
|
|
34
|
+
immutable
|
|
35
|
+
as $$
|
|
36
|
+
select coalesce(
|
|
37
|
+
(select nullif(p_i18n ->> lang, '')
|
|
38
|
+
from unnest(coalesce(p_languages, '{}')) with ordinality as l(lang, ord)
|
|
39
|
+
where nullif(p_i18n ->> lang, '') is not null
|
|
40
|
+
order by l.ord
|
|
41
|
+
limit 1),
|
|
42
|
+
p_name);
|
|
43
|
+
$$;
|
|
44
|
+
|
|
45
|
+
comment on function label_for(text, jsonb, text[]) is
|
|
46
|
+
'The label in the first language of the list that has one, and the row''s own name when none of them does. The only place a translated label is chosen.';
|
|
47
|
+
|
|
48
|
+
-- ---------------------------------------------------------------------------
|
|
49
|
+
-- user_preferences
|
|
50
|
+
-- ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
create table user_preferences (
|
|
53
|
+
user_id uuid primary key references auth.users(id) on delete cascade,
|
|
54
|
+
preferred_company_id uuid references companies(id) on delete set null,
|
|
55
|
+
language text,
|
|
56
|
+
timezone text,
|
|
57
|
+
-- `*_display_format` and not `date_format` / `number_format`: the second of
|
|
58
|
+
-- those is already a column of `country_defaults`, where it is the pattern
|
|
59
|
+
-- a document number is built from. Two questions that have nothing to do
|
|
60
|
+
-- with each other should not answer to one name — a reader meeting both
|
|
61
|
+
-- would have to know which table they were in to know what they had.
|
|
62
|
+
date_display_format text,
|
|
63
|
+
number_display_format text,
|
|
64
|
+
theme text,
|
|
65
|
+
created_at timestamptz not null default now(),
|
|
66
|
+
updated_at timestamptz not null default now(),
|
|
67
|
+
constraint user_preferences_language_shape
|
|
68
|
+
check (language is null or language ~ '^[a-z]{2}(-[A-Za-z0-9]{2,8})?$')
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
comment on table user_preferences is
|
|
72
|
+
'What one person prefers, across every company they are a member of. Every column is nullable and none has a default: null means "take the company''s answer, then the pack''s".';
|
|
73
|
+
comment on column user_preferences.preferred_company_id is
|
|
74
|
+
'The company an interface opens on. Cleared rather than kept when that company is deleted.';
|
|
75
|
+
comment on column user_preferences.language is
|
|
76
|
+
'Language this person reads labels in. First in the list preferred_languages() builds.';
|
|
77
|
+
comment on column user_preferences.theme is
|
|
78
|
+
'A client''s business. The core stores it and interprets nothing.';
|
|
79
|
+
|
|
80
|
+
create trigger user_preferences_set_updated_at
|
|
81
|
+
before update on user_preferences
|
|
82
|
+
for each row execute function set_updated_at();
|
|
83
|
+
|
|
84
|
+
-- ---------------------------------------------------------------------------
|
|
85
|
+
-- preferred_languages — the chain a reader follows
|
|
86
|
+
-- ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
create or replace function preferred_languages(p_company_id uuid default null)
|
|
89
|
+
returns text[]
|
|
90
|
+
language sql
|
|
91
|
+
stable
|
|
92
|
+
security definer
|
|
93
|
+
set search_path = public, pg_temp
|
|
94
|
+
as $$
|
|
95
|
+
select array_remove(array[
|
|
96
|
+
(select p.language from user_preferences p where p.user_id = auth.uid()),
|
|
97
|
+
(select c.language from companies c where c.id = p_company_id),
|
|
98
|
+
(select d.language_default
|
|
99
|
+
from companies c join country_defaults d on d.country = c.country
|
|
100
|
+
where c.id = p_company_id)
|
|
101
|
+
], null);
|
|
102
|
+
$$;
|
|
103
|
+
|
|
104
|
+
comment on function preferred_languages(uuid) is
|
|
105
|
+
'The languages to try, in order: the user''s own, then the company''s, then the one the country pack declares. Feed it to label_for().';
|
|
106
|
+
|
|
107
|
+
-- ---------------------------------------------------------------------------
|
|
108
|
+
-- install_country_template, republished on label_for
|
|
109
|
+
--
|
|
110
|
+
-- Unchanged but for one expression: the chart is copied under
|
|
111
|
+
-- `label_for(t.name, t.name_i18n, array[v_language])` instead of the coalesce
|
|
112
|
+
-- that was written out here. Same answer, one formula.
|
|
113
|
+
-- ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
create or replace function install_country_template(
|
|
116
|
+
p_company_id uuid,
|
|
117
|
+
p_country char(2),
|
|
118
|
+
p_language char(2) default null,
|
|
119
|
+
p_chart_code text default null
|
|
120
|
+
)
|
|
121
|
+
returns void
|
|
122
|
+
language plpgsql
|
|
123
|
+
as $$
|
|
124
|
+
declare
|
|
125
|
+
v_defaults country_defaults%rowtype;
|
|
126
|
+
r record;
|
|
127
|
+
v_tax_id uuid;
|
|
128
|
+
v_language char(2);
|
|
129
|
+
v_version text;
|
|
130
|
+
v_chart text;
|
|
131
|
+
v_charts text;
|
|
132
|
+
begin
|
|
133
|
+
if not exists (select 1 from companies where id = p_company_id) then
|
|
134
|
+
raise exception 'unknown_company: %', p_company_id;
|
|
135
|
+
end if;
|
|
136
|
+
|
|
137
|
+
-- Which chart. The caller names one, or the pack's default stands. A named
|
|
138
|
+
-- chart that does not exist is an error that lists what does: guessing here
|
|
139
|
+
-- would install the wrong plan of accounts and nobody would notice for a
|
|
140
|
+
-- year.
|
|
141
|
+
if p_chart_code is null then
|
|
142
|
+
select c.code into v_chart
|
|
143
|
+
from chart_templates c
|
|
144
|
+
where c.country = p_country and c.is_default;
|
|
145
|
+
if v_chart is null then
|
|
146
|
+
-- No pack for this country, or a pack that names no default chart.
|
|
147
|
+
raise exception 'unknown_country_template: no chart of accounts seeded for %', p_country;
|
|
148
|
+
end if;
|
|
149
|
+
else
|
|
150
|
+
select c.code into v_chart
|
|
151
|
+
from chart_templates c
|
|
152
|
+
where c.country = p_country and c.code = p_chart_code;
|
|
153
|
+
if v_chart is null then
|
|
154
|
+
select string_agg(c.code, ', ' order by c.code) into v_charts
|
|
155
|
+
from chart_templates c where c.country = p_country;
|
|
156
|
+
raise exception 'unknown_chart: % has no chart %. It has: %',
|
|
157
|
+
p_country, p_chart_code, coalesce(v_charts, 'none');
|
|
158
|
+
end if;
|
|
159
|
+
end if;
|
|
160
|
+
|
|
161
|
+
if not exists (
|
|
162
|
+
select 1 from account_templates
|
|
163
|
+
where country = p_country and chart_code = v_chart
|
|
164
|
+
) then
|
|
165
|
+
raise exception 'unknown_country_template: chart % of % holds no account', v_chart, p_country;
|
|
166
|
+
end if;
|
|
167
|
+
|
|
168
|
+
-- The language of the copy: what the caller asked for, else what the
|
|
169
|
+
-- company keeps its books in.
|
|
170
|
+
select coalesce(p_language, c.language) into v_language
|
|
171
|
+
from companies c where c.id = p_company_id;
|
|
172
|
+
|
|
173
|
+
-- 1. Accounts, without the hierarchy.
|
|
174
|
+
insert into accounts (company_id, code, name, name_i18n, statement_hint,
|
|
175
|
+
account_type, reconcilable)
|
|
176
|
+
select p_company_id, t.code,
|
|
177
|
+
label_for(t.name, t.name_i18n, array[v_language]),
|
|
178
|
+
t.name_i18n, t.statement_hint, t.account_type, t.reconcilable
|
|
179
|
+
from account_templates t
|
|
180
|
+
where t.country = p_country
|
|
181
|
+
and t.chart_code = v_chart
|
|
182
|
+
on conflict (company_id, code) do nothing;
|
|
183
|
+
|
|
184
|
+
-- 2. Hierarchy, now that every code exists.
|
|
185
|
+
update accounts a
|
|
186
|
+
set parent_id = p.id
|
|
187
|
+
from account_templates t
|
|
188
|
+
join accounts p on p.company_id = p_company_id and p.code = t.parent_code
|
|
189
|
+
where a.company_id = p_company_id
|
|
190
|
+
and a.code = t.code
|
|
191
|
+
and t.country = p_country
|
|
192
|
+
and t.chart_code = v_chart
|
|
193
|
+
and t.parent_code is not null
|
|
194
|
+
and a.parent_id is null;
|
|
195
|
+
|
|
196
|
+
-- 3. Journals. Common to every chart of the country.
|
|
197
|
+
insert into journals (company_id, code, name, journal_type)
|
|
198
|
+
select p_company_id, t.code, t.name, t.journal_type
|
|
199
|
+
from journal_templates t
|
|
200
|
+
where t.country = p_country
|
|
201
|
+
on conflict (company_id, code) do nothing;
|
|
202
|
+
|
|
203
|
+
-- 4. Taxes and their postings. Also common: a chart changes how a company
|
|
204
|
+
-- presents itself, not what it owes.
|
|
205
|
+
for r in
|
|
206
|
+
select * from tax_templates where country = p_country order by sequence, code
|
|
207
|
+
loop
|
|
208
|
+
v_tax_id := null;
|
|
209
|
+
insert into taxes (company_id, code, name, description, amount_type, amount,
|
|
210
|
+
applies_to, treatment, country, valid_from, valid_to,
|
|
211
|
+
legal_reference, vat_category, exemption_code, sequence,
|
|
212
|
+
tax_kind, recoverable, jurisdiction, price_include,
|
|
213
|
+
cash_basis, cash_basis_transition_account_id)
|
|
214
|
+
values (p_company_id, r.code, r.name, r.description, r.amount_type, r.amount,
|
|
215
|
+
r.applies_to, r.treatment, p_country, r.valid_from, r.valid_to,
|
|
216
|
+
r.legal_reference, r.vat_category, r.exemption_code, r.sequence,
|
|
217
|
+
r.tax_kind, r.recoverable, r.jurisdiction, r.price_include,
|
|
218
|
+
r.cash_basis, account_id_by_code(p_company_id, r.cash_basis_transition_account_code))
|
|
219
|
+
on conflict (company_id, code) do nothing
|
|
220
|
+
returning id into v_tax_id;
|
|
221
|
+
|
|
222
|
+
if v_tax_id is null then
|
|
223
|
+
continue;
|
|
224
|
+
end if;
|
|
225
|
+
|
|
226
|
+
insert into tax_postings (tax_id, company_id, document_kind, posting_type,
|
|
227
|
+
factor_percent, account_id, declaration_box,
|
|
228
|
+
box_factor_percent, report_code, sequence)
|
|
229
|
+
select v_tax_id, p_company_id, tp.document_kind, tp.posting_type,
|
|
230
|
+
tp.factor_percent, account_id_by_code(p_company_id, tp.account_code),
|
|
231
|
+
tp.declaration_box, tp.box_factor_percent, tp.report_code, tp.sequence
|
|
232
|
+
from tax_posting_templates tp
|
|
233
|
+
where tp.tax_template_id = r.id
|
|
234
|
+
order by tp.sequence;
|
|
235
|
+
end loop;
|
|
236
|
+
|
|
237
|
+
-- 5. Roles.
|
|
238
|
+
select * into v_defaults from country_defaults where country = p_country;
|
|
239
|
+
if found then
|
|
240
|
+
update companies c
|
|
241
|
+
set receivable_account_id = coalesce(c.receivable_account_id, account_id_by_code(p_company_id, v_defaults.receivable_code)),
|
|
242
|
+
payable_account_id = coalesce(c.payable_account_id, account_id_by_code(p_company_id, v_defaults.payable_code)),
|
|
243
|
+
suspense_account_id = coalesce(c.suspense_account_id, account_id_by_code(p_company_id, v_defaults.suspense_code)),
|
|
244
|
+
rounding_account_id = coalesce(c.rounding_account_id, account_id_by_code(p_company_id, v_defaults.rounding_code)),
|
|
245
|
+
retained_earnings_account_id = coalesce(c.retained_earnings_account_id, account_id_by_code(p_company_id, v_defaults.retained_earnings_code)),
|
|
246
|
+
default_sales_account_id = coalesce(c.default_sales_account_id, account_id_by_code(p_company_id, v_defaults.sales_account_code)),
|
|
247
|
+
default_purchase_account_id = coalesce(c.default_purchase_account_id, account_id_by_code(p_company_id, v_defaults.purchase_account_code)),
|
|
248
|
+
sales_journal_id = coalesce(c.sales_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.sales_journal_code)),
|
|
249
|
+
purchase_journal_id = coalesce(c.purchase_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.purchase_journal_code)),
|
|
250
|
+
miscellaneous_journal_id = coalesce(c.miscellaneous_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.misc_journal_code))
|
|
251
|
+
where c.id = p_company_id;
|
|
252
|
+
|
|
253
|
+
-- 6. The financial journals point at their account.
|
|
254
|
+
update journals j
|
|
255
|
+
set default_account_id = account_id_by_code(p_company_id, v_defaults.bank_account_code)
|
|
256
|
+
where j.company_id = p_company_id
|
|
257
|
+
and j.journal_type = 'bank'
|
|
258
|
+
and j.default_account_id is null
|
|
259
|
+
and v_defaults.bank_account_code is not null;
|
|
260
|
+
|
|
261
|
+
update journals j
|
|
262
|
+
set default_account_id = account_id_by_code(p_company_id, v_defaults.cash_account_code)
|
|
263
|
+
where j.company_id = p_company_id
|
|
264
|
+
and j.journal_type = 'cash'
|
|
265
|
+
and j.default_account_id is null
|
|
266
|
+
and v_defaults.cash_account_code is not null;
|
|
267
|
+
end if;
|
|
268
|
+
|
|
269
|
+
-- 7. What was copied, from which version, and on which chart.
|
|
270
|
+
v_version := coalesce((select version from country_packs where country = p_country), '1.0.0');
|
|
271
|
+
|
|
272
|
+
insert into company_packs (company_id, country, version, chart_code)
|
|
273
|
+
values (p_company_id, p_country, v_version, v_chart)
|
|
274
|
+
on conflict (company_id, country) do update
|
|
275
|
+
set version = excluded.version,
|
|
276
|
+
chart_code = excluded.chart_code,
|
|
277
|
+
upgraded_at = now()
|
|
278
|
+
where company_packs.version is distinct from excluded.version
|
|
279
|
+
or company_packs.chart_code is distinct from excluded.chart_code;
|
|
280
|
+
end;
|
|
281
|
+
$$;
|
|
282
|
+
|
|
283
|
+
-- ---------------------------------------------------------------------------
|
|
284
|
+
-- Writing them
|
|
285
|
+
--
|
|
286
|
+
-- A patch rather than a row, and jsonb rather than seven arguments, for one
|
|
287
|
+
-- reason: with arguments there is no way to tell "leave the timezone alone"
|
|
288
|
+
-- from "clear the timezone". A key that is present is written — null
|
|
289
|
+
-- included, which is how a preference is cleared — and a key that is absent
|
|
290
|
+
-- is untouched. A key nobody declared is refused rather than dropped on the
|
|
291
|
+
-- floor, because a preference that silently did not save is worse than an
|
|
292
|
+
-- error.
|
|
293
|
+
--
|
|
294
|
+
-- It writes the caller's own row and no other: `auth.uid()` is the key, and
|
|
295
|
+
-- it is not an argument.
|
|
296
|
+
-- ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
create or replace function set_preferences(p_patch jsonb)
|
|
299
|
+
returns user_preferences
|
|
300
|
+
language plpgsql
|
|
301
|
+
as $$
|
|
302
|
+
declare
|
|
303
|
+
v_known text[] := array['preferred_company_id', 'language', 'timezone',
|
|
304
|
+
'date_display_format', 'number_display_format', 'theme'];
|
|
305
|
+
v_unknown text;
|
|
306
|
+
v_row user_preferences%rowtype;
|
|
307
|
+
begin
|
|
308
|
+
if auth.uid() is null then
|
|
309
|
+
raise exception 'no_user: preferences belong to a signed-in user';
|
|
310
|
+
end if;
|
|
311
|
+
if p_patch is null or jsonb_typeof(p_patch) <> 'object' then
|
|
312
|
+
raise exception 'bad_patch: set_preferences takes an object of the preferences to change';
|
|
313
|
+
end if;
|
|
314
|
+
|
|
315
|
+
select k into v_unknown
|
|
316
|
+
from jsonb_object_keys(p_patch) as k
|
|
317
|
+
where k <> all (v_known)
|
|
318
|
+
limit 1;
|
|
319
|
+
if v_unknown is not null then
|
|
320
|
+
raise exception 'unknown_preference: % is not a preference this installation keeps (%)',
|
|
321
|
+
v_unknown, array_to_string(v_known, ', ');
|
|
322
|
+
end if;
|
|
323
|
+
|
|
324
|
+
insert into user_preferences (user_id, preferred_company_id, language, timezone,
|
|
325
|
+
date_display_format, number_display_format, theme)
|
|
326
|
+
values (auth.uid(),
|
|
327
|
+
nullif(p_patch ->> 'preferred_company_id', '')::uuid,
|
|
328
|
+
nullif(p_patch ->> 'language', ''),
|
|
329
|
+
nullif(p_patch ->> 'timezone', ''),
|
|
330
|
+
nullif(p_patch ->> 'date_display_format', ''),
|
|
331
|
+
nullif(p_patch ->> 'number_display_format', ''),
|
|
332
|
+
nullif(p_patch ->> 'theme', ''))
|
|
333
|
+
on conflict (user_id) do update set
|
|
334
|
+
preferred_company_id = case when p_patch ? 'preferred_company_id'
|
|
335
|
+
then nullif(p_patch ->> 'preferred_company_id', '')::uuid
|
|
336
|
+
else user_preferences.preferred_company_id end,
|
|
337
|
+
language = case when p_patch ? 'language'
|
|
338
|
+
then nullif(p_patch ->> 'language', '')
|
|
339
|
+
else user_preferences.language end,
|
|
340
|
+
timezone = case when p_patch ? 'timezone'
|
|
341
|
+
then nullif(p_patch ->> 'timezone', '')
|
|
342
|
+
else user_preferences.timezone end,
|
|
343
|
+
date_display_format = case when p_patch ? 'date_display_format'
|
|
344
|
+
then nullif(p_patch ->> 'date_display_format', '')
|
|
345
|
+
else user_preferences.date_display_format end,
|
|
346
|
+
number_display_format = case when p_patch ? 'number_display_format'
|
|
347
|
+
then nullif(p_patch ->> 'number_display_format', '')
|
|
348
|
+
else user_preferences.number_display_format end,
|
|
349
|
+
theme = case when p_patch ? 'theme'
|
|
350
|
+
then nullif(p_patch ->> 'theme', '')
|
|
351
|
+
else user_preferences.theme end
|
|
352
|
+
returning * into v_row;
|
|
353
|
+
|
|
354
|
+
return v_row;
|
|
355
|
+
end;
|
|
356
|
+
$$;
|
|
357
|
+
|
|
358
|
+
comment on function set_preferences(jsonb) is
|
|
359
|
+
'Writes the signed-in user''s preferences. A key that is present is written, null included; a key that is absent is left alone; a key nobody declared is refused.';
|
|
360
|
+
|
|
361
|
+
-- ---------------------------------------------------------------------------
|
|
362
|
+
-- Row level security
|
|
363
|
+
--
|
|
364
|
+
-- A preference is nobody else's business — not another member's, not an
|
|
365
|
+
-- instance administrator's. One policy, own row, both ways.
|
|
366
|
+
-- ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
alter table user_preferences enable row level security;
|
|
369
|
+
|
|
370
|
+
create policy user_preferences_own on user_preferences
|
|
371
|
+
for all using (user_id = auth.uid()) with check (user_id = auth.uid());
|
|
372
|
+
|
|
373
|
+
comment on policy user_preferences_own on user_preferences is
|
|
374
|
+
'Yours and nobody else''s, including an administrator''s.';
|
|
375
|
+
|
|
376
|
+
revoke execute on all functions in schema public from public;
|