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,759 @@
|
|
|
1
|
+
-- Ekwo OS — opening balances and a year-end close that is parameterised.
|
|
2
|
+
--
|
|
3
|
+
-- The first release said, in `docs/decisions.md`: "No fiscal year closing
|
|
4
|
+
-- function yet. Carrying balances forward and merging the result into
|
|
5
|
+
-- retained earnings is a real piece of work with several national variants."
|
|
6
|
+
-- This migration is that work, and the finding of the pack analysis is that
|
|
7
|
+
-- the national variants are smaller than they look.
|
|
8
|
+
--
|
|
9
|
+
-- In Belgium, in France, in the United Kingdom and in the United States the
|
|
10
|
+
-- mechanism is the same two steps:
|
|
11
|
+
--
|
|
12
|
+
-- 1. move the result of the year out of the income statement,
|
|
13
|
+
-- 2. zero every income and expense account, and close the year.
|
|
14
|
+
--
|
|
15
|
+
-- What differs is *which account the result travels through*, and that is a
|
|
16
|
+
-- value, not a branch. Three styles cover the four countries above, and they
|
|
17
|
+
-- are named after the mechanism rather than after a country:
|
|
18
|
+
--
|
|
19
|
+
-- `retained_earnings` the result is closed straight into retained
|
|
20
|
+
-- earnings (United Kingdom, United States).
|
|
21
|
+
-- `result_accounts` it is closed into a current-year result account
|
|
22
|
+
-- that sits on the balance sheet, where it waits
|
|
23
|
+
-- for the meeting that allocates it (France:
|
|
24
|
+
-- 120 for a profit, 129 for a loss).
|
|
25
|
+
-- `appropriation_accounts` it first travels through an appropriation
|
|
26
|
+
-- account that is itself part of the income
|
|
27
|
+
-- statement, and from there to retained earnings
|
|
28
|
+
-- (Belgium: 693 to 140 for a profit, 793 to 141
|
|
29
|
+
-- for a loss). Those two accounts have to carry a
|
|
30
|
+
-- movement of their own, because the statutory
|
|
31
|
+
-- income statement ends on them.
|
|
32
|
+
--
|
|
33
|
+
-- So `close_fiscal_year` posts up to two entries. The **appropriation entry**
|
|
34
|
+
-- comes first and only exists in the third style: it is the one that puts 693
|
|
35
|
+
-- or 793 on the income statement. The **closing entry** then zeroes every
|
|
36
|
+
-- income and expense account, and needs a counterpart only in the first two
|
|
37
|
+
-- styles — in the third the accounts net to zero on their own, which the
|
|
38
|
+
-- function asserts rather than assumes.
|
|
39
|
+
--
|
|
40
|
+
-- **There is no entry that carries the balance sheet into the next year**,
|
|
41
|
+
-- and that is a deliberate departure from the way a Belgian or French package
|
|
42
|
+
-- prints an *à-nouveaux*. Every report in this schema reads the ledger from
|
|
43
|
+
-- the beginning: `trial_balance` computes the opening balance of a period as
|
|
44
|
+
-- the sum of everything booked before it, so a balance-sheet account already
|
|
45
|
+
-- stands on 1 January at exactly the figure it carried on 31 December. An
|
|
46
|
+
-- opening entry on top of that does not carry the balance forward, it counts
|
|
47
|
+
-- it twice — the first version of this function did, and a test now forbids
|
|
48
|
+
-- it. What the opening journal carries is the *first* opening of a set of
|
|
49
|
+
-- books, which is `opening_balance` below, and the year-end entries, which
|
|
50
|
+
-- are the ones dated on the last day of a year. The day a per-exercise
|
|
51
|
+
-- balance is wanted instead, the reports change first and the à-nouveaux
|
|
52
|
+
-- follows; doing it the other way round makes every carried balance double.
|
|
53
|
+
--
|
|
54
|
+
-- One consequence to know: after a year is closed, its income statement read
|
|
55
|
+
-- from the *movements* of that year is zero, because the closing entry is one
|
|
56
|
+
-- of them. That is what a post-closing trial balance is, and it is true of
|
|
57
|
+
-- every system that closes the income statement at all. The statements of
|
|
58
|
+
-- P0-4 read a closed year by leaving out the entries of the opening journal
|
|
59
|
+
-- dated on its last day.
|
|
60
|
+
--
|
|
61
|
+
-- What is deliberately *not* here: the allocation decided by a general
|
|
62
|
+
-- meeting. Dividends, the legal reserve, the transfer of a French 120 to 110
|
|
63
|
+
-- or 106 — all of that is a later entry taken by people, and a close that
|
|
64
|
+
-- guessed at it would be writing a decision nobody made.
|
|
65
|
+
--
|
|
66
|
+
-- `fiscal_years.is_closed` stops being an ordinary column: a trigger refuses
|
|
67
|
+
-- to let anyone set it, and the two functions below set a transaction-local
|
|
68
|
+
-- setting that the trigger recognises. The flag decides whether a whole year
|
|
69
|
+
-- accepts entries, and a column that any client may flip is not a lock.
|
|
70
|
+
|
|
71
|
+
-- ---------------------------------------------------------------------------
|
|
72
|
+
-- The parameter, as data
|
|
73
|
+
-- ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
create type closing_style as enum (
|
|
76
|
+
'retained_earnings',
|
|
77
|
+
'result_accounts',
|
|
78
|
+
'appropriation_accounts'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
comment on type closing_style is
|
|
82
|
+
'How the year-end close moves the result: straight to retained earnings, through a balance-sheet result account, or through an appropriation account of the income statement.';
|
|
83
|
+
|
|
84
|
+
-- **No column here carries a default**, and that is the point. A default
|
|
85
|
+
-- closing style would be one country's mechanism applied to every country
|
|
86
|
+
-- that has not said otherwise, and `'OPN'` is the journal code Belgium and
|
|
87
|
+
-- France happen to use. A pack that says nothing gets a refusal naming the
|
|
88
|
+
-- field it is missing, never somebody else's answer: `close_fiscal_year`
|
|
89
|
+
-- raises `no_closing_defaults` and `no_opening_journal`, and `ekwo pack
|
|
90
|
+
-- check` catches both before a seed is ever written.
|
|
91
|
+
alter table country_defaults
|
|
92
|
+
add column if not exists closing_style closing_style,
|
|
93
|
+
add column if not exists current_year_result_profit_code text,
|
|
94
|
+
add column if not exists current_year_result_loss_code text,
|
|
95
|
+
add column if not exists retained_earnings_loss_code text,
|
|
96
|
+
add column if not exists opening_journal_code text;
|
|
97
|
+
|
|
98
|
+
comment on column country_defaults.closing_style is
|
|
99
|
+
'Which of the three mechanisms close_fiscal_year() follows for a company of this country. Null until the pack says; there is no default, because a default would be one country''s answer given to every other.';
|
|
100
|
+
comment on column country_defaults.current_year_result_profit_code is
|
|
101
|
+
'Account the result of the year lands on when the year is profitable. Belgium 693, France 120. Null where the result goes straight to retained earnings.';
|
|
102
|
+
comment on column country_defaults.current_year_result_loss_code is
|
|
103
|
+
'Same, for a loss. Belgium 793, France 129. Both countries keep a profit and a loss apart, so this is a pair and not one account.';
|
|
104
|
+
comment on column country_defaults.retained_earnings_loss_code is
|
|
105
|
+
'Retained earnings account for an accumulated loss, where the chart keeps one apart from the profit account. Belgium 141, France 119. Null falls back to retained_earnings_code.';
|
|
106
|
+
comment on column country_defaults.opening_journal_code is
|
|
107
|
+
'Code of the journal the opening and the year-end entries are booked on, from the pack. Null until the pack names one, and then nothing opens or closes: there is no code written into the schema to fall back on.';
|
|
108
|
+
|
|
109
|
+
-- ---------------------------------------------------------------------------
|
|
110
|
+
-- What an entry is for
|
|
111
|
+
--
|
|
112
|
+
-- A closing entry is not an ordinary one: it exists to take the income
|
|
113
|
+
-- statement back to zero, so a report that shows the income statement of a
|
|
114
|
+
-- closed year has to leave it out. Until this column, the only way to know
|
|
115
|
+
-- was a heuristic — a journal of type `opening`, dated on the first or the
|
|
116
|
+
-- last day of a fiscal year — and a statement built on a heuristic is a
|
|
117
|
+
-- statement that goes wrong the first time somebody books something by hand
|
|
118
|
+
-- on that journal.
|
|
119
|
+
--
|
|
120
|
+
-- `normal` is every entry a business writes. `opening` is the one entry that
|
|
121
|
+
-- opens a set of books. `closing` is what `close_fiscal_year` writes, the
|
|
122
|
+
-- appropriation entry included, and so are the reversals `reopen_fiscal_year`
|
|
123
|
+
-- posts against them — a reversal belongs to the same report exclusion as
|
|
124
|
+
-- what it undoes.
|
|
125
|
+
--
|
|
126
|
+
-- The column is **not writable by hand**, for the same reason `is_closed` is
|
|
127
|
+
-- not: a label a client may set is a label a report cannot be built on, and
|
|
128
|
+
-- an ordinary purchase invoice quietly marked `closing` would disappear from
|
|
129
|
+
-- an income statement without anything looking wrong. The three functions
|
|
130
|
+
-- below set `ekwo.year_end_entry` while they write; nothing else may move the
|
|
131
|
+
-- column off `normal`.
|
|
132
|
+
-- ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
create type entry_kind as enum ('normal', 'opening', 'closing');
|
|
135
|
+
|
|
136
|
+
comment on type entry_kind is
|
|
137
|
+
'What an entry is for. A report of a closed year leaves out what is not normal.';
|
|
138
|
+
|
|
139
|
+
alter table entries
|
|
140
|
+
add column if not exists kind entry_kind not null default 'normal';
|
|
141
|
+
|
|
142
|
+
comment on column entries.kind is
|
|
143
|
+
'normal, opening or closing. Written by opening_balance(), close_fiscal_year() and reopen_fiscal_year(), and by nothing else.';
|
|
144
|
+
|
|
145
|
+
create index if not exists entries_kind_idx on entries (company_id, kind) where kind <> 'normal';
|
|
146
|
+
|
|
147
|
+
-- An installation that already booked its opening by hand keeps it: the
|
|
148
|
+
-- heuristic this column replaces runs once, here, and never again.
|
|
149
|
+
update entries e
|
|
150
|
+
set kind = case when e.entry_date = f.start_date then 'opening'::entry_kind
|
|
151
|
+
else 'closing'::entry_kind end
|
|
152
|
+
from fiscal_years f
|
|
153
|
+
join companies c on c.id = f.company_id
|
|
154
|
+
where f.id = e.fiscal_year_id
|
|
155
|
+
and e.journal_id in (select j.id from journals j
|
|
156
|
+
where j.company_id = e.company_id and j.journal_type = 'opening')
|
|
157
|
+
and e.entry_date in (f.start_date, f.end_date);
|
|
158
|
+
|
|
159
|
+
create or replace function entries_guard_kind()
|
|
160
|
+
returns trigger
|
|
161
|
+
language plpgsql
|
|
162
|
+
as $$
|
|
163
|
+
begin
|
|
164
|
+
if coalesce(current_setting('ekwo.year_end_entry', true), '') = 'on' then
|
|
165
|
+
return new;
|
|
166
|
+
end if;
|
|
167
|
+
|
|
168
|
+
if tg_op = 'INSERT' then
|
|
169
|
+
if new.kind <> 'normal' then
|
|
170
|
+
raise exception 'entry_kind_not_a_column: an opening or closing entry is written by opening_balance() and close_fiscal_year(), not by hand'
|
|
171
|
+
using errcode = '55006';
|
|
172
|
+
end if;
|
|
173
|
+
return new;
|
|
174
|
+
end if;
|
|
175
|
+
|
|
176
|
+
if new.kind is distinct from old.kind then
|
|
177
|
+
raise exception 'entry_kind_not_a_column: kind is written by opening_balance() and close_fiscal_year(), not by hand'
|
|
178
|
+
using errcode = '55006';
|
|
179
|
+
end if;
|
|
180
|
+
|
|
181
|
+
return new;
|
|
182
|
+
end;
|
|
183
|
+
$$;
|
|
184
|
+
|
|
185
|
+
comment on function entries_guard_kind() is
|
|
186
|
+
'Keeps entries.kind on `normal` outside the three functions that open and close a year. A label any client may set is a label a statement cannot be built on.';
|
|
187
|
+
|
|
188
|
+
create trigger entries_guard_kind
|
|
189
|
+
before insert or update on entries
|
|
190
|
+
for each row execute function entries_guard_kind();
|
|
191
|
+
|
|
192
|
+
-- ---------------------------------------------------------------------------
|
|
193
|
+
-- Closing a year is an act, not a column
|
|
194
|
+
--
|
|
195
|
+
-- `is_closed` decides whether a whole period accepts entries, and until now
|
|
196
|
+
-- any client that could write a fiscal year could flip it — which is the same
|
|
197
|
+
-- as having no lock. A trigger refuses the *transition*: an update that
|
|
198
|
+
-- changes `is_closed` or `closed_at` raises unless
|
|
199
|
+
-- `ekwo.closing_fiscal_year` is set, and the only two things that set it are
|
|
200
|
+
-- `close_fiscal_year()` and `reopen_fiscal_year()`. The setting is
|
|
201
|
+
-- transaction-local — `set_config(..., true)` — so it is gone when the
|
|
202
|
+
-- function returns, and a client that guesses the name still has to be inside
|
|
203
|
+
-- a transaction it does not control.
|
|
204
|
+
--
|
|
205
|
+
-- Creating a year that is *already* closed stays allowed, because it is not
|
|
206
|
+
-- the same act: it describes a year that happened in whatever kept the books
|
|
207
|
+
-- before, it computes nothing and it writes no entry. That is how a company
|
|
208
|
+
-- arrives with three closed years and one open one, next to the opening
|
|
209
|
+
-- balance that carries their result. What it cannot do is re-open them: the
|
|
210
|
+
-- transition is guarded in both directions.
|
|
211
|
+
-- ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
create or replace function fiscal_years_guard_closed()
|
|
214
|
+
returns trigger
|
|
215
|
+
language plpgsql
|
|
216
|
+
as $$
|
|
217
|
+
begin
|
|
218
|
+
if coalesce(current_setting('ekwo.closing_fiscal_year', true), '') = 'on' then
|
|
219
|
+
return new;
|
|
220
|
+
end if;
|
|
221
|
+
|
|
222
|
+
if new.is_closed is distinct from old.is_closed
|
|
223
|
+
or new.closed_at is distinct from old.closed_at then
|
|
224
|
+
raise exception 'fiscal_year_close_not_a_column: is_closed is set by close_fiscal_year() and cleared by reopen_fiscal_year()'
|
|
225
|
+
using errcode = '55006';
|
|
226
|
+
end if;
|
|
227
|
+
|
|
228
|
+
return new;
|
|
229
|
+
end;
|
|
230
|
+
$$;
|
|
231
|
+
|
|
232
|
+
comment on function fiscal_years_guard_closed() is
|
|
233
|
+
'Refuses a hand-written change to is_closed. A column any client may flip is not a lock.';
|
|
234
|
+
|
|
235
|
+
create trigger fiscal_years_guard_closed
|
|
236
|
+
before update on fiscal_years
|
|
237
|
+
for each row execute function fiscal_years_guard_closed();
|
|
238
|
+
|
|
239
|
+
-- ---------------------------------------------------------------------------
|
|
240
|
+
-- Helpers
|
|
241
|
+
-- ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
create or replace function opening_journal_id(p_company_id uuid)
|
|
244
|
+
returns uuid
|
|
245
|
+
language sql
|
|
246
|
+
stable
|
|
247
|
+
as $$
|
|
248
|
+
select j.id
|
|
249
|
+
from companies c
|
|
250
|
+
join country_defaults d on d.country = c.country
|
|
251
|
+
join journals j on j.company_id = c.id
|
|
252
|
+
and j.code = d.opening_journal_code
|
|
253
|
+
and j.journal_type = 'opening'
|
|
254
|
+
and j.active
|
|
255
|
+
where c.id = p_company_id
|
|
256
|
+
limit 1;
|
|
257
|
+
$$;
|
|
258
|
+
|
|
259
|
+
comment on function opening_journal_id(uuid) is
|
|
260
|
+
'The journal the opening and year-end entries go on, named by the pack of this company''s country. Null when the pack names none, and the callers refuse rather than guessing at a code.';
|
|
261
|
+
|
|
262
|
+
-- An entry that has been reversed, and a reversal itself, cancel out. So
|
|
263
|
+
-- "does this year already hold an opening entry" is a question about the ones
|
|
264
|
+
-- that are still standing, otherwise a year that was closed, re-opened and
|
|
265
|
+
-- closed again could never be closed a third time.
|
|
266
|
+
create or replace function has_opening_entry(p_fiscal_year_id uuid)
|
|
267
|
+
returns boolean
|
|
268
|
+
language sql
|
|
269
|
+
stable
|
|
270
|
+
as $$
|
|
271
|
+
select exists (
|
|
272
|
+
select 1
|
|
273
|
+
from entries e
|
|
274
|
+
where e.fiscal_year_id = p_fiscal_year_id
|
|
275
|
+
and e.state = 'posted'
|
|
276
|
+
and e.kind = 'opening'
|
|
277
|
+
and e.reversed_entry_id is null
|
|
278
|
+
and not exists (
|
|
279
|
+
select 1 from entries r
|
|
280
|
+
where r.reversed_entry_id = e.id and r.state = 'posted'
|
|
281
|
+
)
|
|
282
|
+
);
|
|
283
|
+
$$;
|
|
284
|
+
|
|
285
|
+
comment on function has_opening_entry(uuid) is
|
|
286
|
+
'Whether a fiscal year already carries an opening entry that still stands — an imported balance or the re-opening of the year before.';
|
|
287
|
+
|
|
288
|
+
-- ---------------------------------------------------------------------------
|
|
289
|
+
-- opening_balance
|
|
290
|
+
--
|
|
291
|
+
-- The natural import format from whatever kept the books until now: a trial
|
|
292
|
+
-- balance, one row per account, a debit or a credit. It becomes one entry on
|
|
293
|
+
-- the opening journal, dated on the first day of the year.
|
|
294
|
+
--
|
|
295
|
+
-- Income and expense accounts are refused unless the caller says otherwise.
|
|
296
|
+
-- A year that starts with a profit already on it is the commonest way an
|
|
297
|
+
-- import goes wrong, and the balance sheet is what an opening balance is made
|
|
298
|
+
-- of. `p_allow_result_accounts` exists for the one case where it is right:
|
|
299
|
+
-- taking the books over in the middle of a year that has already run, where
|
|
300
|
+
-- the movements of the months before have to arrive somehow.
|
|
301
|
+
-- ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
create or replace function opening_balance(
|
|
304
|
+
p_company_id uuid,
|
|
305
|
+
p_fiscal_year_id uuid,
|
|
306
|
+
p_lines jsonb,
|
|
307
|
+
p_allow_result_accounts boolean default false
|
|
308
|
+
)
|
|
309
|
+
returns uuid
|
|
310
|
+
language plpgsql
|
|
311
|
+
as $$
|
|
312
|
+
declare
|
|
313
|
+
v_year fiscal_years%rowtype;
|
|
314
|
+
v_journal uuid;
|
|
315
|
+
v_entry entries%rowtype;
|
|
316
|
+
v_line jsonb;
|
|
317
|
+
v_account uuid;
|
|
318
|
+
v_code text;
|
|
319
|
+
v_debit numeric(16, 2);
|
|
320
|
+
v_credit numeric(16, 2);
|
|
321
|
+
v_carries boolean;
|
|
322
|
+
v_sequence integer := 0;
|
|
323
|
+
v_debits numeric(16, 2) := 0;
|
|
324
|
+
v_credits numeric(16, 2) := 0;
|
|
325
|
+
begin
|
|
326
|
+
select * into v_year from fiscal_years where id = p_fiscal_year_id;
|
|
327
|
+
if not found then
|
|
328
|
+
raise exception 'unknown_fiscal_year: fiscal year % does not exist', p_fiscal_year_id;
|
|
329
|
+
end if;
|
|
330
|
+
if v_year.company_id <> p_company_id then
|
|
331
|
+
raise exception 'fiscal_year_other_company: fiscal year % does not belong to company %',
|
|
332
|
+
p_fiscal_year_id, p_company_id;
|
|
333
|
+
end if;
|
|
334
|
+
if v_year.is_closed then
|
|
335
|
+
raise exception 'fiscal_year_closed: % is closed', v_year.name
|
|
336
|
+
using errcode = '55006';
|
|
337
|
+
end if;
|
|
338
|
+
if has_opening_entry(p_fiscal_year_id) then
|
|
339
|
+
raise exception 'opening_entry_exists: % already has an opening entry', v_year.name;
|
|
340
|
+
end if;
|
|
341
|
+
|
|
342
|
+
if jsonb_typeof(p_lines) <> 'array' or jsonb_array_length(p_lines) = 0 then
|
|
343
|
+
raise exception 'opening_empty: opening_balance takes a non-empty array of lines';
|
|
344
|
+
end if;
|
|
345
|
+
|
|
346
|
+
v_journal := opening_journal_id(p_company_id);
|
|
347
|
+
if v_journal is null then
|
|
348
|
+
raise exception 'no_opening_journal: the pack of this company names no journal of type opening. Set defaults.journal_roles.opening in the pack, which fills country_defaults.opening_journal_code.'
|
|
349
|
+
using errcode = '55006';
|
|
350
|
+
end if;
|
|
351
|
+
|
|
352
|
+
perform set_config('ekwo.year_end_entry', 'on', true);
|
|
353
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date,
|
|
354
|
+
description, state, kind)
|
|
355
|
+
values (p_company_id, v_journal, p_fiscal_year_id, v_year.start_date,
|
|
356
|
+
'Opening balance', 'draft', 'opening')
|
|
357
|
+
returning * into v_entry;
|
|
358
|
+
perform set_config('ekwo.year_end_entry', 'off', true);
|
|
359
|
+
|
|
360
|
+
for v_line in select * from jsonb_array_elements(p_lines)
|
|
361
|
+
loop
|
|
362
|
+
v_sequence := v_sequence + 10;
|
|
363
|
+
v_code := v_line ->> 'account_code';
|
|
364
|
+
if v_code is null then
|
|
365
|
+
raise exception 'opening_line_without_account: line % names no account_code', v_sequence / 10;
|
|
366
|
+
end if;
|
|
367
|
+
|
|
368
|
+
select a.id, a.carries_forward into v_account, v_carries
|
|
369
|
+
from accounts a
|
|
370
|
+
where a.company_id = p_company_id and a.code = v_code;
|
|
371
|
+
if v_account is null then
|
|
372
|
+
raise exception 'unknown_account: % is not an account of this company', v_code;
|
|
373
|
+
end if;
|
|
374
|
+
if not v_carries and not p_allow_result_accounts then
|
|
375
|
+
raise exception 'opening_result_account: % is an income or expense account; an opening balance is made of the balance sheet. Pass p_allow_result_accounts when taking over books mid-year.',
|
|
376
|
+
v_code;
|
|
377
|
+
end if;
|
|
378
|
+
|
|
379
|
+
v_debit := round(coalesce((v_line ->> 'debit')::numeric, 0), 2);
|
|
380
|
+
v_credit := round(coalesce((v_line ->> 'credit')::numeric, 0), 2);
|
|
381
|
+
if v_debit < 0 or v_credit < 0 then
|
|
382
|
+
raise exception 'opening_negative_amount: % has a negative amount; a side is chosen, never a sign', v_code;
|
|
383
|
+
end if;
|
|
384
|
+
if v_debit <> 0 and v_credit <> 0 then
|
|
385
|
+
raise exception 'opening_two_sides: % carries both a debit and a credit', v_code;
|
|
386
|
+
end if;
|
|
387
|
+
if v_debit = 0 and v_credit = 0 then
|
|
388
|
+
raise exception 'opening_no_amount: % carries neither a debit nor a credit', v_code;
|
|
389
|
+
end if;
|
|
390
|
+
|
|
391
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
392
|
+
debit, credit, contact_id)
|
|
393
|
+
values (v_entry.id, p_company_id, v_account, v_sequence,
|
|
394
|
+
coalesce(v_line ->> 'label', 'Opening balance'),
|
|
395
|
+
v_debit, v_credit, (v_line ->> 'contact_id')::uuid);
|
|
396
|
+
|
|
397
|
+
v_debits := v_debits + v_debit;
|
|
398
|
+
v_credits := v_credits + v_credit;
|
|
399
|
+
end loop;
|
|
400
|
+
|
|
401
|
+
if v_debits <> v_credits then
|
|
402
|
+
raise exception 'opening_unbalanced: the opening balance has debit % and credit %',
|
|
403
|
+
v_debits, v_credits;
|
|
404
|
+
end if;
|
|
405
|
+
|
|
406
|
+
v_entry := post_entry(v_entry.id);
|
|
407
|
+
return v_entry.id;
|
|
408
|
+
end;
|
|
409
|
+
$$;
|
|
410
|
+
|
|
411
|
+
comment on function opening_balance(uuid, uuid, jsonb, boolean) is
|
|
412
|
+
'Posts a trial balance from a previous system as the opening entry of a fiscal year. Balance-sheet accounts only, unless the caller allows the others.';
|
|
413
|
+
|
|
414
|
+
-- ---------------------------------------------------------------------------
|
|
415
|
+
-- close_fiscal_year
|
|
416
|
+
-- ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
create or replace function close_fiscal_year(p_fiscal_year_id uuid)
|
|
419
|
+
returns jsonb
|
|
420
|
+
language plpgsql
|
|
421
|
+
as $$
|
|
422
|
+
declare
|
|
423
|
+
v_year fiscal_years%rowtype;
|
|
424
|
+
v_company companies%rowtype;
|
|
425
|
+
v_defaults country_defaults%rowtype;
|
|
426
|
+
v_journal uuid;
|
|
427
|
+
v_result numeric(16, 2);
|
|
428
|
+
v_profit boolean;
|
|
429
|
+
v_kind text;
|
|
430
|
+
v_cyr uuid; -- current-year result account, the side that applies
|
|
431
|
+
v_retained uuid; -- retained earnings, the side that applies
|
|
432
|
+
v_carries boolean;
|
|
433
|
+
v_appropriate uuid;
|
|
434
|
+
v_closing uuid;
|
|
435
|
+
v_entry entries%rowtype;
|
|
436
|
+
v_lines integer;
|
|
437
|
+
v_debits numeric(16, 2);
|
|
438
|
+
v_credits numeric(16, 2);
|
|
439
|
+
begin
|
|
440
|
+
select * into v_year from fiscal_years where id = p_fiscal_year_id for update;
|
|
441
|
+
if not found then
|
|
442
|
+
raise exception 'unknown_fiscal_year: fiscal year % does not exist', p_fiscal_year_id;
|
|
443
|
+
end if;
|
|
444
|
+
if v_year.is_closed then
|
|
445
|
+
raise exception 'fiscal_year_already_closed: % was closed on %', v_year.name, v_year.closed_at;
|
|
446
|
+
end if;
|
|
447
|
+
|
|
448
|
+
select * into v_company from companies where id = v_year.company_id;
|
|
449
|
+
|
|
450
|
+
-- 1. What has to be true before a year can be closed.
|
|
451
|
+
|
|
452
|
+
if exists (
|
|
453
|
+
select 1 from entries e
|
|
454
|
+
where e.company_id = v_year.company_id
|
|
455
|
+
and e.entry_date between v_year.start_date and v_year.end_date
|
|
456
|
+
and e.state = 'draft'
|
|
457
|
+
) then
|
|
458
|
+
raise exception 'fiscal_year_has_drafts: % still holds draft entries; post or cancel them first', v_year.name;
|
|
459
|
+
end if;
|
|
460
|
+
|
|
461
|
+
if exists (
|
|
462
|
+
select 1 from fiscal_years f
|
|
463
|
+
where f.company_id = v_year.company_id
|
|
464
|
+
and f.start_date < v_year.start_date
|
|
465
|
+
and not f.is_closed
|
|
466
|
+
and exists (select 1 from entries e where e.fiscal_year_id = f.id and e.state = 'posted')
|
|
467
|
+
) then
|
|
468
|
+
raise exception 'earlier_fiscal_year_open: a year before % holds posted entries and is not closed', v_year.name;
|
|
469
|
+
end if;
|
|
470
|
+
|
|
471
|
+
if exists (
|
|
472
|
+
select 1 from fiscal_years f
|
|
473
|
+
where f.company_id = v_year.company_id
|
|
474
|
+
and f.start_date > v_year.start_date
|
|
475
|
+
and f.is_closed
|
|
476
|
+
) then
|
|
477
|
+
raise exception 'later_fiscal_year_closed: a year after % is already closed; re-open it first', v_year.name;
|
|
478
|
+
end if;
|
|
479
|
+
|
|
480
|
+
-- 2. The accounts the result travels through, from the country model.
|
|
481
|
+
|
|
482
|
+
select * into v_defaults from country_defaults where country = v_company.country;
|
|
483
|
+
if not found then
|
|
484
|
+
raise exception 'unknown_country_template: no country model for %; close_fiscal_year reads its accounts from the pack',
|
|
485
|
+
v_company.country;
|
|
486
|
+
end if;
|
|
487
|
+
|
|
488
|
+
if v_defaults.closing_style is null then
|
|
489
|
+
raise exception 'no_closing_defaults: the pack of this company says nothing about how a year is closed. Set defaults.closing_style, and the account roles it needs, in the pack.'
|
|
490
|
+
using errcode = '55006';
|
|
491
|
+
end if;
|
|
492
|
+
|
|
493
|
+
v_journal := opening_journal_id(v_year.company_id);
|
|
494
|
+
if v_journal is null then
|
|
495
|
+
raise exception 'no_opening_journal: the pack of this company names no journal of type opening. Set defaults.journal_roles.opening in the pack, which fills country_defaults.opening_journal_code.'
|
|
496
|
+
using errcode = '55006';
|
|
497
|
+
end if;
|
|
498
|
+
|
|
499
|
+
-- The result of the year: income less expense, over the accounts that do
|
|
500
|
+
-- not carry forward. A credit balance is a profit.
|
|
501
|
+
select coalesce(-sum(l.balance), 0) into v_result
|
|
502
|
+
from entry_lines l
|
|
503
|
+
join entries e on e.id = l.entry_id and e.state = 'posted'
|
|
504
|
+
join accounts a on a.id = l.account_id
|
|
505
|
+
where l.company_id = v_year.company_id
|
|
506
|
+
and e.entry_date between v_year.start_date and v_year.end_date
|
|
507
|
+
and not a.carries_forward;
|
|
508
|
+
|
|
509
|
+
v_profit := v_result >= 0;
|
|
510
|
+
v_kind := case when v_result > 0 then 'profit'
|
|
511
|
+
when v_result < 0 then 'loss'
|
|
512
|
+
else 'nil' end;
|
|
513
|
+
|
|
514
|
+
v_retained := coalesce(
|
|
515
|
+
case when v_profit then v_company.retained_earnings_account_id end,
|
|
516
|
+
account_id_by_code(v_year.company_id,
|
|
517
|
+
case when v_profit then v_defaults.retained_earnings_code
|
|
518
|
+
else coalesce(v_defaults.retained_earnings_loss_code, v_defaults.retained_earnings_code) end));
|
|
519
|
+
|
|
520
|
+
v_cyr := account_id_by_code(v_year.company_id,
|
|
521
|
+
case when v_profit then v_defaults.current_year_result_profit_code
|
|
522
|
+
else v_defaults.current_year_result_loss_code end);
|
|
523
|
+
|
|
524
|
+
if v_defaults.closing_style = 'retained_earnings' then
|
|
525
|
+
if v_retained is null then
|
|
526
|
+
raise exception 'no_retained_earnings_account: the country model of % names none, and the company has none',
|
|
527
|
+
v_company.country;
|
|
528
|
+
end if;
|
|
529
|
+
v_cyr := v_retained;
|
|
530
|
+
else
|
|
531
|
+
if v_cyr is null then
|
|
532
|
+
raise exception 'no_current_year_result_account: the country model of % names no account for a %',
|
|
533
|
+
v_company.country, v_kind;
|
|
534
|
+
end if;
|
|
535
|
+
end if;
|
|
536
|
+
|
|
537
|
+
-- A style is a promise about where the result sits at the end. Assert it
|
|
538
|
+
-- rather than trust it: a pack that names an income account where the
|
|
539
|
+
-- balance sheet is expected would carry nothing forward, silently.
|
|
540
|
+
select a.carries_forward into v_carries from accounts a where a.id = v_cyr;
|
|
541
|
+
if v_defaults.closing_style = 'appropriation_accounts' and v_carries then
|
|
542
|
+
raise exception 'closing_style_mismatch: % expects an appropriation account inside the income statement, and % carries forward',
|
|
543
|
+
v_defaults.closing_style, v_cyr;
|
|
544
|
+
end if;
|
|
545
|
+
if v_defaults.closing_style <> 'appropriation_accounts' and not v_carries then
|
|
546
|
+
raise exception 'closing_style_mismatch: % expects an account on the balance sheet, and % does not carry forward',
|
|
547
|
+
v_defaults.closing_style, v_cyr;
|
|
548
|
+
end if;
|
|
549
|
+
|
|
550
|
+
-- 3. The appropriation entry: the result leaves the income statement
|
|
551
|
+
-- through an account that is itself part of it, and lands on retained
|
|
552
|
+
-- earnings. Only the third style has one, and only when there is a
|
|
553
|
+
-- result to move.
|
|
554
|
+
|
|
555
|
+
if v_defaults.closing_style = 'appropriation_accounts' and v_result <> 0 then
|
|
556
|
+
if v_retained is null then
|
|
557
|
+
raise exception 'no_retained_earnings_account: the country model of % names none, and the company has none',
|
|
558
|
+
v_company.country;
|
|
559
|
+
end if;
|
|
560
|
+
|
|
561
|
+
perform set_config('ekwo.year_end_entry', 'on', true);
|
|
562
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date,
|
|
563
|
+
description, state, kind)
|
|
564
|
+
values (v_year.company_id, v_journal, v_year.id, v_year.end_date,
|
|
565
|
+
'Result of the year', 'draft', 'closing')
|
|
566
|
+
returning * into v_entry;
|
|
567
|
+
perform set_config('ekwo.year_end_entry', 'off', true);
|
|
568
|
+
v_appropriate := v_entry.id;
|
|
569
|
+
|
|
570
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name, debit, credit)
|
|
571
|
+
values (v_appropriate, v_year.company_id, v_cyr, 10, 'Result of the year',
|
|
572
|
+
case when v_profit then abs(v_result) else 0 end,
|
|
573
|
+
case when v_profit then 0 else abs(v_result) end),
|
|
574
|
+
(v_appropriate, v_year.company_id, v_retained, 20, 'Result of the year',
|
|
575
|
+
case when v_profit then 0 else abs(v_result) end,
|
|
576
|
+
case when v_profit then abs(v_result) else 0 end);
|
|
577
|
+
|
|
578
|
+
perform post_entry(v_appropriate);
|
|
579
|
+
end if;
|
|
580
|
+
|
|
581
|
+
-- 4. The closing entry: every income and expense account back to zero.
|
|
582
|
+
-- In the first two styles the difference is the result and goes to the
|
|
583
|
+
-- account chosen above; in the third the accounts already net to zero,
|
|
584
|
+
-- because the appropriation entry put the result among them.
|
|
585
|
+
|
|
586
|
+
perform set_config('ekwo.year_end_entry', 'on', true);
|
|
587
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date,
|
|
588
|
+
description, state, kind)
|
|
589
|
+
values (v_year.company_id, v_journal, v_year.id, v_year.end_date,
|
|
590
|
+
'Closing entry', 'draft', 'closing')
|
|
591
|
+
returning * into v_entry;
|
|
592
|
+
perform set_config('ekwo.year_end_entry', 'off', true);
|
|
593
|
+
v_closing := v_entry.id;
|
|
594
|
+
|
|
595
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name, debit, credit)
|
|
596
|
+
select v_closing, v_year.company_id, s.account_id,
|
|
597
|
+
row_number() over (order by s.code) * 10,
|
|
598
|
+
'Closing entry',
|
|
599
|
+
case when s.balance < 0 then -s.balance else 0 end,
|
|
600
|
+
case when s.balance > 0 then s.balance else 0 end
|
|
601
|
+
from (
|
|
602
|
+
select a.id as account_id, a.code,
|
|
603
|
+
round(sum(l.balance), 2) as balance
|
|
604
|
+
from entry_lines l
|
|
605
|
+
join entries e on e.id = l.entry_id and e.state = 'posted'
|
|
606
|
+
join accounts a on a.id = l.account_id
|
|
607
|
+
where l.company_id = v_year.company_id
|
|
608
|
+
and e.entry_date between v_year.start_date and v_year.end_date
|
|
609
|
+
and not a.carries_forward
|
|
610
|
+
group by a.id, a.code
|
|
611
|
+
having round(sum(l.balance), 2) <> 0
|
|
612
|
+
) s;
|
|
613
|
+
|
|
614
|
+
select count(*) into v_lines from entry_lines where entry_id = v_closing;
|
|
615
|
+
|
|
616
|
+
if v_lines = 0 then
|
|
617
|
+
delete from entries where id = v_closing;
|
|
618
|
+
v_closing := null;
|
|
619
|
+
else
|
|
620
|
+
if v_defaults.closing_style <> 'appropriation_accounts' and v_result <> 0 then
|
|
621
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name, debit, credit)
|
|
622
|
+
values (v_closing, v_year.company_id, v_cyr, 1000000, 'Result of the year',
|
|
623
|
+
case when v_profit then 0 else abs(v_result) end,
|
|
624
|
+
case when v_profit then abs(v_result) else 0 end);
|
|
625
|
+
end if;
|
|
626
|
+
|
|
627
|
+
select total_debit, total_credit into v_debits, v_credits from entries where id = v_closing;
|
|
628
|
+
if v_debits <> v_credits then
|
|
629
|
+
raise exception 'closing_unbalanced: the closing entry has debit % and credit %. The income statement of % does not net to its result.',
|
|
630
|
+
v_debits, v_credits, v_year.name;
|
|
631
|
+
end if;
|
|
632
|
+
|
|
633
|
+
perform post_entry(v_closing);
|
|
634
|
+
end if;
|
|
635
|
+
|
|
636
|
+
-- 5. The year is closed, and only from here.
|
|
637
|
+
|
|
638
|
+
perform set_config('ekwo.closing_fiscal_year', 'on', true);
|
|
639
|
+
update fiscal_years
|
|
640
|
+
set is_closed = true,
|
|
641
|
+
closed_at = now()
|
|
642
|
+
where id = p_fiscal_year_id;
|
|
643
|
+
perform set_config('ekwo.closing_fiscal_year', 'off', true);
|
|
644
|
+
|
|
645
|
+
return jsonb_build_object(
|
|
646
|
+
'fiscal_year_id', p_fiscal_year_id,
|
|
647
|
+
'closing_style', v_defaults.closing_style,
|
|
648
|
+
'result', to_char(v_result, 'FM9999999999999990.00'),
|
|
649
|
+
'result_kind', v_kind,
|
|
650
|
+
'appropriation_entry_id', v_appropriate,
|
|
651
|
+
'closing_entry_id', v_closing
|
|
652
|
+
);
|
|
653
|
+
end;
|
|
654
|
+
$$;
|
|
655
|
+
|
|
656
|
+
comment on function close_fiscal_year(uuid) is
|
|
657
|
+
'Closes a fiscal year: the result leaves the income statement the way the country model says, and every income and expense account goes back to zero. The balance sheet needs no entry — the reports read the ledger from the beginning. The allocation decided by a meeting is never part of it.';
|
|
658
|
+
|
|
659
|
+
-- ---------------------------------------------------------------------------
|
|
660
|
+
-- reopen_fiscal_year
|
|
661
|
+
--
|
|
662
|
+
-- A close that was run a day too early. The entries it wrote are posted, and
|
|
663
|
+
-- a posted entry is never deleted here: they are reversed, and the flag goes
|
|
664
|
+
-- back. It is refused the moment a later year has been closed or booked into
|
|
665
|
+
-- — at that point re-opening would silently restate a result that later years
|
|
666
|
+
-- already stand on.
|
|
667
|
+
-- ---------------------------------------------------------------------------
|
|
668
|
+
|
|
669
|
+
create or replace function reopen_fiscal_year(p_fiscal_year_id uuid)
|
|
670
|
+
returns jsonb
|
|
671
|
+
language plpgsql
|
|
672
|
+
as $$
|
|
673
|
+
declare
|
|
674
|
+
v_year fiscal_years%rowtype;
|
|
675
|
+
v_original entries%rowtype;
|
|
676
|
+
v_reversal entries%rowtype;
|
|
677
|
+
v_reversed uuid[] := '{}';
|
|
678
|
+
begin
|
|
679
|
+
select * into v_year from fiscal_years where id = p_fiscal_year_id for update;
|
|
680
|
+
if not found then
|
|
681
|
+
raise exception 'unknown_fiscal_year: fiscal year % does not exist', p_fiscal_year_id;
|
|
682
|
+
end if;
|
|
683
|
+
if not v_year.is_closed then
|
|
684
|
+
raise exception 'fiscal_year_not_closed: % is already open', v_year.name;
|
|
685
|
+
end if;
|
|
686
|
+
|
|
687
|
+
-- Re-opening a year changes its result, and the result stands on the
|
|
688
|
+
-- balance sheet every later year reads. So a year that has been built on is
|
|
689
|
+
-- not a year that can be quietly restated.
|
|
690
|
+
if exists (
|
|
691
|
+
select 1 from fiscal_years f
|
|
692
|
+
where f.company_id = v_year.company_id
|
|
693
|
+
and f.start_date > v_year.end_date
|
|
694
|
+
and (f.is_closed or exists (
|
|
695
|
+
select 1 from entries e where e.fiscal_year_id = f.id and e.state = 'posted'))
|
|
696
|
+
) then
|
|
697
|
+
raise exception 'next_fiscal_year_in_use: a year after % is closed or already holds posted entries', v_year.name;
|
|
698
|
+
end if;
|
|
699
|
+
|
|
700
|
+
-- The flag first: the reversals are dated inside the year being re-opened,
|
|
701
|
+
-- and the period guard would refuse them while it is closed.
|
|
702
|
+
perform set_config('ekwo.closing_fiscal_year', 'on', true);
|
|
703
|
+
update fiscal_years
|
|
704
|
+
set is_closed = false,
|
|
705
|
+
closed_at = null
|
|
706
|
+
where id = p_fiscal_year_id;
|
|
707
|
+
perform set_config('ekwo.closing_fiscal_year', 'off', true);
|
|
708
|
+
|
|
709
|
+
for v_original in
|
|
710
|
+
select e.*
|
|
711
|
+
from entries e
|
|
712
|
+
where e.company_id = v_year.company_id
|
|
713
|
+
and e.state = 'posted'
|
|
714
|
+
and e.kind = 'closing'
|
|
715
|
+
and e.fiscal_year_id = p_fiscal_year_id
|
|
716
|
+
and e.reversed_entry_id is null
|
|
717
|
+
and not exists (
|
|
718
|
+
select 1 from entries r where r.reversed_entry_id = e.id and r.state = 'posted'
|
|
719
|
+
)
|
|
720
|
+
order by e.entry_date, e.number
|
|
721
|
+
loop
|
|
722
|
+
-- A reversal carries the kind of what it undoes: it belongs to the same
|
|
723
|
+
-- report exclusion, and a closing entry undone by a `normal` one would
|
|
724
|
+
-- reappear in the income statement on its own.
|
|
725
|
+
perform set_config('ekwo.year_end_entry', 'on', true);
|
|
726
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date,
|
|
727
|
+
description, state, reversed_entry_id, currency_code, kind)
|
|
728
|
+
values (v_original.company_id, v_original.journal_id, v_original.fiscal_year_id,
|
|
729
|
+
v_original.entry_date, 'Reversal of ' || v_original.number, 'draft',
|
|
730
|
+
v_original.id, v_original.currency_code, v_original.kind)
|
|
731
|
+
returning * into v_reversal;
|
|
732
|
+
perform set_config('ekwo.year_end_entry', 'off', true);
|
|
733
|
+
|
|
734
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
735
|
+
debit, credit, contact_id, date_maturity, currency_code)
|
|
736
|
+
select v_reversal.id, l.company_id, l.account_id, l.sequence, l.name,
|
|
737
|
+
l.credit, l.debit, l.contact_id, l.date_maturity, l.currency_code
|
|
738
|
+
from entry_lines l
|
|
739
|
+
where l.entry_id = v_original.id
|
|
740
|
+
order by l.sequence;
|
|
741
|
+
|
|
742
|
+
perform post_entry(v_reversal.id);
|
|
743
|
+
v_reversed := v_reversed || v_reversal.id;
|
|
744
|
+
end loop;
|
|
745
|
+
|
|
746
|
+
return jsonb_build_object(
|
|
747
|
+
'fiscal_year_id', p_fiscal_year_id,
|
|
748
|
+
'reversal_entry_ids', to_jsonb(v_reversed)
|
|
749
|
+
);
|
|
750
|
+
end;
|
|
751
|
+
$$;
|
|
752
|
+
|
|
753
|
+
comment on function reopen_fiscal_year(uuid) is
|
|
754
|
+
'Undoes a close: reverses the entries it wrote and clears is_closed. Refused once a later year is closed or holds entries of its own.';
|
|
755
|
+
|
|
756
|
+
-- A migration that adds a function ends with this line, from PUBLIC and never
|
|
757
|
+
-- from `anon`: `alter default privileges ... revoke execute on functions from
|
|
758
|
+
-- public` does not close a function created later.
|
|
759
|
+
revoke execute on all functions in schema public from public;
|