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,493 @@
|
|
|
1
|
+
-- Ekwo OS — modules: one Postgres schema each, the ledger only through a function.
|
|
2
|
+
--
|
|
3
|
+
-- The socle is `public` and knows nothing about what is built beside it. A
|
|
4
|
+
-- module is a schema of its own — `assets`, `budgets`, `carbon` — with its own
|
|
5
|
+
-- migrations, its own row level security and its own tests. It depends on the
|
|
6
|
+
-- socle by foreign key (`public.companies`, `public.accounts`,
|
|
7
|
+
-- `public.contacts`, `public.products`, `public.fiscal_years`) and it never
|
|
8
|
+
-- writes the ledger by hand.
|
|
9
|
+
--
|
|
10
|
+
-- Three things live here, and nothing else:
|
|
11
|
+
--
|
|
12
|
+
-- 1. **The registry is a table, not code.** `modules` is one row per module
|
|
13
|
+
-- this installation carries, written by the module's own first migration.
|
|
14
|
+
-- There is deliberately no plugin registry in TypeScript: a module that
|
|
15
|
+
-- is installed is a schema that exists and a row that says so, and one
|
|
16
|
+
-- query answers "what is here" for the CLI, the MCP server and a human.
|
|
17
|
+
--
|
|
18
|
+
-- 2. **A module is enabled per company.** `company_modules` is that, and it
|
|
19
|
+
-- is written only by `enable_module()` and `disable_module()` — there is
|
|
20
|
+
-- no write policy on the table at all, so the guard inside the function
|
|
21
|
+
-- is the only way in rather than the polite way in. Same reason
|
|
22
|
+
-- `fiscal_years.is_closed` stopped being an ordinary column.
|
|
23
|
+
--
|
|
24
|
+
-- 3. **A module posts through `post_module_entry()`.** It assembles nothing:
|
|
25
|
+
-- it hands over a company, a date, a journal, a tag and a list of lines,
|
|
26
|
+
-- and the function builds the draft and calls `post_entry()` — which
|
|
27
|
+
-- stays the one place an entry is validated, numbered and locked against
|
|
28
|
+
-- a closed period. So a module never names `entries` or `entry_lines` in
|
|
29
|
+
-- a statement of its own, and a test over `modules/**` proves it.
|
|
30
|
+
--
|
|
31
|
+
-- **What the tag buys.** `entries.module_code` and `entries.module_ref` say
|
|
32
|
+
-- which module wrote an entry and what for, and a unique index on the three
|
|
33
|
+
-- makes a second posting of the same thing impossible rather than merely
|
|
34
|
+
-- discouraged. `assets.run_depreciation` is idempotent because the database
|
|
35
|
+
-- refuses the duplicate, not because the module remembered to look. Tagging by
|
|
36
|
+
-- `(module_code, ref)` is also why `entry_kind` gains no value per module: a
|
|
37
|
+
-- depreciation entry is a normal entry that a report may leave in.
|
|
38
|
+
--
|
|
39
|
+
-- **What the socle does not gain.** No hook, no callback, no way for a module
|
|
40
|
+
-- to change what posting means. The dependency points one way: a module reads
|
|
41
|
+
-- and calls the socle, the socle ignores its modules. The single exception is
|
|
42
|
+
-- `disable_module()`, which asks a module whether it still holds posted data —
|
|
43
|
+
-- by looking for `<schema>.can_disable(uuid)` and running it if it is there.
|
|
44
|
+
-- That is a convention rather than a column, so a module that holds nothing
|
|
45
|
+
-- writes nothing.
|
|
46
|
+
|
|
47
|
+
-- ---------------------------------------------------------------------------
|
|
48
|
+
-- The registry
|
|
49
|
+
-- ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
create type module_status as enum ('draft', 'available', 'deprecated');
|
|
52
|
+
|
|
53
|
+
comment on type module_status is
|
|
54
|
+
'Whether a module may be enabled on a company: draft is not ready, available is, deprecated keeps the companies that already have it and refuses new ones.';
|
|
55
|
+
|
|
56
|
+
create table modules (
|
|
57
|
+
code text primary key,
|
|
58
|
+
name text not null,
|
|
59
|
+
description text,
|
|
60
|
+
-- The Postgres schema the module lives in. Unique, because two modules in
|
|
61
|
+
-- one schema is two sets of tables nobody can tell apart.
|
|
62
|
+
schema_name text not null unique,
|
|
63
|
+
version text not null,
|
|
64
|
+
status module_status not null default 'draft',
|
|
65
|
+
-- The oldest socle migration this module needs, as its timestamp version —
|
|
66
|
+
-- the same shape `country_packs.schema_min` uses. Read by `ekwo module
|
|
67
|
+
-- enable`, which holds the migration history and can compare; the function
|
|
68
|
+
-- below does not, because the history table is the CLI's business and a
|
|
69
|
+
-- database restored from a dump may not carry it.
|
|
70
|
+
requires_socle_min text,
|
|
71
|
+
created_at timestamptz not null default now(),
|
|
72
|
+
updated_at timestamptz not null default now(),
|
|
73
|
+
constraint modules_code_format check (code ~ '^[a-z][a-z0-9_]{1,30}$'),
|
|
74
|
+
constraint modules_schema_format check (schema_name ~ '^[a-z][a-z0-9_]{1,30}$'),
|
|
75
|
+
constraint modules_schema_not_public check (schema_name <> 'public')
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
comment on table modules is
|
|
79
|
+
'One row per module this installation carries, written by the module''s own first migration. The registry is a table, not code.';
|
|
80
|
+
comment on column modules.schema_name is
|
|
81
|
+
'The Postgres schema the module lives in. PostgREST only exposes it once it is listed in the API settings, which no migration can do — `ekwo module enable` prints the line to add.';
|
|
82
|
+
comment on column modules.requires_socle_min is
|
|
83
|
+
'Oldest socle migration version this module needs. Read by `ekwo module enable` against the migration history.';
|
|
84
|
+
|
|
85
|
+
create trigger modules_set_updated_at
|
|
86
|
+
before update on modules
|
|
87
|
+
for each row execute function set_updated_at();
|
|
88
|
+
|
|
89
|
+
-- ---------------------------------------------------------------------------
|
|
90
|
+
-- Which company holds which module
|
|
91
|
+
-- ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
create table company_modules (
|
|
94
|
+
company_id uuid not null references companies(id) on delete cascade,
|
|
95
|
+
module_code text not null references modules(code) on delete restrict,
|
|
96
|
+
enabled_at timestamptz not null default now(),
|
|
97
|
+
enabled_by uuid,
|
|
98
|
+
settings jsonb not null default '{}'::jsonb,
|
|
99
|
+
primary key (company_id, module_code)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
comment on table company_modules is
|
|
103
|
+
'Modules enabled on a company, and the settings that company keeps for each. Written by enable_module() and disable_module() and by nothing else: there is no write policy.';
|
|
104
|
+
comment on column company_modules.settings is
|
|
105
|
+
'Per-company settings of the module, in its own vocabulary. The socle never reads inside this object.';
|
|
106
|
+
|
|
107
|
+
create index company_modules_module_idx on company_modules (module_code);
|
|
108
|
+
|
|
109
|
+
-- ---------------------------------------------------------------------------
|
|
110
|
+
-- The two questions a module asks
|
|
111
|
+
--
|
|
112
|
+
-- `module_is_enabled` is the bare fact and answers for anybody. `module_enabled`
|
|
113
|
+
-- is the one a row level security policy calls: it is the fact **and** the
|
|
114
|
+
-- membership, so a module policy is one call and a stranger gets the same word
|
|
115
|
+
-- `is_company_member` already gives them — no. That is why it can be granted to
|
|
116
|
+
-- `anon` alongside the eight helpers: without the grant an anonymous select on
|
|
117
|
+
-- a module table would raise "permission denied for function" instead of
|
|
118
|
+
-- returning nothing, and an error is a louder answer than an empty set.
|
|
119
|
+
-- ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
create or replace function module_is_enabled(p_company_id uuid, p_code text)
|
|
122
|
+
returns boolean
|
|
123
|
+
language sql
|
|
124
|
+
stable
|
|
125
|
+
security definer
|
|
126
|
+
set search_path = public, pg_temp
|
|
127
|
+
as $$
|
|
128
|
+
select exists (
|
|
129
|
+
select 1 from company_modules m
|
|
130
|
+
where m.company_id = p_company_id and m.module_code = p_code
|
|
131
|
+
);
|
|
132
|
+
$$;
|
|
133
|
+
|
|
134
|
+
comment on function module_is_enabled(uuid, text) is
|
|
135
|
+
'Whether a module is enabled on a company, regardless of who is asking. Definer so a policy on company_modules cannot recurse into it.';
|
|
136
|
+
|
|
137
|
+
create or replace function module_enabled(p_company_id uuid, p_code text)
|
|
138
|
+
returns boolean
|
|
139
|
+
language sql
|
|
140
|
+
stable
|
|
141
|
+
as $$
|
|
142
|
+
select is_company_member(p_company_id) and module_is_enabled(p_company_id, p_code);
|
|
143
|
+
$$;
|
|
144
|
+
|
|
145
|
+
comment on function module_enabled(uuid, text) is
|
|
146
|
+
'The helper a module''s row level security policies call: this module is enabled on this company and the caller is a member of it. One call, and the answer to a stranger is no.';
|
|
147
|
+
|
|
148
|
+
create or replace function module_settings(p_company_id uuid, p_code text)
|
|
149
|
+
returns jsonb
|
|
150
|
+
language sql
|
|
151
|
+
stable
|
|
152
|
+
security definer
|
|
153
|
+
set search_path = public, pg_temp
|
|
154
|
+
as $$
|
|
155
|
+
select m.settings
|
|
156
|
+
from company_modules m
|
|
157
|
+
where m.company_id = p_company_id and m.module_code = p_code;
|
|
158
|
+
$$;
|
|
159
|
+
|
|
160
|
+
comment on function module_settings(uuid, text) is
|
|
161
|
+
'The settings a company keeps for one of its modules, or null when the module is not enabled. The socle never looks inside the object.';
|
|
162
|
+
|
|
163
|
+
-- ---------------------------------------------------------------------------
|
|
164
|
+
-- Enabling and disabling
|
|
165
|
+
--
|
|
166
|
+
-- Both are definer with the guard written inside, because `company_modules`
|
|
167
|
+
-- has no write policy: the function is the only way in, and the rule applies
|
|
168
|
+
-- to psql and PostgREST alike. `register_instance()` is the same shape.
|
|
169
|
+
-- ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
create or replace function enable_module(
|
|
172
|
+
p_company_id uuid,
|
|
173
|
+
p_code text,
|
|
174
|
+
p_settings jsonb default null
|
|
175
|
+
)
|
|
176
|
+
returns company_modules
|
|
177
|
+
language plpgsql
|
|
178
|
+
security definer
|
|
179
|
+
set search_path = public, pg_temp
|
|
180
|
+
as $$
|
|
181
|
+
declare
|
|
182
|
+
v_module modules%rowtype;
|
|
183
|
+
v_row company_modules%rowtype;
|
|
184
|
+
begin
|
|
185
|
+
if not is_company_owner(p_company_id) then
|
|
186
|
+
raise exception 'not_company_owner: enabling a module on a company is the owner''s decision'
|
|
187
|
+
using errcode = '42501';
|
|
188
|
+
end if;
|
|
189
|
+
|
|
190
|
+
select * into v_module from modules where code = p_code;
|
|
191
|
+
if not found then
|
|
192
|
+
raise exception 'unknown_module: % is not installed on this instance; apply its migrations first', p_code;
|
|
193
|
+
end if;
|
|
194
|
+
|
|
195
|
+
if v_module.status = 'draft' and not module_is_enabled(p_company_id, p_code) then
|
|
196
|
+
raise exception 'module_not_available: % is still a draft on this installation', p_code;
|
|
197
|
+
end if;
|
|
198
|
+
if v_module.status = 'deprecated' and not module_is_enabled(p_company_id, p_code) then
|
|
199
|
+
raise exception 'module_deprecated: % is deprecated and takes no new company', p_code;
|
|
200
|
+
end if;
|
|
201
|
+
|
|
202
|
+
insert into company_modules (company_id, module_code, enabled_by, settings)
|
|
203
|
+
values (p_company_id, p_code, auth.uid(), coalesce(p_settings, '{}'::jsonb))
|
|
204
|
+
on conflict (company_id, module_code) do update
|
|
205
|
+
set settings = coalesce(p_settings, company_modules.settings)
|
|
206
|
+
returning * into v_row;
|
|
207
|
+
|
|
208
|
+
return v_row;
|
|
209
|
+
end;
|
|
210
|
+
$$;
|
|
211
|
+
|
|
212
|
+
comment on function enable_module(uuid, text, jsonb) is
|
|
213
|
+
'Enables a module on a company, and updates its settings when it is already enabled. The owner''s decision, checked here because the table has no write policy.';
|
|
214
|
+
|
|
215
|
+
create or replace function disable_module(p_company_id uuid, p_code text)
|
|
216
|
+
returns void
|
|
217
|
+
language plpgsql
|
|
218
|
+
security definer
|
|
219
|
+
set search_path = public, pg_temp
|
|
220
|
+
as $$
|
|
221
|
+
declare
|
|
222
|
+
v_module modules%rowtype;
|
|
223
|
+
v_check regprocedure;
|
|
224
|
+
v_reason text;
|
|
225
|
+
begin
|
|
226
|
+
if not is_company_owner(p_company_id) then
|
|
227
|
+
raise exception 'not_company_owner: disabling a module on a company is the owner''s decision'
|
|
228
|
+
using errcode = '42501';
|
|
229
|
+
end if;
|
|
230
|
+
|
|
231
|
+
select * into v_module from modules where code = p_code;
|
|
232
|
+
if not found then
|
|
233
|
+
raise exception 'unknown_module: % is not installed on this instance', p_code;
|
|
234
|
+
end if;
|
|
235
|
+
|
|
236
|
+
if not module_is_enabled(p_company_id, p_code) then
|
|
237
|
+
return;
|
|
238
|
+
end if;
|
|
239
|
+
|
|
240
|
+
-- A module says for itself whether it still holds something a company would
|
|
241
|
+
-- lose. The convention is one function, `<schema>.can_disable(uuid)`,
|
|
242
|
+
-- returning null when there is nothing in the way and a sentence when there
|
|
243
|
+
-- is. A module that holds no posted data writes none, and `to_regprocedure`
|
|
244
|
+
-- answers null rather than raising.
|
|
245
|
+
v_check := to_regprocedure(format('%I.can_disable(uuid)', v_module.schema_name));
|
|
246
|
+
if v_check is not null then
|
|
247
|
+
execute format('select %I.can_disable($1)', v_module.schema_name)
|
|
248
|
+
into v_reason using p_company_id;
|
|
249
|
+
if v_reason is not null then
|
|
250
|
+
raise exception 'module_holds_data: % cannot be disabled on this company — %', p_code, v_reason
|
|
251
|
+
using errcode = '55006';
|
|
252
|
+
end if;
|
|
253
|
+
end if;
|
|
254
|
+
|
|
255
|
+
delete from company_modules
|
|
256
|
+
where company_id = p_company_id and module_code = p_code;
|
|
257
|
+
end;
|
|
258
|
+
$$;
|
|
259
|
+
|
|
260
|
+
comment on function disable_module(uuid, text) is
|
|
261
|
+
'Disables a module on a company, unless the module says it still holds data — `<schema>.can_disable(company)` returning a sentence refuses, returning null allows. Nothing the module wrote is deleted.';
|
|
262
|
+
|
|
263
|
+
-- ---------------------------------------------------------------------------
|
|
264
|
+
-- The tag an entry carries when a module wrote it
|
|
265
|
+
-- ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
alter table entries
|
|
268
|
+
add column if not exists module_code text references modules(code) on delete restrict,
|
|
269
|
+
add column if not exists module_ref text;
|
|
270
|
+
|
|
271
|
+
comment on column entries.module_code is
|
|
272
|
+
'Which module wrote this entry, or null for an entry of the socle. Set by post_module_entry().';
|
|
273
|
+
comment on column entries.module_ref is
|
|
274
|
+
'What the entry is for, in the module''s own words — `depreciation:2026-03` , `disposal:<uuid>`. Unique per company and module, which is what makes a module''s posting idempotent.';
|
|
275
|
+
|
|
276
|
+
-- The idempotence of every module that posts, in one index. A second call that
|
|
277
|
+
-- would book the same period twice fails on the key rather than on a check the
|
|
278
|
+
-- module remembered to write.
|
|
279
|
+
create unique index entries_module_ref_idx
|
|
280
|
+
on entries (company_id, module_code, module_ref)
|
|
281
|
+
where module_code is not null and module_ref is not null;
|
|
282
|
+
|
|
283
|
+
create index entries_module_idx on entries (company_id, module_code)
|
|
284
|
+
where module_code is not null;
|
|
285
|
+
|
|
286
|
+
-- A tag is a fact about who wrote an entry, so it is written once and never
|
|
287
|
+
-- moved, and it may only name a module the company actually holds. An entry
|
|
288
|
+
-- carrying one is also never born posted: `post_module_entry` builds a draft
|
|
289
|
+
-- and lets `post_entry` do the posting, which is where the period lock, the
|
|
290
|
+
-- balance check and the numbering live.
|
|
291
|
+
create or replace function entries_guard_module()
|
|
292
|
+
returns trigger
|
|
293
|
+
language plpgsql
|
|
294
|
+
as $$
|
|
295
|
+
begin
|
|
296
|
+
if tg_op = 'INSERT' then
|
|
297
|
+
if new.module_code is not null then
|
|
298
|
+
if not module_is_enabled(new.company_id, new.module_code) then
|
|
299
|
+
raise exception 'module_not_enabled: % is not enabled on this company', new.module_code
|
|
300
|
+
using errcode = '55006';
|
|
301
|
+
end if;
|
|
302
|
+
if new.state = 'posted' then
|
|
303
|
+
raise exception 'module_entry_not_posted_by_hand: a module entry is built as a draft and posted by post_entry()'
|
|
304
|
+
using errcode = '55006';
|
|
305
|
+
end if;
|
|
306
|
+
end if;
|
|
307
|
+
return new;
|
|
308
|
+
end if;
|
|
309
|
+
|
|
310
|
+
if new.module_code is distinct from old.module_code
|
|
311
|
+
or new.module_ref is distinct from old.module_ref then
|
|
312
|
+
raise exception 'module_tag_immutable: which module wrote an entry, and what for, is written once'
|
|
313
|
+
using errcode = '55006';
|
|
314
|
+
end if;
|
|
315
|
+
|
|
316
|
+
return new;
|
|
317
|
+
end;
|
|
318
|
+
$$;
|
|
319
|
+
|
|
320
|
+
comment on function entries_guard_module() is
|
|
321
|
+
'Keeps the module tag of an entry honest: a module the company holds, never posted on insert, never moved afterwards.';
|
|
322
|
+
|
|
323
|
+
create trigger entries_guard_module
|
|
324
|
+
before insert or update on entries
|
|
325
|
+
for each row execute function entries_guard_module();
|
|
326
|
+
|
|
327
|
+
-- ---------------------------------------------------------------------------
|
|
328
|
+
-- post_module_entry
|
|
329
|
+
--
|
|
330
|
+
-- The one way a module reaches the ledger. It takes the lines as data — the
|
|
331
|
+
-- same shape `opening_balance()` takes a trial balance in, because that is the
|
|
332
|
+
-- shape this schema has already chosen for "here are some ledger lines" — and
|
|
333
|
+
-- it does exactly what `post_document` and `post_payment` do with them: build
|
|
334
|
+
-- a draft, then `post_entry()`. Nothing about sides, rounding, numbering or
|
|
335
|
+
-- period locks moves into a module, which is the whole reason this function
|
|
336
|
+
-- exists rather than a grant on `entries`.
|
|
337
|
+
-- ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
create or replace function post_module_entry(
|
|
340
|
+
p_company_id uuid,
|
|
341
|
+
p_module_code text,
|
|
342
|
+
p_ref text,
|
|
343
|
+
p_date date,
|
|
344
|
+
p_description text,
|
|
345
|
+
p_lines jsonb,
|
|
346
|
+
p_journal_id uuid default null
|
|
347
|
+
)
|
|
348
|
+
returns uuid
|
|
349
|
+
language plpgsql
|
|
350
|
+
as $$
|
|
351
|
+
declare
|
|
352
|
+
v_company companies%rowtype;
|
|
353
|
+
v_journal uuid;
|
|
354
|
+
v_entry entries%rowtype;
|
|
355
|
+
v_line jsonb;
|
|
356
|
+
v_account uuid;
|
|
357
|
+
v_code text;
|
|
358
|
+
v_debit numeric(16, 2);
|
|
359
|
+
v_credit numeric(16, 2);
|
|
360
|
+
v_sequence integer := 0;
|
|
361
|
+
v_debits numeric(16, 2) := 0;
|
|
362
|
+
v_credits numeric(16, 2) := 0;
|
|
363
|
+
begin
|
|
364
|
+
select * into v_company from companies where id = p_company_id;
|
|
365
|
+
if not found then
|
|
366
|
+
raise exception 'unknown_company: %', p_company_id;
|
|
367
|
+
end if;
|
|
368
|
+
|
|
369
|
+
if not module_is_enabled(p_company_id, p_module_code) then
|
|
370
|
+
raise exception 'module_not_enabled: % is not enabled on this company', p_module_code
|
|
371
|
+
using errcode = '55006';
|
|
372
|
+
end if;
|
|
373
|
+
|
|
374
|
+
if p_ref is null or p_ref = '' then
|
|
375
|
+
raise exception 'module_entry_without_ref: an entry written by a module says what it is for, so a second one cannot repeat it';
|
|
376
|
+
end if;
|
|
377
|
+
|
|
378
|
+
if jsonb_typeof(p_lines) <> 'array' or jsonb_array_length(p_lines) = 0 then
|
|
379
|
+
raise exception 'module_entry_empty: post_module_entry takes a non-empty array of lines';
|
|
380
|
+
end if;
|
|
381
|
+
|
|
382
|
+
-- The miscellaneous journal unless the caller names one. A module booking is
|
|
383
|
+
-- not a movement of money and not a document, so it has no business on a
|
|
384
|
+
-- bank or a sales journal — the same reasoning that put the exchange
|
|
385
|
+
-- difference and the cash-basis transfer there.
|
|
386
|
+
v_journal := coalesce(p_journal_id, v_company.miscellaneous_journal_id);
|
|
387
|
+
if v_journal is null then
|
|
388
|
+
raise exception 'no_miscellaneous_journal: this company has no journal for an entry that is neither a document nor a payment';
|
|
389
|
+
end if;
|
|
390
|
+
|
|
391
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date,
|
|
392
|
+
description, state, module_code, module_ref)
|
|
393
|
+
values (p_company_id, v_journal, fiscal_year_at(p_company_id, p_date), p_date,
|
|
394
|
+
p_description, 'draft', p_module_code, p_ref)
|
|
395
|
+
returning * into v_entry;
|
|
396
|
+
|
|
397
|
+
for v_line in select * from jsonb_array_elements(p_lines)
|
|
398
|
+
loop
|
|
399
|
+
v_sequence := v_sequence + 10;
|
|
400
|
+
v_code := v_line ->> 'account_code';
|
|
401
|
+
v_account := (v_line ->> 'account_id')::uuid;
|
|
402
|
+
if v_account is null then
|
|
403
|
+
if v_code is null then
|
|
404
|
+
raise exception 'module_line_without_account: line % names neither account_id nor account_code', v_sequence / 10;
|
|
405
|
+
end if;
|
|
406
|
+
v_account := account_id_by_code(p_company_id, v_code);
|
|
407
|
+
if v_account is null then
|
|
408
|
+
raise exception 'unknown_account: % is not an account of this company', v_code;
|
|
409
|
+
end if;
|
|
410
|
+
end if;
|
|
411
|
+
|
|
412
|
+
v_debit := round(coalesce((v_line ->> 'debit')::numeric, 0), 2);
|
|
413
|
+
v_credit := round(coalesce((v_line ->> 'credit')::numeric, 0), 2);
|
|
414
|
+
if v_debit < 0 or v_credit < 0 then
|
|
415
|
+
raise exception 'module_line_negative: a line chooses a side, never a sign';
|
|
416
|
+
end if;
|
|
417
|
+
if v_debit <> 0 and v_credit <> 0 then
|
|
418
|
+
raise exception 'module_line_two_sides: a line carries a debit or a credit, not both';
|
|
419
|
+
end if;
|
|
420
|
+
if v_debit = 0 and v_credit = 0 then
|
|
421
|
+
raise exception 'module_line_no_amount: a line with neither a debit nor a credit says nothing';
|
|
422
|
+
end if;
|
|
423
|
+
|
|
424
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
425
|
+
debit, credit, contact_id, date_maturity)
|
|
426
|
+
values (v_entry.id, p_company_id, v_account, v_sequence,
|
|
427
|
+
coalesce(v_line ->> 'label', p_description),
|
|
428
|
+
v_debit, v_credit,
|
|
429
|
+
(v_line ->> 'contact_id')::uuid,
|
|
430
|
+
(v_line ->> 'date_maturity')::date);
|
|
431
|
+
|
|
432
|
+
v_debits := v_debits + v_debit;
|
|
433
|
+
v_credits := v_credits + v_credit;
|
|
434
|
+
end loop;
|
|
435
|
+
|
|
436
|
+
if v_debits <> v_credits then
|
|
437
|
+
raise exception 'entry_unbalanced: the lines of % have debit % and credit %', p_ref, v_debits, v_credits;
|
|
438
|
+
end if;
|
|
439
|
+
|
|
440
|
+
v_entry := post_entry(v_entry.id);
|
|
441
|
+
return v_entry.id;
|
|
442
|
+
end;
|
|
443
|
+
$$;
|
|
444
|
+
|
|
445
|
+
comment on function post_module_entry(uuid, text, text, date, text, jsonb, uuid) is
|
|
446
|
+
'The only way a module reaches the ledger: it hands over lines as data and this builds the draft and calls post_entry(). The tag (module_code, ref) is unique per company, so posting the same thing twice is refused by the database.';
|
|
447
|
+
|
|
448
|
+
create or replace function module_entry_id(p_company_id uuid, p_module_code text, p_ref text)
|
|
449
|
+
returns uuid
|
|
450
|
+
language sql
|
|
451
|
+
stable
|
|
452
|
+
as $$
|
|
453
|
+
select e.id
|
|
454
|
+
from entries e
|
|
455
|
+
where e.company_id = p_company_id
|
|
456
|
+
and e.module_code = p_module_code
|
|
457
|
+
and e.module_ref = p_ref
|
|
458
|
+
and e.state = 'posted'
|
|
459
|
+
limit 1;
|
|
460
|
+
$$;
|
|
461
|
+
|
|
462
|
+
comment on function module_entry_id(uuid, text, text) is
|
|
463
|
+
'The entry a module already posted under a reference, or null. What a module reads before deciding it has work to do.';
|
|
464
|
+
|
|
465
|
+
-- ---------------------------------------------------------------------------
|
|
466
|
+
-- Row level security
|
|
467
|
+
--
|
|
468
|
+
-- `modules` is reference data about the installation, like `country_defaults`:
|
|
469
|
+
-- readable by anyone signed in, written by a migration. `company_modules` is
|
|
470
|
+
-- readable by the members of its company and written by the two functions
|
|
471
|
+
-- above, which is why it has no write policy at all.
|
|
472
|
+
-- ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
alter table modules enable row level security;
|
|
475
|
+
alter table company_modules enable row level security;
|
|
476
|
+
|
|
477
|
+
create policy modules_select on modules
|
|
478
|
+
for select using (auth.uid() is not null);
|
|
479
|
+
|
|
480
|
+
create policy company_modules_select on company_modules
|
|
481
|
+
for select using (is_company_member(company_id));
|
|
482
|
+
|
|
483
|
+
comment on policy company_modules_select on company_modules is
|
|
484
|
+
'Members read. Nobody writes: enable_module() and disable_module() are the way in, and they check the owner themselves.';
|
|
485
|
+
|
|
486
|
+
-- The helper a module's policies call, for the same reason the eight policy
|
|
487
|
+
-- helpers of 20260911210131 keep theirs: without it an anonymous select on a
|
|
488
|
+
-- module table raises instead of returning nothing.
|
|
489
|
+
grant execute on function module_enabled(uuid, text) to anon;
|
|
490
|
+
|
|
491
|
+
-- A migration that adds a function ends with this line, from PUBLIC and never
|
|
492
|
+
-- from `anon`.
|
|
493
|
+
revoke execute on all functions in schema public from public;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- Ekwo OS — where the disposal of a fixed asset lands, per country.
|
|
2
|
+
--
|
|
3
|
+
-- Four nullable columns on `country_defaults`, for the `assets` module to
|
|
4
|
+
-- read. They are in the socle and not in the module's own schema on purpose: a
|
|
5
|
+
-- role is the answer to "which account of this chart plays this part", and
|
|
6
|
+
-- `country_defaults` is the one place a pack answers it. A second roles table
|
|
7
|
+
-- per module would be a second source of truth for the same question — and
|
|
8
|
+
-- `ekwo pack check`, which already refuses a pack whose role codes are not in
|
|
9
|
+
-- *every* chart it ships, would not be checking the second one.
|
|
10
|
+
--
|
|
11
|
+
-- **There are four and not two, because two countries derecognise an asset
|
|
12
|
+
-- differently and neither is a variant of the other.**
|
|
13
|
+
--
|
|
14
|
+
-- `net_result` the asset and its accumulated depreciation are cleared, the
|
|
15
|
+
-- proceeds are booked, and the difference lands on one account
|
|
16
|
+
-- — a gain or a loss. Belgium: 763 and 663, which is how the
|
|
17
|
+
-- statutory income statement presents a *plus-value* or a
|
|
18
|
+
-- *moins-value de réalisation*.
|
|
19
|
+
-- `gross` the net book value is booked as a charge and the proceeds as
|
|
20
|
+
-- an income, both in full, and the result is the difference
|
|
21
|
+
-- between two lines the income statement prints separately.
|
|
22
|
+
-- France: 675 *valeurs comptables des éléments d'actif cédés*
|
|
23
|
+
-- and 775 *produits des cessions d'éléments d'actif*.
|
|
24
|
+
--
|
|
25
|
+
-- Which of the two a country follows is not a role, it is a mechanism, and it
|
|
26
|
+
-- lives with the rest of the module's country model in
|
|
27
|
+
-- `assets.country_rules` — the same division `closing_style` made between the
|
|
28
|
+
-- style and the accounts that style needs.
|
|
29
|
+
--
|
|
30
|
+
-- **None of the four carries a default.** A pack that says nothing about
|
|
31
|
+
-- disposals leaves them null and `assets.dispose_asset` refuses by name,
|
|
32
|
+
-- rather than booking a gain on the account another country happens to use.
|
|
33
|
+
|
|
34
|
+
alter table country_defaults
|
|
35
|
+
add column if not exists asset_disposal_gain_code text,
|
|
36
|
+
add column if not exists asset_disposal_loss_code text,
|
|
37
|
+
add column if not exists asset_disposal_proceeds_code text,
|
|
38
|
+
add column if not exists asset_disposal_value_code text;
|
|
39
|
+
|
|
40
|
+
comment on column country_defaults.asset_disposal_gain_code is
|
|
41
|
+
'Net-result disposal: the account a gain on the disposal of a fixed asset lands on (Belgium 763). Null under the gross style, and null until a pack names one.';
|
|
42
|
+
comment on column country_defaults.asset_disposal_loss_code is
|
|
43
|
+
'Net-result disposal: the account a loss lands on (Belgium 663). Left empty where the chart keeps one account for both signs, and then the gain account answers for both.';
|
|
44
|
+
comment on column country_defaults.asset_disposal_proceeds_code is
|
|
45
|
+
'Gross disposal: the income account the proceeds of a sale are booked on in full (France 775). Null under the net-result style.';
|
|
46
|
+
comment on column country_defaults.asset_disposal_value_code is
|
|
47
|
+
'Gross disposal: the charge account the net book value of the asset sold is booked on in full (France 675). Null under the net-result style.';
|