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,138 @@
1
+ -- Ekwo OS — a euro written into the schema is a country written into the core.
2
+ --
3
+ -- Seven columns were declared `char(3) not null default 'EUR'` and one
4
+ -- `char(2) not null default 'fr'`. They date from before "a country is data"
5
+ -- was a rule, and they are the same mistake the country literals were: a
6
+ -- Canadian installation that forgets to name a currency does not get an
7
+ -- error, it gets euros — in its documents, its payments, its catalogue, its
8
+ -- bank accounts and its statements — and finds out at the first report.
9
+ --
10
+ -- The defaults go. What replaces them is not another default but the answer
11
+ -- that was always the right one, taken from the row above:
12
+ --
13
+ -- * a company's currency and language come from **the pack of its fiscal
14
+ -- country** or from whoever creates it. `create_company()` already
15
+ -- refuses both by name when the pack is silent (`no_currency`,
16
+ -- `no_language`) and `ekwo init` asks; for every other writer a trigger
17
+ -- reads `country_defaults`, so a silent pack leaves the column null and
18
+ -- the NOT NULL refuses the insert. Silence is an error again, where it
19
+ -- used to be euros;
20
+ -- * a document, a payment, a catalogue item and a bank account take the
21
+ -- currency of **their company**, and a statement line takes the currency
22
+ -- of **its bank account**, which is a stronger answer than the company's
23
+ -- and falls back to it. One trigger, one place, no literal;
24
+ -- * `country_defaults.currency_code` is pack data and the compiled seed of
25
+ -- every country fills it. A default there would have meant a pack that
26
+ -- says nothing about its own currency silently claims the euro.
27
+ --
28
+ -- The trigger fills only what the caller left empty, so a foreign-currency
29
+ -- document still says so and nothing that used to work stops working. What
30
+ -- changes is what happens when *nobody* answers: it used to be euros, and it
31
+ -- is now the company, which is an answer the installation actually chose.
32
+
33
+ alter table companies alter column currency_code drop default;
34
+ alter table companies alter column language drop default;
35
+ alter table country_defaults alter column currency_code drop default;
36
+ alter table documents alter column currency_code drop default;
37
+ alter table payments alter column currency_code drop default;
38
+ alter table products alter column currency_code drop default;
39
+ alter table bank_accounts alter column currency_code drop default;
40
+ alter table bank_transactions alter column currency_code drop default;
41
+
42
+ -- ---------------------------------------------------------------------------
43
+ -- Where a currency comes from when the caller names none
44
+ --
45
+ -- A BEFORE INSERT trigger rather than a column default, because a default can
46
+ -- only be a constant and the answer is a lookup. It runs before the NOT NULL
47
+ -- is checked, which is what lets the column stay NOT NULL with no default at
48
+ -- all: leave it out and the company answers; leave the company out and the
49
+ -- insert is refused by the foreign key, as it already was.
50
+ -- ---------------------------------------------------------------------------
51
+
52
+ create or replace function currency_of_company()
53
+ returns trigger
54
+ language plpgsql
55
+ as $$
56
+ begin
57
+ if new.currency_code is null then
58
+ select c.currency_code into new.currency_code
59
+ from companies c where c.id = new.company_id;
60
+ end if;
61
+ return new;
62
+ end;
63
+ $$;
64
+
65
+ comment on function currency_of_company() is
66
+ 'Fills currency_code from the company when the caller named none. The one place the question is answered for a table that belongs to a company.';
67
+
68
+ -- A company has no company above it: its answer is its country's pack. A
69
+ -- pack that names neither leaves the column null, and NOT NULL refuses the
70
+ -- row — which is the whole point of dropping the default.
71
+ create or replace function locale_of_country_pack()
72
+ returns trigger
73
+ language plpgsql
74
+ as $$
75
+ declare
76
+ v_defaults country_defaults%rowtype;
77
+ begin
78
+ if new.currency_code is null or new.language is null then
79
+ select * into v_defaults
80
+ from country_defaults
81
+ where country = coalesce(new.fiscal_country, new.country);
82
+ new.currency_code := coalesce(new.currency_code, v_defaults.currency_code);
83
+ new.language := coalesce(new.language, v_defaults.language_default);
84
+ end if;
85
+ return new;
86
+ end;
87
+ $$;
88
+
89
+ comment on function locale_of_country_pack() is
90
+ 'Fills a company''s currency and language from the pack of its fiscal country when the caller named neither. A pack that says nothing leaves them null, and NOT NULL refuses the row.';
91
+
92
+ create trigger companies_locale_of_country_pack
93
+ before insert on companies
94
+ for each row execute function locale_of_country_pack();
95
+
96
+ create trigger documents_currency_of_company
97
+ before insert on documents
98
+ for each row execute function currency_of_company();
99
+
100
+ create trigger payments_currency_of_company
101
+ before insert on payments
102
+ for each row execute function currency_of_company();
103
+
104
+ create trigger products_currency_of_company
105
+ before insert on products
106
+ for each row execute function currency_of_company();
107
+
108
+ create trigger bank_accounts_currency_of_company
109
+ before insert on bank_accounts
110
+ for each row execute function currency_of_company();
111
+
112
+ -- A statement line is in the currency of the account it came off, which is
113
+ -- not always the company's. The account answers first, the company after.
114
+ create or replace function currency_of_bank_account()
115
+ returns trigger
116
+ language plpgsql
117
+ as $$
118
+ begin
119
+ if new.currency_code is null then
120
+ select b.currency_code into new.currency_code
121
+ from bank_accounts b where b.id = new.bank_account_id;
122
+ end if;
123
+ if new.currency_code is null then
124
+ select c.currency_code into new.currency_code
125
+ from companies c where c.id = new.company_id;
126
+ end if;
127
+ return new;
128
+ end;
129
+ $$;
130
+
131
+ comment on function currency_of_bank_account() is
132
+ 'Fills a statement line''s currency_code from its bank account, and from the company as a last resort.';
133
+
134
+ create trigger bank_transactions_currency_of_bank_account
135
+ before insert on bank_transactions
136
+ for each row execute function currency_of_bank_account();
137
+
138
+ revoke execute on all functions in schema public from public;
@@ -0,0 +1,453 @@
1
+ -- Ekwo OS — a tax that waits for a box that does not exist waits for ever.
2
+ --
3
+ -- `settle_cash_basis_tax()` is what moves a cash-basis tax off the transition
4
+ -- account when the invoice is paid. It finds the lines still waiting with
5
+ --
6
+ -- where t.cash_basis and q.declaration_box is null and q.box_amount is not null
7
+ --
8
+ -- and `post_document()` writes `box_amount` only when the posting names a
9
+ -- `declaration_box`:
10
+ --
11
+ -- v_box_amount := case when p.declaration_box is null then null else … end
12
+ --
13
+ -- So a cash-basis tax whose posting names no box books its amount onto the
14
+ -- transition account, is never seen as waiting, never settles, and never
15
+ -- reaches a declaration. Nothing raises. The balance simply grows, and it is
16
+ -- discovered — if it is discovered — years later by somebody reconciling a
17
+ -- transition account against nothing. The `no_cash_basis_box` exception that
18
+ -- was written for exactly this case sits inside the loop that already
19
+ -- excluded the line, so it could not fire.
20
+ --
21
+ -- The rule belongs upstream of the ledger, twice over, and both are cheap:
22
+ --
23
+ -- * `ekwo pack check` refuses a cash-basis tax whose tax posting names no
24
+ -- box, in the same place it already refuses one that names no transition
25
+ -- account, no more than one tax posting, or a non-deductible share;
26
+ -- * `post_document()` raises `no_cash_basis_box` before it writes the
27
+ -- entry, next to `no_cash_basis_account` and `cash_basis_split_tax` —
28
+ -- because a pack can be written by hand and installed without the CLI.
29
+ --
30
+ -- The unreachable check in `settle_cash_basis_tax()` stays where it is. It is
31
+ -- the last line of defence for a posting whose box was removed after a
32
+ -- document was booked, and with the two above in front of it that is the only
33
+ -- way to reach it.
34
+
35
+ create or replace function post_document(p_document_id uuid)
36
+ returns entries
37
+ language plpgsql
38
+ as $$
39
+ declare
40
+ v_doc documents%rowtype;
41
+ v_entry entries%rowtype;
42
+ v_journal uuid;
43
+ v_date date;
44
+ v_is_sale boolean;
45
+ v_is_credit boolean;
46
+ v_kind tax_document_kind;
47
+ v_base_credit boolean;
48
+ v_seq integer := 0;
49
+ v_contact uuid;
50
+ v_maturity date;
51
+ v_terms smallint;
52
+ v_counterpart uuid;
53
+ v_diff numeric(16, 2);
54
+ v_diff_cur numeric(16, 2);
55
+ v_amount numeric(16, 2);
56
+ v_book numeric(16, 2);
57
+ v_box_amount numeric(16, 2);
58
+ v_share numeric(16, 2);
59
+ v_share_book numeric(16, 2);
60
+ v_share_box numeric(16, 2);
61
+ v_left numeric(16, 2);
62
+ v_left_box numeric(16, 2);
63
+ v_side_left numeric(16, 2);
64
+ v_side_left_neg numeric(16, 2);
65
+ v_side_credit boolean;
66
+ v_label text;
67
+ v_rate numeric(18, 8);
68
+ v_home char(3);
69
+ v_foreign boolean;
70
+ v_total_cur numeric(16, 2);
71
+ v_cash boolean;
72
+ v_transition uuid;
73
+ v_postings integer;
74
+ r record;
75
+ p record;
76
+ g record;
77
+ begin
78
+ select * into v_doc from documents where id = p_document_id for update;
79
+ if not found then
80
+ raise exception 'unknown_document: document % does not exist', p_document_id;
81
+ end if;
82
+ if v_doc.state = 'posted' then
83
+ raise exception 'document_already_posted: document % is already posted', p_document_id;
84
+ end if;
85
+ if v_doc.state = 'cancelled' then
86
+ raise exception 'document_cancelled: document % cannot be posted', p_document_id;
87
+ end if;
88
+ if v_doc.entry_id is not null then
89
+ raise exception 'document_already_booked: document % already points at entry %',
90
+ p_document_id, v_doc.entry_id;
91
+ end if;
92
+
93
+ if v_doc.doc_type in ('sale_quote', 'purchase_order') then
94
+ raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
95
+ end if;
96
+
97
+ v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
98
+ v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
99
+ v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
100
+ -- Sale invoice and purchase credit note credit the base; the other two debit it.
101
+ v_base_credit := (v_is_sale <> v_is_credit);
102
+
103
+ v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
104
+
105
+ if not exists (
106
+ select 1 from document_lines
107
+ where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
108
+ ) then
109
+ raise exception 'document_empty: document % has no billable line', p_document_id;
110
+ end if;
111
+
112
+ -- A fixed-amount tax has no basis to spread over lines; refuse rather than
113
+ -- guess.
114
+ if exists (
115
+ select 1 from document_lines l join taxes t on t.id = l.tax_id
116
+ where l.document_id = p_document_id and t.amount_type <> 'percent'
117
+ ) then
118
+ raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
119
+ end if;
120
+
121
+ -- Every tax used must be in force on the accounting date.
122
+ for r in
123
+ select distinct t.id, t.code, t.valid_from, t.valid_to
124
+ from document_lines l join taxes t on t.id = l.tax_id
125
+ where l.document_id = p_document_id
126
+ loop
127
+ if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
128
+ raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
129
+ end if;
130
+ end loop;
131
+
132
+ -- Totals are derived; make sure they reflect the lines as they stand now.
133
+ perform documents_refresh_totals(p_document_id);
134
+ select * into v_doc from documents where id = p_document_id;
135
+
136
+ select c.currency_code into v_home from companies c where c.id = v_doc.company_id;
137
+ v_rate := v_doc.exchange_rate;
138
+ v_foreign := v_doc.currency_code <> v_home;
139
+
140
+ v_journal := coalesce(
141
+ v_doc.journal_id,
142
+ case when v_is_sale
143
+ then (select sales_journal_id from companies where id = v_doc.company_id)
144
+ else (select purchase_journal_id from companies where id = v_doc.company_id)
145
+ end
146
+ );
147
+ if v_journal is null then
148
+ raise exception 'no_journal: set journal_id on the document or a default journal on the company';
149
+ end if;
150
+
151
+ perform assert_period_open(v_doc.company_id, v_date, true);
152
+
153
+ v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
154
+
155
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
156
+ description, state, document_id, currency_code)
157
+ values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
158
+ coalesce(v_doc.number, v_doc.supplier_reference),
159
+ v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
160
+ then ' / ' || v_doc.supplier_reference else '' end,
161
+ 'draft', p_document_id, v_doc.currency_code)
162
+ returning * into v_entry;
163
+
164
+ -- ------------------------------------------------------------------ bases
165
+ for r in
166
+ select l.account_id,
167
+ l.tax_id,
168
+ sum(l.amount_untaxed) as base_amount,
169
+ min(l.sequence) as seq,
170
+ string_agg(distinct l.name, ', ') as label
171
+ from document_lines l
172
+ where l.document_id = p_document_id
173
+ and l.line_type = 'product'
174
+ group by l.account_id, l.tax_id
175
+ having sum(l.amount_untaxed) <> 0
176
+ order by 4
177
+ loop
178
+ select tp.declaration_box, tp.factor_percent, tp.box_factor_percent,
179
+ coalesce(t.cash_basis, false) as cash_basis
180
+ into p
181
+ from tax_postings tp
182
+ join taxes t on t.id = tp.tax_id
183
+ where tp.tax_id = r.tax_id
184
+ and tp.document_kind = v_kind
185
+ and tp.posting_type = 'base'
186
+ limit 1;
187
+
188
+ v_amount := round(r.base_amount * coalesce(p.factor_percent, 100) / 100, 2);
189
+ v_book := round(v_amount / v_rate, 2);
190
+ v_seq := v_seq + 10;
191
+
192
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
193
+ debit, credit, tax_id, tax_line,
194
+ declaration_box, box_amount, currency_code, amount_currency)
195
+ values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
196
+ case when v_base_credit then 0 else v_book end,
197
+ case when v_base_credit then v_book else 0 end,
198
+ r.tax_id, false,
199
+ case when coalesce(p.cash_basis, false) then null else p.declaration_box end,
200
+ case when p.declaration_box is null then null
201
+ else round(r.base_amount * coalesce(p.box_factor_percent, 100) / 100 / v_rate, 2) end,
202
+ v_doc.currency_code,
203
+ case when v_foreign then v_amount end);
204
+ end loop;
205
+
206
+ -- ------------------------------------------------------------------ taxes
207
+ for r in
208
+ select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
209
+ from document_tax_summary s
210
+ where s.document_id = p_document_id
211
+ and s.tax_id is not null
212
+ and s.tax_amount <> 0
213
+ order by s.tax_code
214
+ loop
215
+ select t.cash_basis, t.cash_basis_transition_account_id
216
+ into v_cash, v_transition
217
+ from taxes t where t.id = r.tax_id;
218
+
219
+ if v_cash then
220
+ -- A tax that waits needs somewhere to wait. Refuse by name rather than
221
+ -- book it on the account it is due on, which would make it due.
222
+ if v_transition is null then
223
+ raise exception 'no_cash_basis_account: tax % falls due on collection and names no transition account',
224
+ r.tax_code;
225
+ end if;
226
+ -- One posting per side, or the transition lines of a document cannot be
227
+ -- told apart when the matching sends each of them on. A tax whose
228
+ -- postings net out has nothing waiting to collect anyway.
229
+ select count(*) into v_postings
230
+ from tax_postings tp
231
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
232
+ and tp.posting_type = 'tax';
233
+ if v_postings > 1 then
234
+ raise exception 'cash_basis_split_tax: tax % falls due on collection and has % tax postings; it takes one',
235
+ r.tax_code, v_postings;
236
+ end if;
237
+ if exists (select 1 from tax_postings tp
238
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
239
+ and tp.posting_type = 'tax_on_base') then
240
+ raise exception 'cash_basis_tax_on_base: tax % falls due on collection and carries a non-deductible share; a cost is not deferred',
241
+ r.tax_code;
242
+ end if;
243
+ -- And it needs a box to fall due *into*. `settle_cash_basis_tax()` only
244
+ -- ever looks at lines that carry a `box_amount`, and a posting with no
245
+ -- `declaration_box` produces none — so the amount would sit on the
246
+ -- transition account for ever, settled by nothing and reported by
247
+ -- nothing, with no error anywhere. Refuse it here, where the pack can
248
+ -- still be corrected, rather than discover it in a balance years later.
249
+ if not exists (select 1 from tax_postings tp
250
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
251
+ and tp.posting_type = 'tax'
252
+ and tp.declaration_box is not null) then
253
+ raise exception 'no_cash_basis_box: tax % falls due on collection and its posting names no declaration box; the amount would wait on the transition account and never settle',
254
+ r.tax_code;
255
+ end if;
256
+ end if;
257
+
258
+ -- The postings of one side share out the tax of the group; the last of
259
+ -- each side takes what is left. Until `tax_on_base` there was never more
260
+ -- than one posting per side, so this changes no existing tax by a cent —
261
+ -- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
262
+ -- where rounding each half on its own would book 0.64 against a document
263
+ -- that totals 0.63.
264
+ v_side_left := null;
265
+ v_side_left_neg := null;
266
+
267
+ for p in
268
+ select tp.posting_type, tp.factor_percent, tp.account_id,
269
+ tp.declaration_box, tp.box_factor_percent,
270
+ case when tp.factor_percent >= 0 then 1 else -1 end as side,
271
+ sum(abs(tp.factor_percent))
272
+ over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
273
+ as side_factor,
274
+ row_number() over (
275
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end
276
+ order by tp.sequence, tp.id)
277
+ = count(*) over (
278
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end)
279
+ as is_last_of_side
280
+ from tax_postings tp
281
+ where tp.tax_id = r.tax_id
282
+ and tp.document_kind = v_kind
283
+ and tp.posting_type in ('tax', 'tax_on_base')
284
+ order by tp.sequence, tp.id
285
+ loop
286
+ -- The tax of the group was rounded once, in the view. Every posting is
287
+ -- a share of that one figure, never of a re-derived one.
288
+ if p.side >= 0 then
289
+ if v_side_left is null then
290
+ v_side_left := round(r.tax_amount * p.side_factor / 100, 2);
291
+ end if;
292
+ if p.is_last_of_side then
293
+ v_amount := v_side_left;
294
+ else
295
+ v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
296
+ v_side_left := v_side_left - v_amount;
297
+ end if;
298
+ else
299
+ if v_side_left_neg is null then
300
+ v_side_left_neg := round(r.tax_amount * p.side_factor / 100, 2);
301
+ end if;
302
+ if p.is_last_of_side then
303
+ v_amount := v_side_left_neg;
304
+ else
305
+ v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
306
+ v_side_left_neg := v_side_left_neg - v_amount;
307
+ end if;
308
+ end if;
309
+
310
+ if v_amount = 0 then
311
+ continue;
312
+ end if;
313
+ -- A positive factor keeps the side of the base, a negative one flips it.
314
+ v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
315
+ -- The box keeps its own rounding: `box_factor_percent` was always
316
+ -- independent from `factor_percent`, because a declaration figure is
317
+ -- not a ledger figure and only the ledger has to balance.
318
+ v_box_amount := case when p.declaration_box is null then null
319
+ else round(r.tax_amount * p.box_factor_percent / 100 / v_rate, 2) end;
320
+ v_book := round(v_amount / v_rate, 2);
321
+
322
+ if p.posting_type = 'tax' then
323
+ v_seq := v_seq + 10;
324
+
325
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
326
+ debit, credit, tax_id, tax_line,
327
+ declaration_box, box_amount, currency_code, amount_currency)
328
+ values (v_entry.id, v_doc.company_id,
329
+ case when v_cash then v_transition else p.account_id end,
330
+ v_seq, r.tax_name,
331
+ case when v_side_credit then 0 else v_book end,
332
+ case when v_side_credit then v_book else 0 end,
333
+ r.tax_id, true,
334
+ case when v_cash then null else p.declaration_box end,
335
+ v_box_amount, v_doc.currency_code,
336
+ case when v_foreign then v_amount end);
337
+ continue;
338
+ end if;
339
+
340
+ -- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
341
+ -- lines it taxes, split in proportion to their base. The last share
342
+ -- takes whatever is left, so the shares add up to the amount that was
343
+ -- rounded once on the group and the entry still balances to the cent.
344
+ v_left := v_amount;
345
+ v_left_box := v_box_amount;
346
+
347
+ for g in
348
+ select account_id,
349
+ base_amount,
350
+ seq,
351
+ sum(base_amount) over () as total_base,
352
+ row_number() over (order by seq) = count(*) over () as is_last
353
+ from (
354
+ select l.account_id,
355
+ sum(l.amount_untaxed) as base_amount,
356
+ min(l.sequence) as seq
357
+ from document_lines l
358
+ where l.document_id = p_document_id
359
+ and l.line_type = 'product'
360
+ and l.tax_id = r.tax_id
361
+ group by l.account_id
362
+ having sum(l.amount_untaxed) <> 0
363
+ ) as groups
364
+ order by seq
365
+ loop
366
+ if g.is_last then
367
+ v_share := v_left;
368
+ v_share_box := v_left_box;
369
+ else
370
+ v_share := round(v_amount * g.base_amount / g.total_base, 2);
371
+ v_share_box := case when v_box_amount is null then null
372
+ else round(v_box_amount * g.base_amount / g.total_base, 2) end;
373
+ v_left := v_left - v_share;
374
+ v_left_box := v_left_box - v_share_box;
375
+ end if;
376
+
377
+ if v_share = 0 then
378
+ continue;
379
+ end if;
380
+
381
+ v_seq := v_seq + 10;
382
+ v_share_book := round(v_share / v_rate, 2);
383
+
384
+ -- `tax_line` stays false: the amount is on a base account and belongs
385
+ -- to the base side of the declaration, which is why the Belgian grids
386
+ -- 82 and 83 report it together with the base.
387
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
388
+ debit, credit, tax_id, tax_line,
389
+ declaration_box, box_amount, currency_code, amount_currency)
390
+ values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
391
+ case when v_side_credit then 0 else v_share_book end,
392
+ case when v_side_credit then v_share_book else 0 end,
393
+ r.tax_id, false,
394
+ p.declaration_box, v_share_box, v_doc.currency_code,
395
+ case when v_foreign then v_share end);
396
+ end loop;
397
+ end loop;
398
+ end loop;
399
+
400
+ -- ------------------------------------------------------------ counterpart
401
+ select total_debit - total_credit into v_diff from entries where id = v_entry.id;
402
+ select coalesce(sum(case when l.debit > 0 then l.amount_currency else -l.amount_currency end), 0)
403
+ into v_diff_cur
404
+ from entry_lines l where l.entry_id = v_entry.id;
405
+
406
+ if v_diff = 0 then
407
+ raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
408
+ end if;
409
+
410
+ v_contact := commercial_entity(v_doc.contact_id);
411
+ v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
412
+
413
+ select payment_terms_days into v_terms from contacts where id = v_contact;
414
+ v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
415
+
416
+ v_amount := abs(v_diff);
417
+ -- The counterpart balances the entry in both currencies. The total it is
418
+ -- checked against is the document's own, which is the currency
419
+ -- `amount_total` is stated in.
420
+ v_total_cur := case when v_foreign then abs(v_diff_cur) else v_amount end;
421
+ v_seq := v_seq + 10;
422
+
423
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
424
+ debit, credit, contact_id, date_maturity, currency_code,
425
+ amount_currency)
426
+ values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
427
+ case when v_diff > 0 then 0 else v_amount end,
428
+ case when v_diff > 0 then v_amount else 0 end,
429
+ v_contact, v_maturity, v_doc.currency_code,
430
+ case when v_foreign then v_total_cur end);
431
+
432
+ -- The ledger is right by construction. If the header disagrees, the header
433
+ -- is what is wrong, and we say so instead of quietly patching a line.
434
+ if abs(v_total_cur - abs(v_doc.amount_total)) > 0.005 then
435
+ raise exception 'document_total_mismatch: document % totals % but its lines book %',
436
+ p_document_id, v_doc.amount_total, v_total_cur;
437
+ end if;
438
+
439
+ -- --------------------------------------------------------------- posting
440
+ v_entry := post_entry(v_entry.id);
441
+
442
+ update documents
443
+ set state = 'posted',
444
+ number = coalesce(number, v_entry.number),
445
+ entry_id = v_entry.id,
446
+ accounting_date = v_date
447
+ where id = p_document_id;
448
+
449
+ return v_entry;
450
+ end;
451
+ $$;
452
+
453
+ revoke execute on all functions in schema public from public;
@@ -0,0 +1,79 @@
1
+ -- Ekwo OS — "not payable" is not the same as "receivable".
2
+ --
3
+ -- `aged_balance(company, at, group)` tested the group twice, both times as
4
+ -- `case when p_group = 'payable' then … else …`. So every value that is not
5
+ -- exactly `'payable'` was silently the receivable ageing: `'supplier'`,
6
+ -- `'creditors'`, `'Payable'`, a typo, a column name passed by mistake. The
7
+ -- caller gets a full, plausible, wrong report — the one failure mode a report
8
+ -- must never have, because nothing about it looks like an error.
9
+ --
10
+ -- Two named groups, and anything else is refused by name. The body is
11
+ -- unchanged; it moves to plpgsql only so that it can raise.
12
+
13
+ create or replace function aged_balance(
14
+ p_company_id uuid,
15
+ p_at date default current_date,
16
+ p_group text default 'receivable'
17
+ )
18
+ returns table (
19
+ contact_id uuid,
20
+ contact_name text,
21
+ account_id uuid,
22
+ account_code text,
23
+ not_due numeric,
24
+ days_1_30 numeric,
25
+ days_31_60 numeric,
26
+ days_61_90 numeric,
27
+ days_over_90 numeric,
28
+ total numeric
29
+ )
30
+ language plpgsql
31
+ stable
32
+ as $$
33
+ begin
34
+ if p_group is null or p_group not in ('receivable', 'payable') then
35
+ raise exception 'invalid_group: % is not an ageing group; it is receivable or payable', coalesce(p_group, 'null');
36
+ end if;
37
+
38
+ return query
39
+ with open_lines as (
40
+ select l.contact_id,
41
+ l.account_id,
42
+ case when p_group = 'payable' then -l.balance else l.balance end
43
+ * (case when abs(l.balance) = 0 then 0
44
+ else (abs(l.balance) - l.matched_amount) / abs(l.balance) end) as residual,
45
+ coalesce(l.date_maturity, e.entry_date) as due_date
46
+ from entry_lines l
47
+ join entries e on e.id = l.entry_id
48
+ join accounts a on a.id = l.account_id
49
+ where l.company_id = p_company_id
50
+ and e.state = 'posted'
51
+ and e.entry_date <= p_at
52
+ and a.reconcilable
53
+ and a.account_type = case when p_group = 'payable'
54
+ then 'liability_payable'::account_type
55
+ else 'asset_receivable'::account_type end
56
+ and abs(l.balance) - l.matched_amount > 0.005
57
+ )
58
+ select o.contact_id,
59
+ c.name,
60
+ o.account_id,
61
+ a.code,
62
+ round(sum(o.residual) filter (where o.due_date >= p_at), 2),
63
+ round(sum(o.residual) filter (where p_at - o.due_date between 1 and 30), 2),
64
+ round(sum(o.residual) filter (where p_at - o.due_date between 31 and 60), 2),
65
+ round(sum(o.residual) filter (where p_at - o.due_date between 61 and 90), 2),
66
+ round(sum(o.residual) filter (where p_at - o.due_date > 90), 2),
67
+ round(sum(o.residual), 2)
68
+ from open_lines o
69
+ left join contacts c on c.id = o.contact_id
70
+ join accounts a on a.id = o.account_id
71
+ group by o.contact_id, c.name, o.account_id, a.code
72
+ order by c.name nulls last;
73
+ end;
74
+ $$;
75
+
76
+ comment on function aged_balance(uuid, date, text) is
77
+ 'Ageing of what is still open, read from the ledger and from the matching. Two groups, receivable and payable; anything else is refused by name rather than reported as receivable.';
78
+
79
+ revoke execute on all functions in schema public from public;