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.
Files changed (361) hide show
  1. package/README.md +873 -0
  2. package/dist/args.d.ts +57 -0
  3. package/dist/args.d.ts.map +1 -0
  4. package/dist/args.js +134 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/assets/expected-objects.json +9432 -0
  7. package/dist/assets/migrations/20260911120000_core_companies.sql +260 -0
  8. package/dist/assets/migrations/20260911120100_accounts_journals.sql +211 -0
  9. package/dist/assets/migrations/20260911120200_contacts.sql +90 -0
  10. package/dist/assets/migrations/20260911120300_taxes.sql +146 -0
  11. package/dist/assets/migrations/20260911120400_entries.sql +338 -0
  12. package/dist/assets/migrations/20260911120500_documents.sql +279 -0
  13. package/dist/assets/migrations/20260911120600_payments_reconciliation.sql +251 -0
  14. package/dist/assets/migrations/20260911120700_bank.sql +178 -0
  15. package/dist/assets/migrations/20260911120800_analytics_attachments.sql +128 -0
  16. package/dist/assets/migrations/20260911120900_post_document.sql +279 -0
  17. package/dist/assets/migrations/20260911121000_reporting.sql +235 -0
  18. package/dist/assets/migrations/20260911121100_country_templates.sql +226 -0
  19. package/dist/assets/migrations/20260911121200_fec.sql +71 -0
  20. package/dist/assets/migrations/20260911130000_instance.sql +77 -0
  21. package/dist/assets/migrations/20260911130100_instance_members.sql +224 -0
  22. package/dist/assets/migrations/20260911140000_instance_admins.sql +112 -0
  23. package/dist/assets/migrations/20260911160000_tax_posting_templates_unique.sql +17 -0
  24. package/dist/assets/migrations/20260911170000_document_amount_paid.sql +82 -0
  25. package/dist/assets/migrations/20260911173000_post_payment.sql +122 -0
  26. package/dist/assets/migrations/20260911173100_sequence_counters_under_rls.sql +84 -0
  27. package/dist/assets/migrations/20260911183000_country_journal_defaults.sql +143 -0
  28. package/dist/assets/migrations/20260911193853_line_account_defaults.sql +274 -0
  29. package/dist/assets/migrations/20260911195054_products.sql +221 -0
  30. package/dist/assets/migrations/20260911210131_anon_surface.sql +85 -0
  31. package/dist/assets/migrations/20260912074712_country_packs.sql +318 -0
  32. package/dist/assets/migrations/20260912080311_report_code_and_region.sql +198 -0
  33. package/dist/assets/migrations/20260912081014_pack_certification_maintained.sql +31 -0
  34. package/dist/assets/migrations/20260912081015_pack_certification_backfill.sql +16 -0
  35. package/dist/assets/migrations/20260912090407_tax_report_boxes.sql +329 -0
  36. package/dist/assets/migrations/20260912091917_tax_on_base_value.sql +25 -0
  37. package/dist/assets/migrations/20260912091918_tax_engine_columns.sql +776 -0
  38. package/dist/assets/migrations/20260912094412_opening_and_closing.sql +759 -0
  39. package/dist/assets/migrations/20260912095825_charts_of_accounts.sql +326 -0
  40. package/dist/assets/migrations/20260912100412_financial_statements.sql +632 -0
  41. package/dist/assets/migrations/20260912104719_one_formula_evaluator.sql +209 -0
  42. package/dist/assets/migrations/20260912105720_entry_kind_appropriation.sql +26 -0
  43. package/dist/assets/migrations/20260912105721_appropriation_entry_kind.sql +444 -0
  44. package/dist/assets/migrations/20260912111751_document_rules.sql +274 -0
  45. package/dist/assets/migrations/20260912112132_cash_basis_vat_and_fx.sql +1189 -0
  46. package/dist/assets/migrations/20260913074512_modules.sql +493 -0
  47. package/dist/assets/migrations/20260913075903_asset_disposal_roles.sql +47 -0
  48. package/dist/assets/migrations/20260913083216_capabilities.sql +637 -0
  49. package/dist/assets/migrations/20260913083901_company_invitations.sql +262 -0
  50. package/dist/assets/migrations/20260913084402_user_preferences.sql +376 -0
  51. package/dist/assets/migrations/20260913084847_company_profile.sql +261 -0
  52. package/dist/assets/migrations/20260913085436_numbering_reads_the_pack.sql +235 -0
  53. package/dist/assets/migrations/20260913085932_api_keys.sql +303 -0
  54. package/dist/assets/migrations/20260913090216_fiscal_year_bounds.sql +159 -0
  55. package/dist/assets/migrations/20260913092527_entries_import.sql +219 -0
  56. package/dist/assets/migrations/20260913101536_null_safe_guards.sql +188 -0
  57. package/dist/assets/migrations/20260913102115_installer_is_named.sql +462 -0
  58. package/dist/assets/migrations/20260913102758_no_currency_default.sql +138 -0
  59. package/dist/assets/migrations/20260913103355_cash_basis_needs_a_box.sql +453 -0
  60. package/dist/assets/migrations/20260913104014_aged_balance_named_group.sql +79 -0
  61. package/dist/assets/migrations/20260913104232_foreign_key_indexes.sql +144 -0
  62. package/dist/assets/migrations/20260913105120_declared_no_reader.sql +41 -0
  63. package/dist/assets/migrations/20260913111407_pack_languages.sql +223 -0
  64. package/dist/assets/migrations/20260913112233_schema_comments_for_a_reader.sql +27 -0
  65. package/dist/assets/migrations/20260913114535_fec_opening_balances.sql +307 -0
  66. package/dist/assets/migrations/20260914103412_audit_log.sql +509 -0
  67. package/dist/assets/migrations/20260914111907_pack_upgrade.sql +428 -0
  68. package/dist/assets/migrations/20260914120500_rounding_reads_the_currency.sql +206 -0
  69. package/dist/assets/migrations/20260914121200_amounts_round_at_the_currency.sql +2403 -0
  70. package/dist/assets/migrations/20260914134325_schema_version_0_2_0.sql +36 -0
  71. package/dist/assets/migrations/20260914143915_accounts_in_use.sql +428 -0
  72. package/dist/assets/migrations/20260914144731_account_code_frozen.sql +99 -0
  73. package/dist/assets/migrations/20260914151207_schema_grants_its_own_rights.sql +284 -0
  74. package/dist/assets/migrations/20260914152840_pack_upgrade_records_its_own_line.sql +237 -0
  75. package/dist/assets/migrations/20260914163943_declaration_periodicity.sql +349 -0
  76. package/dist/assets/migrations/20260915094000_foreign_services_received.sql +37 -0
  77. package/dist/assets/migrations/20260915094500_mentions_read_a_foreign_service.sql +72 -0
  78. package/dist/assets/migrations/20260915153000_document_shares.sql +568 -0
  79. package/dist/assets/migrations/20260915160000_ec_sales_list.sql +181 -0
  80. package/dist/assets/migrations/20260915161842_pack_sources.sql +56 -0
  81. package/dist/assets/migrations/20260915170500_schema_version_0_3_0.sql +45 -0
  82. package/dist/assets/migrations/20260915174500_a_vat_category_is_a_code.sql +177 -0
  83. package/dist/assets/migrations/20260915180000_document_rule_references.sql +85 -0
  84. package/dist/assets/migrations/20260915181000_territories.sql +342 -0
  85. package/dist/assets/migrations/20260915181500_ec_sales_list_reads_the_territories.sql +222 -0
  86. package/dist/assets/migrations/20260915182000_intracom_triangular.sql +53 -0
  87. package/dist/assets/migrations/20260915182500_a_triangular_supply_says_reverse_charge.sql +99 -0
  88. package/dist/assets/migrations/20260915191200_a_document_knows_its_language.sql +633 -0
  89. package/dist/assets/migrations/20260915195000_a_price_that_holds_its_tax.sql +649 -0
  90. package/dist/assets/migrations/20260915200000_a_posting_names_its_boxes.sql +1004 -0
  91. package/dist/assets/migrations/20260916094500_a_company_files_more_than_one_declaration.sql +795 -0
  92. package/dist/assets/migrations/20260916103000_a_ledger_line_names_its_posting.sql +902 -0
  93. package/dist/assets/migrations/20260916123000_a_vocabulary_for_a_tax_that_is_not_a_vat.sql +144 -0
  94. package/dist/assets/migrations/20260916124000_a_box_can_be_a_rate_of_a_box.sql +525 -0
  95. package/dist/assets/migrations/20260916125000_a_tax_follows_the_territory.sql +1318 -0
  96. package/dist/assets/migrations/20260916126000_a_tax_point_and_its_exception.sql +1211 -0
  97. package/dist/assets/migrations/20260917090000_a_counterparty_that_learns.sql +561 -0
  98. package/dist/assets/migrations/20260917120000_what_the_money_pays.sql +545 -0
  99. package/dist/assets/migrations/20260917150000_a_filing_is_a_row.sql +488 -0
  100. package/dist/assets/migrations/20260917170000_when_it_is_due.sql +194 -0
  101. package/dist/assets/migrations/20260917180000_a_box_is_a_number_and_a_kind.sql +213 -0
  102. package/dist/assets/migrations/20260917190000_what_a_declaration_owes.sql +486 -0
  103. package/dist/assets/migrations/20260917200000_what_moved_after_it_went.sql +227 -0
  104. package/dist/assets/migrations/20260918090000_a_form_names_its_file.sql +27 -0
  105. package/dist/assets/migrations/20260918110000_a_deposit_and_what_came_back.sql +286 -0
  106. package/dist/assets/migrations/20260918113741_a_fourth_preset_is_named.sql +9 -0
  107. package/dist/assets/migrations/20260918113807_a_client_reads_and_hands_over.sql +96 -0
  108. package/dist/assets/migrations/20260918114322_two_definer_functions_check_their_caller.sql +75 -0
  109. package/dist/assets/migrations/20260918130000_every_company_somebody_keeps.sql +177 -0
  110. package/dist/assets/migrations/20260918140000_a_session_nobody_prepared.sql +40 -0
  111. package/dist/assets/migrations/20260918141107_a_line_keeps_the_tax_it_was_posted_with.sql +256 -0
  112. package/dist/assets/migrations/20260918141342_a_company_has_an_electronic_address.sql +66 -0
  113. package/dist/assets/migrations/20260918141605_an_invoice_reads_whole_from_the_views.sql +313 -0
  114. package/dist/assets/migrations/20260918141627_a_policy_asks_once.sql +132 -0
  115. package/dist/assets/migrations/20260918143352_a_suggestion_reads_the_words_once.sql +204 -0
  116. package/dist/assets/migrations/20260918143417_a_reference_of_the_caller_and_a_rehearsal.sql +115 -0
  117. package/dist/assets/migrations/20260918150712_a_company_leaves_with_its_books.sql +1119 -0
  118. package/dist/assets/migrations/20260918150931_a_statement_is_imported_once.sql +654 -0
  119. package/dist/assets/migrations/20260918161204_a_posted_document_does_not_move.sql +437 -0
  120. package/dist/assets/migrations/20260918161538_a_posted_entry_does_not_move.sql +252 -0
  121. package/dist/assets/migrations/20260918171946_an_entry_is_posted_by_post_entry.sql +289 -0
  122. package/dist/assets/migrations/20260918174312_schema_version_0_4_0.sql +53 -0
  123. package/dist/assets/modules/assets/module.json +13 -0
  124. package/dist/assets/modules/assets/supabase/migrations/20260913081447_assets.sql +1190 -0
  125. package/dist/assets/modules/assets/supabase/migrations/20260913104234_foreign_key_indexes.sql +47 -0
  126. package/dist/assets/modules/assets/supabase/migrations/20260913104500_assets_capabilities.sql +145 -0
  127. package/dist/assets/modules/assets/supabase/migrations/20260914122100_assets_round_at_the_currency.sql +564 -0
  128. package/dist/assets/modules/assets/supabase/migrations/20260914145012_assets_accounts_in_use.sql +37 -0
  129. package/dist/assets/modules/assets/supabase/migrations/20260914151530_assets_grants_its_own_rights.sql +63 -0
  130. package/dist/assets/modules/assets/supabase/migrations/20260918113912_assets_client_reads.sql +20 -0
  131. package/dist/assets/modules/assets/supabase/migrations/20260918150801_assets_leave_with_the_company.sql +41 -0
  132. package/dist/assets/modules/budgets/module.json +12 -0
  133. package/dist/assets/modules/budgets/supabase/migrations/20260913083012_budgets.sql +210 -0
  134. package/dist/assets/modules/budgets/supabase/migrations/20260913104233_foreign_key_indexes.sql +37 -0
  135. package/dist/assets/modules/budgets/supabase/migrations/20260913104501_budgets_capabilities.sql +72 -0
  136. package/dist/assets/modules/budgets/supabase/migrations/20260914122400_budgets_round_at_the_currency.sql +68 -0
  137. package/dist/assets/modules/budgets/supabase/migrations/20260914145048_budgets_accounts_in_use.sql +30 -0
  138. package/dist/assets/modules/budgets/supabase/migrations/20260914151812_budgets_grants_its_own_rights.sql +31 -0
  139. package/dist/assets/modules/budgets/supabase/migrations/20260918113954_budgets_client_reads.sql +20 -0
  140. package/dist/assets/modules/budgets/supabase/migrations/20260918150823_budgets_leave_with_the_company.sql +37 -0
  141. package/dist/assets/modules/schema/module.1.json +69 -0
  142. package/dist/assets/seed/00_currencies.sql +15 -0
  143. package/dist/assets/seed/00_territories.sql +280 -0
  144. package/dist/assets/seed/05_framework_generic.sql +110 -0
  145. package/dist/assets/seed/10_pack_be.sql +1284 -0
  146. package/dist/assets/seed/11_pack_fr.sql +1112 -0
  147. package/dist/assets/seed/12_pack_lu.sql +2484 -0
  148. package/dist/assets/seed/13_pack_ee.sql +733 -0
  149. package/dist/assets/seed/14_pack_gb.sql +796 -0
  150. package/dist/assets/seed/15_pack_us.sql +751 -0
  151. package/dist/assets/seed/90_demo_company.sql +368 -0
  152. package/dist/assets/seed/modules/assets/10_pack_be.sql +58 -0
  153. package/dist/assets/seed/modules/assets/11_pack_fr.sql +57 -0
  154. package/dist/assets/seed/modules/assets/14_pack_gb.sql +58 -0
  155. package/dist/assets/seed/modules/assets/15_pack_us.sql +60 -0
  156. package/dist/auth.d.ts +59 -0
  157. package/dist/auth.d.ts.map +1 -0
  158. package/dist/auth.js +134 -0
  159. package/dist/auth.js.map +1 -0
  160. package/dist/bin.d.ts +6 -0
  161. package/dist/bin.d.ts.map +1 -0
  162. package/dist/bin.js +8 -0
  163. package/dist/bin.js.map +1 -0
  164. package/dist/books.d.ts +60 -0
  165. package/dist/books.d.ts.map +1 -0
  166. package/dist/books.js +168 -0
  167. package/dist/books.js.map +1 -0
  168. package/dist/bootstrap.d.ts +236 -0
  169. package/dist/bootstrap.d.ts.map +1 -0
  170. package/dist/bootstrap.js +459 -0
  171. package/dist/bootstrap.js.map +1 -0
  172. package/dist/bundle.d.ts +27 -0
  173. package/dist/bundle.d.ts.map +1 -0
  174. package/dist/bundle.js +47 -0
  175. package/dist/bundle.js.map +1 -0
  176. package/dist/checklist.d.ts +35 -0
  177. package/dist/checklist.d.ts.map +1 -0
  178. package/dist/checklist.js +64 -0
  179. package/dist/checklist.js.map +1 -0
  180. package/dist/cli.d.ts +14 -0
  181. package/dist/cli.d.ts.map +1 -0
  182. package/dist/cli.js +318 -0
  183. package/dist/cli.js.map +1 -0
  184. package/dist/commands/company.d.ts +59 -0
  185. package/dist/commands/company.d.ts.map +1 -0
  186. package/dist/commands/company.js +287 -0
  187. package/dist/commands/company.js.map +1 -0
  188. package/dist/commands/contact.d.ts +9 -0
  189. package/dist/commands/contact.d.ts.map +1 -0
  190. package/dist/commands/contact.js +82 -0
  191. package/dist/commands/contact.js.map +1 -0
  192. package/dist/commands/demo.d.ts +19 -0
  193. package/dist/commands/demo.d.ts.map +1 -0
  194. package/dist/commands/demo.js +72 -0
  195. package/dist/commands/demo.js.map +1 -0
  196. package/dist/commands/doctor.d.ts +12 -0
  197. package/dist/commands/doctor.d.ts.map +1 -0
  198. package/dist/commands/doctor.js +68 -0
  199. package/dist/commands/doctor.js.map +1 -0
  200. package/dist/commands/document.d.ts +17 -0
  201. package/dist/commands/document.d.ts.map +1 -0
  202. package/dist/commands/document.js +217 -0
  203. package/dist/commands/document.js.map +1 -0
  204. package/dist/commands/init.d.ts +31 -0
  205. package/dist/commands/init.d.ts.map +1 -0
  206. package/dist/commands/init.js +535 -0
  207. package/dist/commands/init.js.map +1 -0
  208. package/dist/commands/login.d.ts +24 -0
  209. package/dist/commands/login.d.ts.map +1 -0
  210. package/dist/commands/login.js +128 -0
  211. package/dist/commands/login.js.map +1 -0
  212. package/dist/commands/migrate.d.ts +30 -0
  213. package/dist/commands/migrate.d.ts.map +1 -0
  214. package/dist/commands/migrate.js +115 -0
  215. package/dist/commands/migrate.js.map +1 -0
  216. package/dist/commands/module.d.ts +32 -0
  217. package/dist/commands/module.d.ts.map +1 -0
  218. package/dist/commands/module.js +285 -0
  219. package/dist/commands/module.js.map +1 -0
  220. package/dist/commands/pack.d.ts +22 -0
  221. package/dist/commands/pack.d.ts.map +1 -0
  222. package/dist/commands/pack.js +481 -0
  223. package/dist/commands/pack.js.map +1 -0
  224. package/dist/commands/payment.d.ts +15 -0
  225. package/dist/commands/payment.d.ts.map +1 -0
  226. package/dist/commands/payment.js +97 -0
  227. package/dist/commands/payment.js.map +1 -0
  228. package/dist/commands/register.d.ts +20 -0
  229. package/dist/commands/register.d.ts.map +1 -0
  230. package/dist/commands/register.js +134 -0
  231. package/dist/commands/register.js.map +1 -0
  232. package/dist/commands/status.d.ts +8 -0
  233. package/dist/commands/status.d.ts.map +1 -0
  234. package/dist/commands/status.js +134 -0
  235. package/dist/commands/status.js.map +1 -0
  236. package/dist/commands/whoami.d.ts +21 -0
  237. package/dist/commands/whoami.d.ts.map +1 -0
  238. package/dist/commands/whoami.js +133 -0
  239. package/dist/commands/whoami.js.map +1 -0
  240. package/dist/company.d.ts +16 -0
  241. package/dist/company.d.ts.map +1 -0
  242. package/dist/company.js +26 -0
  243. package/dist/company.js.map +1 -0
  244. package/dist/config.d.ts +24 -0
  245. package/dist/config.d.ts.map +1 -0
  246. package/dist/config.js +50 -0
  247. package/dist/config.js.map +1 -0
  248. package/dist/connection.d.ts +79 -0
  249. package/dist/connection.d.ts.map +1 -0
  250. package/dist/connection.js +116 -0
  251. package/dist/connection.js.map +1 -0
  252. package/dist/context.d.ts +47 -0
  253. package/dist/context.d.ts.map +1 -0
  254. package/dist/context.js +93 -0
  255. package/dist/context.js.map +1 -0
  256. package/dist/doctor.d.ts +45 -0
  257. package/dist/doctor.d.ts.map +1 -0
  258. package/dist/doctor.js +448 -0
  259. package/dist/doctor.js.map +1 -0
  260. package/dist/grants.d.ts +88 -0
  261. package/dist/grants.d.ts.map +1 -0
  262. package/dist/grants.js +174 -0
  263. package/dist/grants.js.map +1 -0
  264. package/dist/identity.d.ts +54 -0
  265. package/dist/identity.d.ts.map +1 -0
  266. package/dist/identity.js +132 -0
  267. package/dist/identity.js.map +1 -0
  268. package/dist/index.d.ts +44 -0
  269. package/dist/index.d.ts.map +1 -0
  270. package/dist/index.js +43 -0
  271. package/dist/index.js.map +1 -0
  272. package/dist/inventory.d.ts +155 -0
  273. package/dist/inventory.d.ts.map +1 -0
  274. package/dist/inventory.js +303 -0
  275. package/dist/inventory.js.map +1 -0
  276. package/dist/migrations.d.ts +76 -0
  277. package/dist/migrations.d.ts.map +1 -0
  278. package/dist/migrations.js +226 -0
  279. package/dist/migrations.js.map +1 -0
  280. package/dist/module/read.d.ts +91 -0
  281. package/dist/module/read.d.ts.map +1 -0
  282. package/dist/module/read.js +172 -0
  283. package/dist/module/read.js.map +1 -0
  284. package/dist/output.d.ts +110 -0
  285. package/dist/output.d.ts.map +1 -0
  286. package/dist/output.js +173 -0
  287. package/dist/output.js.map +1 -0
  288. package/dist/pack/certification.d.ts +27 -0
  289. package/dist/pack/certification.d.ts.map +1 -0
  290. package/dist/pack/certification.js +38 -0
  291. package/dist/pack/certification.js.map +1 -0
  292. package/dist/pack/compile.d.ts +75 -0
  293. package/dist/pack/compile.d.ts.map +1 -0
  294. package/dist/pack/compile.js +801 -0
  295. package/dist/pack/compile.js.map +1 -0
  296. package/dist/pack/filing.d.ts +48 -0
  297. package/dist/pack/filing.d.ts.map +1 -0
  298. package/dist/pack/filing.js +56 -0
  299. package/dist/pack/filing.js.map +1 -0
  300. package/dist/pack/read.d.ts +766 -0
  301. package/dist/pack/read.d.ts.map +1 -0
  302. package/dist/pack/read.js +2336 -0
  303. package/dist/pack/read.js.map +1 -0
  304. package/dist/pack/schema.d.ts +26 -0
  305. package/dist/pack/schema.d.ts.map +1 -0
  306. package/dist/pack/schema.js +181 -0
  307. package/dist/pack/schema.js.map +1 -0
  308. package/dist/pack/territories.d.ts +98 -0
  309. package/dist/pack/territories.d.ts.map +1 -0
  310. package/dist/pack/territories.js +0 -0
  311. package/dist/pack/territories.js.map +1 -0
  312. package/dist/pack/upgrade.d.ts +86 -0
  313. package/dist/pack/upgrade.d.ts.map +1 -0
  314. package/dist/pack/upgrade.js +76 -0
  315. package/dist/pack/upgrade.js.map +1 -0
  316. package/dist/pack/vat-codes.d.ts +273 -0
  317. package/dist/pack/vat-codes.d.ts.map +1 -0
  318. package/dist/pack/vat-codes.js +513 -0
  319. package/dist/pack/vat-codes.js.map +1 -0
  320. package/dist/profiles.d.ts +67 -0
  321. package/dist/profiles.d.ts.map +1 -0
  322. package/dist/profiles.js +187 -0
  323. package/dist/profiles.js.map +1 -0
  324. package/dist/prompt.d.ts +40 -0
  325. package/dist/prompt.d.ts.map +1 -0
  326. package/dist/prompt.js +120 -0
  327. package/dist/prompt.js.map +1 -0
  328. package/dist/registry.d.ts +84 -0
  329. package/dist/registry.d.ts.map +1 -0
  330. package/dist/registry.js +104 -0
  331. package/dist/registry.js.map +1 -0
  332. package/dist/rest.d.ts +81 -0
  333. package/dist/rest.d.ts.map +1 -0
  334. package/dist/rest.js +245 -0
  335. package/dist/rest.js.map +1 -0
  336. package/dist/schema.d.ts +15 -0
  337. package/dist/schema.d.ts.map +1 -0
  338. package/dist/schema.js +15 -0
  339. package/dist/schema.js.map +1 -0
  340. package/dist/seeds.d.ts +27 -0
  341. package/dist/seeds.d.ts.map +1 -0
  342. package/dist/seeds.js +47 -0
  343. package/dist/seeds.js.map +1 -0
  344. package/dist/session.d.ts +39 -0
  345. package/dist/session.d.ts.map +1 -0
  346. package/dist/session.js +92 -0
  347. package/dist/session.js.map +1 -0
  348. package/dist/sql.d.ts +48 -0
  349. package/dist/sql.d.ts.map +1 -0
  350. package/dist/sql.js +112 -0
  351. package/dist/sql.js.map +1 -0
  352. package/dist/status.d.ts +94 -0
  353. package/dist/status.d.ts.map +1 -0
  354. package/dist/status.js +120 -0
  355. package/dist/status.js.map +1 -0
  356. package/dist/ui.d.ts +55 -0
  357. package/dist/ui.d.ts.map +1 -0
  358. package/dist/ui.js +114 -0
  359. package/dist/ui.js.map +1 -0
  360. package/package.json +29 -0
  361. 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;