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,252 @@
|
|
|
1
|
+
-- Ekwo OS — a posted entry is immutable, in an open period too.
|
|
2
|
+
--
|
|
3
|
+
-- The schema has said so from the first day, in comments, in the description
|
|
4
|
+
-- of the audit trail — "a posted entry is immutable and is corrected by a
|
|
5
|
+
-- reversal, so what is recorded is the act of posting and never the lines" —
|
|
6
|
+
-- and in every design decision that leans on it. What enforced it was
|
|
7
|
+
-- `entries_guard_period`, and that guards a *locked* period. In an open one,
|
|
8
|
+
-- established under row level security as a member holding `entries.write`:
|
|
9
|
+
--
|
|
10
|
+
-- * the lines of a posted entry could be deleted, all of them;
|
|
11
|
+
-- * the entry itself could be deleted;
|
|
12
|
+
-- * its state could be set back to `draft`, after which it is an ordinary
|
|
13
|
+
-- draft and anything goes;
|
|
14
|
+
-- * an amount on its own was refused — by `entries_posted_is_balanced`, a
|
|
15
|
+
-- check that the entry still balances, which two amounts changed together
|
|
16
|
+
-- satisfy.
|
|
17
|
+
--
|
|
18
|
+
-- The audit trail records the posting and deliberately not the lines, on the
|
|
19
|
+
-- ground that they cannot change. So a change to them was not only possible,
|
|
20
|
+
-- it left no trace.
|
|
21
|
+
--
|
|
22
|
+
-- Same shape as `20260918161204` for the document, for the same reason and
|
|
23
|
+
-- with the same absence of an exemption. Once an entry has left `draft`:
|
|
24
|
+
--
|
|
25
|
+
-- * it is not deleted and its state does not change. A posted entry is
|
|
26
|
+
-- undone by a reversal — a second entry naming it in `reversed_entry_id` —
|
|
27
|
+
-- which is what `reopen_fiscal_year()` already does;
|
|
28
|
+
-- * no column of it moves but `updated_at`. `total_debit` and `total_credit`
|
|
29
|
+
-- are derived from lines that no longer move;
|
|
30
|
+
-- * its lines take no insert and no delete, and keep every column but the
|
|
31
|
+
-- two the matching writes — `matching_number` and `matched_amount` — which
|
|
32
|
+
-- is the exception `entry_lines_guard_period` has made for a locked period
|
|
33
|
+
-- since the first migration: matching is not a change to the accounts.
|
|
34
|
+
--
|
|
35
|
+
-- Closed lists again: a column added later is frozen the day it is added.
|
|
36
|
+
--
|
|
37
|
+
-- **Every path of the schema that writes an entry was read before this was
|
|
38
|
+
-- written**, because a guard nobody is exempt from has to be one nobody needs
|
|
39
|
+
-- an exemption from. They all do the same thing: insert a `draft`, insert its
|
|
40
|
+
-- lines, call `post_entry()`.
|
|
41
|
+
--
|
|
42
|
+
-- * `post_document()`, `post_payment()`, `post_module_entry()` — which is how
|
|
43
|
+
-- every module reaches the ledger —, `opening_balance()`,
|
|
44
|
+
-- `settle_cash_basis_tax()`, `settle_filing()`: a draft, its lines,
|
|
45
|
+
-- `post_entry()`. Nothing is touched afterwards.
|
|
46
|
+
-- * `close_fiscal_year()`: the same, twice; its one `delete from entries` is
|
|
47
|
+
-- of the closing entry it has just created as a draft and found nothing to
|
|
48
|
+
-- put in.
|
|
49
|
+
-- * `reconcile()` books an exchange difference as a new entry, and
|
|
50
|
+
-- `unreconcile()` undoes it with a **mirror** naming it in
|
|
51
|
+
-- `reversed_entry_id` — it never deletes or edits the first.
|
|
52
|
+
-- `reopen_fiscal_year()` does the same for the entries of a close.
|
|
53
|
+
-- * `reconciliations_refresh_lines()` writes `matched_amount` and
|
|
54
|
+
-- `matching_number` on posted lines: the closed list below.
|
|
55
|
+
-- * `entries_refresh_totals()` rewrote `total_debit` and `total_credit` on
|
|
56
|
+
-- every write of a line, whatever the state. The lines of a posted entry
|
|
57
|
+
-- no longer move, so it now acts on drafts.
|
|
58
|
+
--
|
|
59
|
+
-- So the reversal is already how this schema undoes an entry, everywhere it
|
|
60
|
+
-- does; what was missing is that nothing made it the *only* way.
|
|
61
|
+
--
|
|
62
|
+
-- **Nobody is born posted**, as for a document and for the same reason: an
|
|
63
|
+
-- entry inserted with a state that is not `draft`, or a line inserted under
|
|
64
|
+
-- one, is `entry_born_posted` for everybody, the installer included. Books
|
|
65
|
+
-- kept elsewhere arrive through `import_company()`, which switches the
|
|
66
|
+
-- triggers of the tables it fills off by name for the time of the load and
|
|
67
|
+
-- needs no exemption here. What it loads is still judged by what no switch
|
|
68
|
+
-- turns off — `entries_posted_is_balanced`, `entries_posted_has_number` — and
|
|
69
|
+
-- is frozen afterwards like everything else. The period guards are triggers
|
|
70
|
+
-- and are off during a load with the rest: an archive brings closed years with
|
|
71
|
+
-- it, and whether a loaded entry may sit in one is the import's question.
|
|
72
|
+
--
|
|
73
|
+
-- **The guards are `security definer`**, for the reason found on the document:
|
|
74
|
+
-- asked under the caller's row level security, "is the company still there?"
|
|
75
|
+
-- is answered *no row* by whoever cannot read `companies`, and that answer let
|
|
76
|
+
-- a delete through. Unlike the guard of a document line they are not named
|
|
77
|
+
-- to fire first: the older guards of these two tables — who may post, what a
|
|
78
|
+
-- module tag may become, which period is locked — refuse more precisely what
|
|
79
|
+
-- they refuse, and keep answering first where they do.
|
|
80
|
+
--
|
|
81
|
+
-- `draft` to `posted` is `post_entry()`'s one statement on a row that is still
|
|
82
|
+
-- a draft, and passes untouched. The cascade of a company being deleted passes
|
|
83
|
+
-- for the reason it does on a document: the company's row is already gone.
|
|
84
|
+
-- The period guards stay exactly as they are and keep their own job — a draft
|
|
85
|
+
-- dated in a locked period is theirs to refuse, not this file's.
|
|
86
|
+
|
|
87
|
+
create or replace function entries_guard_posted()
|
|
88
|
+
returns trigger
|
|
89
|
+
language plpgsql
|
|
90
|
+
security definer
|
|
91
|
+
set search_path = public, pg_temp
|
|
92
|
+
as $$
|
|
93
|
+
declare
|
|
94
|
+
v_moved text[];
|
|
95
|
+
begin
|
|
96
|
+
if tg_op = 'INSERT' then
|
|
97
|
+
if new.state <> 'draft' then
|
|
98
|
+
raise exception 'entry_born_posted: an entry is written as a draft and posted by post_entry(). One that is already % arrives with a company being loaded, by import_company(), and in no other way.',
|
|
99
|
+
new.state
|
|
100
|
+
using errcode = '55006';
|
|
101
|
+
end if;
|
|
102
|
+
return new;
|
|
103
|
+
end if;
|
|
104
|
+
|
|
105
|
+
if old.state = 'draft' then
|
|
106
|
+
return coalesce(new, old);
|
|
107
|
+
end if;
|
|
108
|
+
|
|
109
|
+
if tg_op = 'DELETE' then
|
|
110
|
+
if not exists (select 1 from companies c where c.id = old.company_id) then
|
|
111
|
+
return old;
|
|
112
|
+
end if;
|
|
113
|
+
raise exception 'entry_posted: entry % is % and cannot be deleted. A posted entry is undone by a reversal that names it.',
|
|
114
|
+
coalesce(old.number, old.id::text), old.state
|
|
115
|
+
using errcode = '55006';
|
|
116
|
+
end if;
|
|
117
|
+
|
|
118
|
+
if new.state is distinct from old.state then
|
|
119
|
+
raise exception 'entry_posted: entry % is % and stays so. A posted entry is undone by a reversal that names it, never by changing its state to %.',
|
|
120
|
+
coalesce(old.number, old.id::text), old.state, new.state
|
|
121
|
+
using errcode = '55006';
|
|
122
|
+
end if;
|
|
123
|
+
|
|
124
|
+
select array_agg(n.key order by n.key)
|
|
125
|
+
into v_moved
|
|
126
|
+
from jsonb_each(to_jsonb(new)) n
|
|
127
|
+
join jsonb_each(to_jsonb(old)) o on o.key = n.key
|
|
128
|
+
where n.value is distinct from o.value
|
|
129
|
+
-- `is_balanced` is generated, and not yet computed in a `before` trigger.
|
|
130
|
+
and n.key not in ('updated_at', 'is_balanced');
|
|
131
|
+
|
|
132
|
+
if v_moved is not null then
|
|
133
|
+
raise exception 'entry_posted: entry % was posted and keeps its % (%). A posted entry is undone by a reversal that names it.',
|
|
134
|
+
coalesce(old.number, old.id::text),
|
|
135
|
+
case when array_length(v_moved, 1) = 1 then 'value of' else 'values of' end,
|
|
136
|
+
array_to_string(v_moved, ', ')
|
|
137
|
+
using errcode = '55006';
|
|
138
|
+
end if;
|
|
139
|
+
return new;
|
|
140
|
+
end;
|
|
141
|
+
$$;
|
|
142
|
+
|
|
143
|
+
comment on function entries_guard_posted() is
|
|
144
|
+
'Once an entry has left draft: it is not deleted, its state does not change and no column moves. Refuses entry_posted, by name, to everybody, and entry_born_posted to whoever inserts one that is already posted: that arrives only with a company loaded by import_company(). The period guards keep their own job.';
|
|
145
|
+
|
|
146
|
+
create trigger entries_guard_posted
|
|
147
|
+
before insert or update or delete on entries
|
|
148
|
+
for each row execute function entries_guard_posted();
|
|
149
|
+
|
|
150
|
+
revoke execute on function entries_guard_posted() from public, anon, authenticated, service_role;
|
|
151
|
+
|
|
152
|
+
create or replace function entry_lines_guard_posted()
|
|
153
|
+
returns trigger
|
|
154
|
+
language plpgsql
|
|
155
|
+
security definer
|
|
156
|
+
set search_path = public, pg_temp
|
|
157
|
+
as $$
|
|
158
|
+
declare
|
|
159
|
+
-- What the matching writes, and nothing else. `balance` is generated from
|
|
160
|
+
-- two columns that are frozen, and is not yet computed when a `before`
|
|
161
|
+
-- trigger reads the new row.
|
|
162
|
+
c_still_moves constant text[] := array['matching_number', 'matched_amount', 'balance', 'updated_at'];
|
|
163
|
+
v_state entry_state;
|
|
164
|
+
v_number text;
|
|
165
|
+
v_moved text[];
|
|
166
|
+
begin
|
|
167
|
+
-- The entry the line is leaving, where it is leaving one: a line does not
|
|
168
|
+
-- walk out of a posted entry into a draft.
|
|
169
|
+
select e.state, e.number into v_state, v_number
|
|
170
|
+
from entries e where e.id = coalesce(old.entry_id, new.entry_id);
|
|
171
|
+
if found and v_state = 'draft' and tg_op = 'UPDATE' and new.entry_id is distinct from old.entry_id then
|
|
172
|
+
select e.state, e.number into v_state, v_number from entries e where e.id = new.entry_id;
|
|
173
|
+
end if;
|
|
174
|
+
|
|
175
|
+
if not found or v_state = 'draft' then
|
|
176
|
+
return coalesce(new, old);
|
|
177
|
+
end if;
|
|
178
|
+
|
|
179
|
+
if tg_op = 'DELETE' then
|
|
180
|
+
if not exists (select 1 from companies c where c.id = old.company_id) then
|
|
181
|
+
return old;
|
|
182
|
+
end if;
|
|
183
|
+
raise exception 'entry_posted: line % of entry % was posted and cannot be deleted. A posted entry is undone by a reversal that names it.',
|
|
184
|
+
old.sequence, coalesce(v_number, old.entry_id::text)
|
|
185
|
+
using errcode = '55006';
|
|
186
|
+
end if;
|
|
187
|
+
|
|
188
|
+
if tg_op = 'INSERT' then
|
|
189
|
+
raise exception 'entry_posted: entry % was posted with the lines it has, and takes no other. A posted entry is undone by a reversal that names it.',
|
|
190
|
+
coalesce(v_number, new.entry_id::text)
|
|
191
|
+
using errcode = '55006';
|
|
192
|
+
end if;
|
|
193
|
+
|
|
194
|
+
select array_agg(n.key order by n.key)
|
|
195
|
+
into v_moved
|
|
196
|
+
from jsonb_each(to_jsonb(new)) n
|
|
197
|
+
join jsonb_each(to_jsonb(old)) o on o.key = n.key
|
|
198
|
+
where n.value is distinct from o.value
|
|
199
|
+
and n.key <> all (c_still_moves);
|
|
200
|
+
|
|
201
|
+
if v_moved is not null then
|
|
202
|
+
raise exception 'entry_posted: line % of entry % was posted and keeps its % (%). A posted entry is undone by a reversal that names it.',
|
|
203
|
+
old.sequence, coalesce(v_number, old.entry_id::text),
|
|
204
|
+
case when array_length(v_moved, 1) = 1 then 'value of' else 'values of' end,
|
|
205
|
+
array_to_string(v_moved, ', ')
|
|
206
|
+
using errcode = '55006';
|
|
207
|
+
end if;
|
|
208
|
+
return new;
|
|
209
|
+
end;
|
|
210
|
+
$$;
|
|
211
|
+
|
|
212
|
+
comment on function entry_lines_guard_posted() is
|
|
213
|
+
'Once an entry has left draft, its lines are the lines that were posted: no insert, no delete, and no column changed but the two the matching writes, matching_number and matched_amount. Refuses entry_posted, by name, to everybody.';
|
|
214
|
+
|
|
215
|
+
create trigger entry_lines_guard_posted
|
|
216
|
+
before insert or update or delete on entry_lines
|
|
217
|
+
for each row execute function entry_lines_guard_posted();
|
|
218
|
+
|
|
219
|
+
revoke execute on function entry_lines_guard_posted() from public, anon, authenticated, service_role;
|
|
220
|
+
|
|
221
|
+
-- ---------------------------------------------------------------------------
|
|
222
|
+
-- The totals of a posted entry are not recomputed
|
|
223
|
+
--
|
|
224
|
+
-- `20260911120400` as it was, acting on drafts only. The lines of a posted
|
|
225
|
+
-- entry do not move, so there is nothing to recompute; and a trigger that
|
|
226
|
+
-- restates the totals of a posted entry from its lines is the mechanism by
|
|
227
|
+
-- which a changed line used to become a changed entry.
|
|
228
|
+
-- ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
create or replace function entries_refresh_totals()
|
|
231
|
+
returns trigger
|
|
232
|
+
language plpgsql
|
|
233
|
+
as $$
|
|
234
|
+
declare
|
|
235
|
+
v_entry_id uuid := coalesce(new.entry_id, old.entry_id);
|
|
236
|
+
begin
|
|
237
|
+
update entries e
|
|
238
|
+
set total_debit = coalesce(s.debit, 0),
|
|
239
|
+
total_credit = coalesce(s.credit, 0)
|
|
240
|
+
from (
|
|
241
|
+
select sum(l.debit) as debit, sum(l.credit) as credit
|
|
242
|
+
from entry_lines l
|
|
243
|
+
where l.entry_id = v_entry_id
|
|
244
|
+
) s
|
|
245
|
+
where e.id = v_entry_id
|
|
246
|
+
and e.state = 'draft';
|
|
247
|
+
return null;
|
|
248
|
+
end;
|
|
249
|
+
$$;
|
|
250
|
+
|
|
251
|
+
comment on function entries_refresh_totals() is
|
|
252
|
+
'Keeps the totals of a draft entry in step with its lines. A posted entry keeps the totals it was posted, or loaded, with.';
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
-- Ekwo OS — an entry is posted by post_entry(), a document by post_document().
|
|
2
|
+
--
|
|
3
|
+
-- The two guards published this morning freeze what is posted. They looked at
|
|
4
|
+
-- the way *out* of `posted` and let the way *in* alone, on the ground that the
|
|
5
|
+
-- transition is one statement of `post_entry()`. It is — and it is also one
|
|
6
|
+
-- statement of anybody's. Established as the `accountant` preset, which holds
|
|
7
|
+
-- `entries.post`, under row level security, on a balanced draft:
|
|
8
|
+
--
|
|
9
|
+
-- update entries set state = 'posted', number = 'HAND/1' where id = …
|
|
10
|
+
--
|
|
11
|
+
-- went through. What stood in the way was two check constraints (a posted
|
|
12
|
+
-- entry has a number and balances), the capability, and the lock of the
|
|
13
|
+
-- period as `entries_guard_period` reads it. What did not: the number was
|
|
14
|
+
-- chosen by hand, in a country whose law forbids a hole in the sequence and
|
|
15
|
+
-- where `post_entry()` refuses exactly that by name (`numbering_gapless`);
|
|
16
|
+
-- `posted_at` and `fiscal_year_id` stayed null; an entry with no line at all
|
|
17
|
+
-- balances at zero and was accepted; the period was not asked the stricter
|
|
18
|
+
-- question `post_entry()` asks. And the guard of this morning then froze the
|
|
19
|
+
-- result for good.
|
|
20
|
+
--
|
|
21
|
+
-- The same on a document: `update documents set state = 'posted', entry_id = …`
|
|
22
|
+
-- pointing at an entry that is legitimately posted and is *not* the one the
|
|
23
|
+
-- document produced — an entry keyed by hand, say — went through, because
|
|
24
|
+
-- `document_posted_without_entry` only asked that the entry be posted.
|
|
25
|
+
--
|
|
26
|
+
-- **Judged on facts, not on a flag.** The obvious fix is a transaction-local
|
|
27
|
+
-- setting that `post_entry()` raises and the guard reads. It does not hold: a
|
|
28
|
+
-- custom setting is writable by any session that can run `set_config`, so the
|
|
29
|
+
-- flag would say "post_entry is running" for whoever said so first — and
|
|
30
|
+
-- `post_entry()` runs as its caller, so there is no privilege behind which to
|
|
31
|
+
-- hide it. What `post_entry()` *produces*, on the other hand, is all on the
|
|
32
|
+
-- row and in the counter, and can be checked there whoever wrote it:
|
|
33
|
+
--
|
|
34
|
+
-- * `posted_at` is set, and `fiscal_year_id`, where there is one, is the year
|
|
35
|
+
-- the date falls in;
|
|
36
|
+
-- * the entry has at least one line (`entry_empty`);
|
|
37
|
+
-- * the period is open, asked the way `post_entry()` asks it;
|
|
38
|
+
-- * **the number was drawn, not chosen**, wherever the rule applies: in a
|
|
39
|
+
-- country that numbers without a hole, for a caller who does not hold
|
|
40
|
+
-- `entries.import`, the number has to be the last one the counter of its
|
|
41
|
+
-- journal delivered. `next_entry_number()` advances that counter in the
|
|
42
|
+
-- same statement and holds its row until the transaction ends, and a number
|
|
43
|
+
-- already used is refused by `entries_company_number_idx` — so "equal to
|
|
44
|
+
-- the last delivered, and unique" *is* "just drawn". Elsewhere a number
|
|
45
|
+
-- chosen by hand is what `post_entry()` allows too, and the guard then
|
|
46
|
+
-- brings the counter up to it, as the function does.
|
|
47
|
+
--
|
|
48
|
+
-- A row that satisfies all of that is, fact for fact, the row `post_entry()`
|
|
49
|
+
-- writes, and is let through whoever wrote it: a rule about facts cannot refuse
|
|
50
|
+
-- the facts, and nothing is lost by it. `post_entry()` is unchanged, and so are
|
|
51
|
+
-- the seven functions that call it; `import_company()` switches the guard off
|
|
52
|
+
-- for a load, as before.
|
|
53
|
+
--
|
|
54
|
+
-- The cost is said plainly: the conditions of `post_entry()` are now read in
|
|
55
|
+
-- two places, the function and the guard. `tests/posted_by_hand.test.ts` walks
|
|
56
|
+
-- every refusal of the function beside the same attempt by hand, so that a
|
|
57
|
+
-- condition added to one and not to the other fails a test.
|
|
58
|
+
--
|
|
59
|
+
-- For a document the facts are on two rows: the entry names this document
|
|
60
|
+
-- (`entries.document_id`, written by `post_document()` when it builds it, and
|
|
61
|
+
-- frozen with the entry), the document is booked on its entry's day, and it
|
|
62
|
+
-- carries the number it had, or its entry's. `document_posted_by_hand`
|
|
63
|
+
-- otherwise.
|
|
64
|
+
|
|
65
|
+
-- ---------------------------------------------------------------------------
|
|
66
|
+
-- 1. The guard of an entry, asking at the transition what post_entry() leaves
|
|
67
|
+
--
|
|
68
|
+
-- `20260918161538` as it was, with the transition judged. It is written in the
|
|
69
|
+
-- trigger and not in a function beside it: a function nobody may call is a
|
|
70
|
+
-- function the grants have no honest line for.
|
|
71
|
+
-- ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
create or replace function entries_guard_posted()
|
|
74
|
+
returns trigger
|
|
75
|
+
language plpgsql
|
|
76
|
+
security definer
|
|
77
|
+
set search_path = public, pg_temp
|
|
78
|
+
as $$
|
|
79
|
+
declare
|
|
80
|
+
v_moved text[];
|
|
81
|
+
v_format text;
|
|
82
|
+
v_gapless boolean;
|
|
83
|
+
v_code text;
|
|
84
|
+
v_period smallint;
|
|
85
|
+
v_last integer;
|
|
86
|
+
begin
|
|
87
|
+
if tg_op = 'INSERT' then
|
|
88
|
+
if new.state <> 'draft' then
|
|
89
|
+
raise exception 'entry_born_posted: an entry is written as a draft and posted by post_entry(). One that is already % arrives with a company being loaded, by import_company(), and in no other way.',
|
|
90
|
+
new.state
|
|
91
|
+
using errcode = '55006';
|
|
92
|
+
end if;
|
|
93
|
+
return new;
|
|
94
|
+
end if;
|
|
95
|
+
|
|
96
|
+
if old.state = 'draft' then
|
|
97
|
+
if tg_op = 'UPDATE' and new.state = 'posted' then
|
|
98
|
+
if new.posted_at is null then
|
|
99
|
+
raise exception 'entry_posted_by_hand: entry % would be posted with no instant it was posted at. An entry is posted by post_entry() and by nothing else.',
|
|
100
|
+
coalesce(new.number, new.id::text)
|
|
101
|
+
using errcode = '55006';
|
|
102
|
+
end if;
|
|
103
|
+
-- Null is what post_entry() itself writes where it finds no year — a company
|
|
104
|
+
-- that has not opened one, or a caller who cannot read `fiscal_years`, which
|
|
105
|
+
-- is a machine key today. A year that is *named* has to be the right one.
|
|
106
|
+
if new.fiscal_year_id is not null
|
|
107
|
+
and new.fiscal_year_id is distinct from fiscal_year_at(new.company_id, new.entry_date) then
|
|
108
|
+
raise exception 'entry_posted_by_hand: entry % would be posted outside the financial year its date falls in. An entry is posted by post_entry() and by nothing else.',
|
|
109
|
+
coalesce(new.number, new.id::text)
|
|
110
|
+
using errcode = '55006';
|
|
111
|
+
end if;
|
|
112
|
+
|
|
113
|
+
if not exists (select 1 from entry_lines l where l.entry_id = new.id) then
|
|
114
|
+
raise exception 'entry_empty: entry % has no lines', new.id;
|
|
115
|
+
end if;
|
|
116
|
+
|
|
117
|
+
perform assert_period_open(new.company_id, new.entry_date, true);
|
|
118
|
+
|
|
119
|
+
select r.number_format, r.numbering_gapless into v_format, v_gapless
|
|
120
|
+
from numbering_rules(new.company_id) r;
|
|
121
|
+
select j.code into v_code from journals j where j.id = new.journal_id;
|
|
122
|
+
v_period := case when v_format ~ '\{(YYYY|YY)\}'
|
|
123
|
+
then extract(year from new.entry_date)::smallint
|
|
124
|
+
else 0::smallint end;
|
|
125
|
+
select s.last_number into v_last
|
|
126
|
+
from journal_sequences s
|
|
127
|
+
where s.journal_id = new.journal_id and s.year = v_period;
|
|
128
|
+
|
|
129
|
+
if v_last is not null and new.number = format_number(v_format, v_code, new.entry_date, v_last) then
|
|
130
|
+
return new; -- drawn from the counter, a moment ago, by this transaction
|
|
131
|
+
end if;
|
|
132
|
+
|
|
133
|
+
-- A number chosen by hand: on the draft beforehand, or in this statement.
|
|
134
|
+
if v_gapless and not (auth.uid() is not null and has_capability(new.company_id, 'entries.import')) then
|
|
135
|
+
raise exception 'entry_posted_by_hand: % is not the number the counter of this journal delivered, and this country forbids a hole in the sequence. An entry is posted by post_entry(), which draws it; books that already have numbers are brought in by whoever holds entries.import.',
|
|
136
|
+
new.number
|
|
137
|
+
using errcode = '55006';
|
|
138
|
+
end if;
|
|
139
|
+
perform catch_up_journal_sequence(new.journal_id, new.entry_date, new.number);
|
|
140
|
+
end if;
|
|
141
|
+
return coalesce(new, old);
|
|
142
|
+
end if;
|
|
143
|
+
|
|
144
|
+
if tg_op = 'DELETE' then
|
|
145
|
+
if not exists (select 1 from companies c where c.id = old.company_id) then
|
|
146
|
+
return old;
|
|
147
|
+
end if;
|
|
148
|
+
raise exception 'entry_posted: entry % is % and cannot be deleted. A posted entry is undone by a reversal that names it.',
|
|
149
|
+
coalesce(old.number, old.id::text), old.state
|
|
150
|
+
using errcode = '55006';
|
|
151
|
+
end if;
|
|
152
|
+
|
|
153
|
+
if new.state is distinct from old.state then
|
|
154
|
+
raise exception 'entry_posted: entry % is % and stays so. A posted entry is undone by a reversal that names it, never by changing its state to %.',
|
|
155
|
+
coalesce(old.number, old.id::text), old.state, new.state
|
|
156
|
+
using errcode = '55006';
|
|
157
|
+
end if;
|
|
158
|
+
|
|
159
|
+
select array_agg(n.key order by n.key)
|
|
160
|
+
into v_moved
|
|
161
|
+
from jsonb_each(to_jsonb(new)) n
|
|
162
|
+
join jsonb_each(to_jsonb(old)) o on o.key = n.key
|
|
163
|
+
where n.value is distinct from o.value
|
|
164
|
+
-- `is_balanced` is generated, and not yet computed in a `before` trigger.
|
|
165
|
+
and n.key not in ('updated_at', 'is_balanced');
|
|
166
|
+
|
|
167
|
+
if v_moved is not null then
|
|
168
|
+
raise exception 'entry_posted: entry % was posted and keeps its % (%). A posted entry is undone by a reversal that names it.',
|
|
169
|
+
coalesce(old.number, old.id::text),
|
|
170
|
+
case when array_length(v_moved, 1) = 1 then 'value of' else 'values of' end,
|
|
171
|
+
array_to_string(v_moved, ', ')
|
|
172
|
+
using errcode = '55006';
|
|
173
|
+
end if;
|
|
174
|
+
return new;
|
|
175
|
+
end;
|
|
176
|
+
$$;
|
|
177
|
+
|
|
178
|
+
-- ---------------------------------------------------------------------------
|
|
179
|
+
-- 2. The guard of a document, asking whose entry it is
|
|
180
|
+
--
|
|
181
|
+
-- `20260918161204` as it was; the transition reads the entry instead of only
|
|
182
|
+
-- finding it.
|
|
183
|
+
-- ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
create or replace function documents_guard_posted()
|
|
186
|
+
returns trigger
|
|
187
|
+
language plpgsql
|
|
188
|
+
security definer
|
|
189
|
+
set search_path = public, pg_temp
|
|
190
|
+
as $$
|
|
191
|
+
declare
|
|
192
|
+
-- What may still move once the state has left `draft`. A closed list: a
|
|
193
|
+
-- column that is not named here is frozen, including one that does not
|
|
194
|
+
-- exist yet.
|
|
195
|
+
c_still_moves constant text[] := array[
|
|
196
|
+
'amount_paid', 'amount_residual', 'payment_state',
|
|
197
|
+
'sent_at', 'peppol_status', 'peppol_message_id',
|
|
198
|
+
'updated_at'
|
|
199
|
+
];
|
|
200
|
+
v_moved text[];
|
|
201
|
+
v_entry entries%rowtype;
|
|
202
|
+
begin
|
|
203
|
+
if tg_op = 'INSERT' then
|
|
204
|
+
if new.state <> 'draft' then
|
|
205
|
+
raise exception 'document_born_posted: a document is written as a draft and posted by post_document(). One that is already % arrives with a company being loaded, by import_company(), and in no other way.',
|
|
206
|
+
new.state
|
|
207
|
+
using errcode = '55006';
|
|
208
|
+
end if;
|
|
209
|
+
return new;
|
|
210
|
+
end if;
|
|
211
|
+
|
|
212
|
+
if tg_op = 'DELETE' then
|
|
213
|
+
if old.state = 'draft'
|
|
214
|
+
or not exists (select 1 from companies c where c.id = old.company_id) then
|
|
215
|
+
return old;
|
|
216
|
+
end if;
|
|
217
|
+
raise exception 'document_posted: % % was issued and cannot be deleted. It is undone by a credit note that names it.',
|
|
218
|
+
old.doc_type, coalesce(old.number, old.id::text)
|
|
219
|
+
using errcode = '55006';
|
|
220
|
+
end if;
|
|
221
|
+
|
|
222
|
+
if old.state = 'draft' then
|
|
223
|
+
-- The one transition that produces an entry has to have produced one.
|
|
224
|
+
if new.state = 'posted' then
|
|
225
|
+
select e.* into v_entry from entries e where e.id = new.entry_id and e.state = 'posted';
|
|
226
|
+
if not found then
|
|
227
|
+
raise exception 'document_posted_without_entry: % has no posted entry. A document is posted by post_document(), which builds its entry and posts it first.',
|
|
228
|
+
coalesce(new.number, new.id::text)
|
|
229
|
+
using errcode = '55006';
|
|
230
|
+
end if;
|
|
231
|
+
-- What post_document() leaves behind, read on the two rows: the entry
|
|
232
|
+
-- was built for this document and for no other, the document is booked
|
|
233
|
+
-- on the day its entry is, and it is numbered as it was — or, where it
|
|
234
|
+
-- had no number, as its entry.
|
|
235
|
+
if v_entry.document_id is distinct from new.id
|
|
236
|
+
or new.accounting_date is distinct from v_entry.entry_date
|
|
237
|
+
or new.number is distinct from coalesce(old.number, v_entry.number) then
|
|
238
|
+
raise exception 'document_posted_by_hand: % is not what post_document() writes — its entry % was not built for it, or is not booked on its day, or did not give it its number. A document is posted by post_document() and by nothing else.',
|
|
239
|
+
coalesce(new.number, new.id::text), coalesce(v_entry.number, v_entry.id::text)
|
|
240
|
+
using errcode = '55006';
|
|
241
|
+
end if;
|
|
242
|
+
end if;
|
|
243
|
+
return new;
|
|
244
|
+
end if;
|
|
245
|
+
|
|
246
|
+
if new.state is distinct from old.state then
|
|
247
|
+
raise exception 'document_posted: % % is % and stays so. It is undone by a credit note that names it, never by changing its state to %.',
|
|
248
|
+
old.doc_type, coalesce(old.number, old.id::text), old.state, new.state
|
|
249
|
+
using errcode = '55006';
|
|
250
|
+
end if;
|
|
251
|
+
|
|
252
|
+
select array_agg(n.key order by n.key)
|
|
253
|
+
into v_moved
|
|
254
|
+
from jsonb_each(to_jsonb(new)) n
|
|
255
|
+
join jsonb_each(to_jsonb(old)) o on o.key = n.key
|
|
256
|
+
where n.value is distinct from o.value
|
|
257
|
+
and n.key <> all (c_still_moves);
|
|
258
|
+
|
|
259
|
+
if v_moved is not null then
|
|
260
|
+
raise exception 'document_posted: % % was issued and keeps its % (%). What produced an entry does not change after it; credit it and issue another.',
|
|
261
|
+
old.doc_type, coalesce(old.number, old.id::text),
|
|
262
|
+
case when array_length(v_moved, 1) = 1 then 'value of' else 'values of' end,
|
|
263
|
+
array_to_string(v_moved, ', ')
|
|
264
|
+
using errcode = '55006';
|
|
265
|
+
end if;
|
|
266
|
+
|
|
267
|
+
if new.amount_paid is distinct from old.amount_paid
|
|
268
|
+
and new.amount_paid is distinct from document_amount_paid(new.id) then
|
|
269
|
+
raise exception 'document_amount_paid_is_derived: % has been settled by %, which is what its matching says. The figure is never keyed.',
|
|
270
|
+
coalesce(old.number, old.id::text), document_amount_paid(new.id)
|
|
271
|
+
using errcode = '55006';
|
|
272
|
+
end if;
|
|
273
|
+
|
|
274
|
+
if new.payment_state is distinct from old.payment_state
|
|
275
|
+
and new.amount_paid is not distinct from old.amount_paid then
|
|
276
|
+
raise exception 'document_payment_state_is_derived: the settlement of % follows from what was matched against it, and is never keyed.',
|
|
277
|
+
coalesce(old.number, old.id::text)
|
|
278
|
+
using errcode = '55006';
|
|
279
|
+
end if;
|
|
280
|
+
|
|
281
|
+
return new;
|
|
282
|
+
end;
|
|
283
|
+
$$;
|
|
284
|
+
|
|
285
|
+
comment on function documents_guard_posted() is
|
|
286
|
+
'Once a document has left draft: its state does not change, it is not deleted, and no column moves but the closed list of what happens to a document after it is issued — amount_paid (to the figure the matching gives, and no other), payment_state, sent_at, peppol_status, peppol_message_id. Refuses document_posted, by name, to everybody. Becoming posted is held to what post_document() writes — a posted entry built for this document, booked on its day, that gave it its number — document_posted_by_hand otherwise; and nobody inserts a document that is already posted (document_born_posted).';
|
|
287
|
+
|
|
288
|
+
comment on function entries_guard_posted() is
|
|
289
|
+
'Once an entry has left draft: it is not deleted, its state does not change and no column moves (entry_posted); nobody inserts one that is already posted (entry_born_posted). Becoming posted is held to what post_entry() produces, read on the row and in the counter — an instant it was posted at, no financial year but the one its date falls in, at least one line, an open period, and, where the country numbers without a hole and the caller does not hold entries.import, the number the counter of its journal has just delivered: entry_posted_by_hand otherwise.';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- Ekwo OS — the schema this release defines is 0.4.0.
|
|
2
|
+
--
|
|
3
|
+
-- `ekwo_schema_version()` is the one place the number lives, and a migration
|
|
4
|
+
-- is the only thing that moves it. A database then answers for the files it
|
|
5
|
+
-- has actually run rather than for what somebody believed it had run: the
|
|
6
|
+
-- column default and `init_instance()` both call this function, so a fresh
|
|
7
|
+
-- install records the new number without a second edit, and `ekwo migrate`
|
|
8
|
+
-- reads it back onto `instance.schema_version` for a database that was
|
|
9
|
+
-- installed at an older one.
|
|
10
|
+
--
|
|
11
|
+
-- Between 0.3.0 and here the schema gained the whole life of a declaration,
|
|
12
|
+
-- a firm that keeps the books of its clients, a statement a bank sent, and a
|
|
13
|
+
-- posted row that does not move. The packages of this release read all of it,
|
|
14
|
+
-- and a 0.3.0 database has none of it: `tax_filings` with `tax_filing_boxes`
|
|
15
|
+
-- and `tax_filing_deposits`, and `file_filing()`, `record_filing_outcome()`,
|
|
16
|
+
-- `reopen_filing()`, `upcoming_filings()` and `filing_drift()` around them;
|
|
17
|
+
-- the `client` preset with `documents.deposit`, `company.export`, and
|
|
18
|
+
-- `portfolio_upcoming_filings()` and `portfolio_filings_touched_since()`;
|
|
19
|
+
-- `export_company()` and `import_company()` with `company_archive_registry`;
|
|
20
|
+
-- `import_bank_statement()` with `bank_statement_lines` and
|
|
21
|
+
-- `bank_transactions.import_key`; `document_lines.vat_category` and
|
|
22
|
+
-- `vat_rate`, there since the first schema and written at last, then frozen
|
|
23
|
+
-- at posting, and `companies.peppol_scheme` and
|
|
24
|
+
-- `peppol_identifier`, which the invoice written for the network is read
|
|
25
|
+
-- from; `documents.client_ref` and `rehearse_post_document()`, which the
|
|
26
|
+
-- command line keeps books through; `companies_with_capability()`, which
|
|
27
|
+
-- every policy of a company's table now asks once per statement; and
|
|
28
|
+
-- `document_amount_paid()` with the four guards that refuse, by name, a change
|
|
29
|
+
-- to a posted document, to a posted entry, and a posting that did not go
|
|
30
|
+
-- through `post_entry()` — `document_posted`, `entry_posted`,
|
|
31
|
+
-- `document_born_posted`, `entry_born_posted`, `entry_posted_by_hand`,
|
|
32
|
+
-- `document_posted_by_hand` — which the command line and the MCP server
|
|
33
|
+
-- explain rather than report as a failure. So the packages declare 0.4.0 as
|
|
34
|
+
-- their floor and refuse an older database by name, rather than improvising
|
|
35
|
+
-- over a column that is not there.
|
|
36
|
+
|
|
37
|
+
create or replace function ekwo_schema_version()
|
|
38
|
+
returns text
|
|
39
|
+
language sql
|
|
40
|
+
immutable
|
|
41
|
+
as $$
|
|
42
|
+
select '0.4.0'::text;
|
|
43
|
+
$$;
|
|
44
|
+
|
|
45
|
+
comment on function ekwo_schema_version() is
|
|
46
|
+
'Schema version of the installed release. Bumped by a migration, never by hand.';
|
|
47
|
+
|
|
48
|
+
-- `create or replace` keeps the privileges the function already had, and the
|
|
49
|
+
-- revoke is what every migration of this directory ends with: a function that
|
|
50
|
+
-- comes out with the built-in default is published by Supabase as an
|
|
51
|
+
-- anonymous RPC endpoint.
|
|
52
|
+
revoke execute on all functions in schema public from public;
|
|
53
|
+
grant execute on function ekwo_schema_version() to authenticated, service_role;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ekwo.ai/schemas/module/1.json",
|
|
3
|
+
"code": "assets",
|
|
4
|
+
"name": "Fixed assets",
|
|
5
|
+
"description": "Fixed assets, their depreciation schedule and their disposal. The durations, the declining coefficients and the prorata convention of a country are pack data; the engine knows no country.",
|
|
6
|
+
"schema": "assets",
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
"status": "available",
|
|
9
|
+
"requires_socle_min": "20260913075903",
|
|
10
|
+
"posts": true,
|
|
11
|
+
"mcp": { "prefix": "assets" },
|
|
12
|
+
"pack": { "section": "assets" }
|
|
13
|
+
}
|