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,776 @@
1
+ -- Ekwo OS — one tax engine, several kinds.
2
+ --
3
+ -- The core knew one tax: European VAT, fully deductible, computed on a price
4
+ -- that excludes it, rounded to the cent. Everything else a country does was
5
+ -- unsayable, and the pack format already had words for it: `kind`,
6
+ -- `recoverable`, `price_include`, `jurisdiction`, `cash_basis`,
7
+ -- `rounding_method` and `cash_rounding_unit` are in `packs/schema/pack.1.json`
8
+ -- today, marked *deferred*, and the compiler drops them on the floor. This
9
+ -- migration gives them a column, so the pack stops promising what the schema
10
+ -- cannot hold.
11
+ --
12
+ -- What each one is for, and what it is *not*:
13
+ --
14
+ -- `tax_kind` vat | gst | sales_tax | withholding | other. A **label that
15
+ -- drives the reports**, never the calculation. GST is
16
+ -- computed exactly like VAT; a report that lists "the VAT" of
17
+ -- a Canadian company needs to know which of two taxes it is
18
+ -- looking at, and guessing from the code is how a localisation
19
+ -- ends up in application code. Default `vat`, which is what
20
+ -- every tax of Belgium and France is.
21
+ -- `recoverable` false for an American sales tax and for a Canadian PST: the
22
+ -- buyer never gets it back. Documentation and a report filter;
23
+ -- the *ledger* consequence of non-recoverability is the
24
+ -- `tax_on_base` posting below, which says where the money
25
+ -- goes rather than merely that it is lost.
26
+ -- `price_include` the unit price already holds the tax (UK and Australian
27
+ -- retail). `taxes` had it since day one and `tax_templates`
28
+ -- did not, so a pack could not express it — a column missing
29
+ -- on one side of a copy. Added here, nothing reads it yet;
30
+ -- the gross-to-net computation belongs with the country that
31
+ -- needs it.
32
+ -- `jurisdiction` ISO 3166-2 *with* the country prefix — `CA-QC`, `US-CA` —
33
+ -- because a sales tax is levied by a state, not by a country.
34
+ -- Null in Europe. Distinct from `companies.region`, which is
35
+ -- where a *party* sits and carries no prefix.
36
+ -- `cash_basis` and `cash_basis_transition_account_*`: **columns only, and
37
+ -- deliberately unread**. The tax falls due when the invoice
38
+ -- is paid — French VAT on services, the British cash
39
+ -- accounting scheme — and the amount waits on a transition
40
+ -- account until then. P0-6 implements the behaviour; putting
41
+ -- the columns here means `taxes` and `tax_postings` migrate
42
+ -- once rather than twice, which is the same argument
43
+ -- `20260912080311` made for `report_code`.
44
+ --
45
+ -- And on the country model, two columns the manifest already declares:
46
+ --
47
+ -- `rounding_method` half_up | half_even | down | up. Belgium and France
48
+ -- both round half away from zero, per tax group, which is
49
+ -- what `round()` on a numeric does and what `post_document`
50
+ -- has always done — so `half_up` is today's behaviour written
51
+ -- down, not a change. Half-even is the Anglo-Saxon banker's
52
+ -- rounding.
53
+ -- `cash_rounding_unit` the smallest coin, when it is not the cent: 0.05 in
54
+ -- Switzerland, where an invoice total is rounded to the
55
+ -- nearest five centimes and the difference goes to the
56
+ -- rounding account. 0 means the cent is the unit, which is
57
+ -- every country of phase 0.
58
+ --
59
+ -- These two, like `cash_basis`, have no reader yet. That is the third and
60
+ -- last time this schema takes that liberty (`region` was the first), and for
61
+ -- the reason `20260912080311` set out: the alternative is migrating a table
62
+ -- of tax rows twice. `post_document` is unchanged for every tax that exists
63
+ -- today, and the tests that prove it are untouched.
64
+ --
65
+ -- ---------------------------------------------------------------------------
66
+ -- The one behaviour this migration does add: `tax_on_base`
67
+ -- ---------------------------------------------------------------------------
68
+ --
69
+ -- Non-deductible VAT is not a claim on the State, so it cannot sit on a VAT
70
+ -- account. It is a cost, and it follows the account of the thing that was
71
+ -- bought. A Belgian company car, 1 000 € at 21 %, deduction capped at 50 % by
72
+ -- art. 45 § 2 CTVA:
73
+ --
74
+ -- 242000 Matériel roulant debit 1 000.00 box 83 1 000.00
75
+ -- 411000 TVA déductible debit 105.00 box 59 105.00
76
+ -- 242000 TVA non déductible debit 105.00 box 83 105.00
77
+ -- 440000 Fournisseur credit 1 210.00
78
+ --
79
+ -- The supplier is owed the full 1 210 €; half the VAT is recovered and half
80
+ -- is part of what the car cost. Grid 83 comes out at 1 105 €, because the
81
+ -- Belgian return asks for the base *plus* the non-deductible VAT — the form
82
+ -- says « montant (TVA déductible non comprise) », which excludes the
83
+ -- deductible VAT and not all of it. That falls out of `box_factor_percent`
84
+ -- being independent from `factor_percent`, with no new column: the base
85
+ -- posting reports 100 % of the base in box 83 and the `tax_on_base` posting
86
+ -- reports 50 % of the tax in the same box.
87
+ --
88
+ -- Three rules the posting keeps:
89
+ --
90
+ -- * **It carries no account**, exactly like `base`, and for the same
91
+ -- reason: the account is the one the document line names. The check
92
+ -- constraint says so.
93
+ -- * **It is split across the accounts of the lines it taxes**, in
94
+ -- proportion to their base, with the last share carrying the rounding
95
+ -- difference. An invoice with a car and its insurance on two accounts
96
+ -- gets the non-deductible VAT on both, and the entry still balances to
97
+ -- the cent because the shares sum to an amount that was rounded **once**,
98
+ -- on the tax group (EN 16931 BR-CO-14, unchanged).
99
+ -- * **`tax_line` stays false.** The amount lands on a base account and
100
+ -- belongs to the base side of the grid — which is precisely why Belgium
101
+ -- puts it in 82/83. Marking it as a tax line would split grid 83 into two
102
+ -- rows in `vat_return()` and would tell every reader that filters on
103
+ -- `tax_line` to go looking for it among the VAT accounts, where it is not.
104
+ --
105
+ -- A fully non-deductible tax is the same thing at 100 %: one `tax_on_base`
106
+ -- posting, no `tax` posting, and `recoverable = false` to say it in one word.
107
+
108
+ -- ---------------------------------------------------------------------------
109
+ -- tax_kind
110
+ -- ---------------------------------------------------------------------------
111
+
112
+ do $$
113
+ begin
114
+ if not exists (select 1 from pg_type where typname = 'tax_kind') then
115
+ create type tax_kind as enum ('vat', 'gst', 'sales_tax', 'withholding', 'other');
116
+ end if;
117
+ end;
118
+ $$;
119
+
120
+ comment on type tax_kind is
121
+ 'What kind of tax this is. Drives the reports and nothing else: a GST is computed exactly like a VAT.';
122
+
123
+ -- ---------------------------------------------------------------------------
124
+ -- rounding_method
125
+ -- ---------------------------------------------------------------------------
126
+
127
+ do $$
128
+ begin
129
+ if not exists (select 1 from pg_type where typname = 'rounding_method') then
130
+ create type rounding_method as enum ('half_up', 'half_even', 'down', 'up');
131
+ end if;
132
+ end;
133
+ $$;
134
+
135
+ comment on type rounding_method is
136
+ 'How a country rounds a tax amount. half_up is half away from zero, which is what Postgres round() does and what every posting has always used.';
137
+
138
+ -- ---------------------------------------------------------------------------
139
+ -- The reference taxes
140
+ -- ---------------------------------------------------------------------------
141
+
142
+ alter table tax_templates
143
+ add column if not exists tax_kind tax_kind not null default 'vat',
144
+ add column if not exists recoverable boolean not null default true,
145
+ add column if not exists jurisdiction text,
146
+ add column if not exists price_include boolean not null default false,
147
+ add column if not exists cash_basis boolean not null default false,
148
+ add column if not exists cash_basis_transition_account_code text;
149
+
150
+ comment on column tax_templates.tax_kind is
151
+ 'vat, gst, sales_tax, withholding, other. A label for the reports, never an input to the calculation.';
152
+ comment on column tax_templates.recoverable is
153
+ 'False when the buyer never gets the tax back: American sales tax, Canadian PST. Where it lands is said by a tax_on_base posting.';
154
+ comment on column tax_templates.jurisdiction is
155
+ 'ISO 3166-2 with the country prefix — CA-QC, US-CA — for a tax levied by a state. Null in Europe.';
156
+ comment on column tax_templates.price_include is
157
+ 'The unit price already holds the tax (UK and Australian retail). `taxes` carried this from the start and the template did not.';
158
+ comment on column tax_templates.cash_basis is
159
+ 'The tax falls due when the invoice is paid. Column only: P0-6 implements the behaviour, this migration just stops the pack from losing the value.';
160
+ comment on column tax_templates.cash_basis_transition_account_code is
161
+ 'Account the tax waits on until the invoice is paid. Column only, read by P0-6.';
162
+
163
+ alter table taxes
164
+ add column if not exists tax_kind tax_kind not null default 'vat',
165
+ add column if not exists recoverable boolean not null default true,
166
+ add column if not exists jurisdiction text,
167
+ add column if not exists cash_basis boolean not null default false,
168
+ add column if not exists cash_basis_transition_account_id uuid references accounts(id) on delete restrict;
169
+
170
+ comment on column taxes.tax_kind is
171
+ 'vat, gst, sales_tax, withholding, other. A label for the reports, never an input to the calculation.';
172
+ comment on column taxes.recoverable is
173
+ 'False when the buyer never gets the tax back. The ledger consequence is a tax_on_base posting, not this column.';
174
+ comment on column taxes.jurisdiction is
175
+ 'ISO 3166-2 with the country prefix, for a tax levied by a state. Null in Europe.';
176
+ comment on column taxes.cash_basis is
177
+ 'The tax falls due when the invoice is paid. Column only until P0-6.';
178
+ comment on column taxes.cash_basis_transition_account_id is
179
+ 'Account the tax waits on until the invoice is paid. Column only until P0-6.';
180
+
181
+ -- A tax belongs to a company and so does the account it points at; the
182
+ -- composite key is how every other reference in this schema says it.
183
+ alter table taxes
184
+ add constraint taxes_cash_basis_account_company
185
+ foreign key (cash_basis_transition_account_id, company_id) references accounts(id, company_id);
186
+
187
+ -- ---------------------------------------------------------------------------
188
+ -- Which posting type carries an account
189
+ --
190
+ -- Two constraints said it before, one per type, and neither had an opinion
191
+ -- about a type that did not exist. `tax_on_base` must carry no account — the
192
+ -- account is the line's — so the rule is now stated once, for every value of
193
+ -- the enum, and a value added later has to come back here rather than slip
194
+ -- through. This is the promised replacement of `tax_has_account`: a `tax`
195
+ -- still needs an account, and `tax_on_base` is exempt.
196
+ -- ---------------------------------------------------------------------------
197
+
198
+ alter table tax_postings
199
+ drop constraint if exists tax_postings_base_has_no_account,
200
+ drop constraint if exists tax_postings_tax_has_account;
201
+
202
+ alter table tax_postings
203
+ add constraint tax_postings_account_by_type check (
204
+ case posting_type when 'tax' then account_id is not null else account_id is null end
205
+ );
206
+
207
+ alter table tax_posting_templates
208
+ drop constraint if exists tax_posting_templates_base_has_no_account,
209
+ drop constraint if exists tax_posting_templates_tax_has_account;
210
+
211
+ alter table tax_posting_templates
212
+ add constraint tax_posting_templates_account_by_type check (
213
+ case posting_type when 'tax' then account_code is not null else account_code is null end
214
+ );
215
+
216
+ -- ---------------------------------------------------------------------------
217
+ -- The country model
218
+ -- ---------------------------------------------------------------------------
219
+
220
+ alter table country_defaults
221
+ add column if not exists rounding_method rounding_method not null default 'half_up',
222
+ add column if not exists cash_rounding_unit numeric(8, 4) not null default 0;
223
+
224
+ comment on column country_defaults.rounding_method is
225
+ 'How this country rounds a tax amount. half_up is what post_document does today, in every country, so the default changes nothing.';
226
+ comment on column country_defaults.cash_rounding_unit is
227
+ 'Smallest coin when it is not the cent: 0.05 in Switzerland. 0 means the cent, which is every country of phase 0.';
228
+
229
+ alter table country_defaults
230
+ add constraint country_defaults_cash_rounding_unit_positive check (cash_rounding_unit >= 0);
231
+
232
+ -- ---------------------------------------------------------------------------
233
+ -- document_tax_summary — what the other party actually pays
234
+ --
235
+ -- `tax_charged` sums the accounting factors of the *tax* postings, so a tax
236
+ -- whose postings net to zero adds nothing to the document total. That is what
237
+ -- makes a self-assessed purchase come out at the net amount. A `tax_on_base`
238
+ -- posting is money that leaves the company exactly like a `tax` posting — the
239
+ -- supplier of a company car is owed 1 210 € and not 1 105 € — so it counts
240
+ -- here too. Without this line the document total would be short by the
241
+ -- non-deductible half and `post_document` would refuse the document with
242
+ -- `document_total_mismatch`, which is at least an honest failure.
243
+ -- ---------------------------------------------------------------------------
244
+
245
+ create or replace view document_tax_summary
246
+ with (security_invoker = true) as
247
+ select l.document_id,
248
+ l.company_id,
249
+ d.doc_type,
250
+ l.tax_id,
251
+ t.code as tax_code,
252
+ t.name as tax_name,
253
+ t.vat_category,
254
+ t.amount as tax_rate,
255
+ sum(l.amount_untaxed) as base_amount,
256
+ -- Gross tax: what the VAT return reports.
257
+ round(sum(l.amount_untaxed) * coalesce(t.amount, 0) / 100, 2) as tax_amount,
258
+ -- Charged tax: what the other party actually pays. Zero when the tax
259
+ -- postings net out, which is exactly what self-assessment means.
260
+ round(
261
+ sum(l.amount_untaxed) * coalesce(t.amount, 0) / 100
262
+ * coalesce((
263
+ select sum(tp.factor_percent)
264
+ from tax_postings tp
265
+ where tp.tax_id = t.id
266
+ and tp.posting_type in ('tax', 'tax_on_base')
267
+ and tp.document_kind = case
268
+ when d.doc_type in ('sale_credit_note', 'purchase_credit_note')
269
+ then 'credit_note'::tax_document_kind
270
+ else 'invoice'::tax_document_kind
271
+ end
272
+ ), 100) / 100,
273
+ 2) as tax_charged
274
+ from document_lines l
275
+ join documents d on d.id = l.document_id
276
+ left join taxes t on t.id = l.tax_id
277
+ where l.line_type = 'product'
278
+ group by l.document_id, l.company_id, d.doc_type, l.tax_id,
279
+ t.id, t.code, t.name, t.vat_category, t.amount;
280
+
281
+ comment on view document_tax_summary is
282
+ 'VAT breakdown of a document, one row per tax, rounded on the group basis (EN 16931 BR-CO-14).';
283
+
284
+ -- ---------------------------------------------------------------------------
285
+ -- post_document — the same function, plus one posting type
286
+ --
287
+ -- Everything about an existing tax is byte for byte what it was: the base
288
+ -- loop, the counterpart, the rounding, the refusals. The tax loop now also
289
+ -- reads `tax_on_base` postings, and for those it writes the amount on the
290
+ -- accounts of the lines instead of on an account of its own.
291
+ -- ---------------------------------------------------------------------------
292
+
293
+ create or replace function post_document(p_document_id uuid)
294
+ returns entries
295
+ language plpgsql
296
+ as $$
297
+ declare
298
+ v_doc documents%rowtype;
299
+ v_entry entries%rowtype;
300
+ v_journal uuid;
301
+ v_date date;
302
+ v_is_sale boolean;
303
+ v_is_credit boolean;
304
+ v_kind tax_document_kind;
305
+ v_base_credit boolean;
306
+ v_seq integer := 0;
307
+ v_contact uuid;
308
+ v_maturity date;
309
+ v_terms smallint;
310
+ v_counterpart uuid;
311
+ v_diff numeric(16, 2);
312
+ v_amount numeric(16, 2);
313
+ v_box_amount numeric(16, 2);
314
+ v_share numeric(16, 2);
315
+ v_share_box numeric(16, 2);
316
+ v_left numeric(16, 2);
317
+ v_left_box numeric(16, 2);
318
+ v_side_left numeric(16, 2);
319
+ v_side_left_neg numeric(16, 2);
320
+ v_side_credit boolean;
321
+ v_label text;
322
+ r record;
323
+ p record;
324
+ g record;
325
+ begin
326
+ select * into v_doc from documents where id = p_document_id for update;
327
+ if not found then
328
+ raise exception 'unknown_document: document % does not exist', p_document_id;
329
+ end if;
330
+ if v_doc.state = 'posted' then
331
+ raise exception 'document_already_posted: document % is already posted', p_document_id;
332
+ end if;
333
+ if v_doc.state = 'cancelled' then
334
+ raise exception 'document_cancelled: document % cannot be posted', p_document_id;
335
+ end if;
336
+ if v_doc.entry_id is not null then
337
+ raise exception 'document_already_booked: document % already points at entry %',
338
+ p_document_id, v_doc.entry_id;
339
+ end if;
340
+
341
+ if v_doc.doc_type in ('sale_quote', 'purchase_order') then
342
+ raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
343
+ end if;
344
+
345
+ v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
346
+ v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
347
+ v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
348
+ -- Sale invoice and purchase credit note credit the base; the other two debit it.
349
+ v_base_credit := (v_is_sale <> v_is_credit);
350
+
351
+ v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
352
+
353
+ if not exists (
354
+ select 1 from document_lines
355
+ where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
356
+ ) then
357
+ raise exception 'document_empty: document % has no billable line', p_document_id;
358
+ end if;
359
+
360
+ -- A fixed-amount tax has no basis to spread over lines; refuse rather than
361
+ -- guess.
362
+ if exists (
363
+ select 1 from document_lines l join taxes t on t.id = l.tax_id
364
+ where l.document_id = p_document_id and t.amount_type <> 'percent'
365
+ ) then
366
+ raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
367
+ end if;
368
+
369
+ -- Every tax used must be in force on the accounting date.
370
+ for r in
371
+ select distinct t.id, t.code, t.valid_from, t.valid_to
372
+ from document_lines l join taxes t on t.id = l.tax_id
373
+ where l.document_id = p_document_id
374
+ loop
375
+ if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
376
+ raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
377
+ end if;
378
+ end loop;
379
+
380
+ -- Totals are derived; make sure they reflect the lines as they stand now.
381
+ perform documents_refresh_totals(p_document_id);
382
+ select * into v_doc from documents where id = p_document_id;
383
+
384
+ v_journal := coalesce(
385
+ v_doc.journal_id,
386
+ case when v_is_sale
387
+ then (select sales_journal_id from companies where id = v_doc.company_id)
388
+ else (select purchase_journal_id from companies where id = v_doc.company_id)
389
+ end
390
+ );
391
+ if v_journal is null then
392
+ raise exception 'no_journal: set journal_id on the document or a default journal on the company';
393
+ end if;
394
+
395
+ perform assert_period_open(v_doc.company_id, v_date, true);
396
+
397
+ v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
398
+
399
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
400
+ description, state, document_id, currency_code)
401
+ values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
402
+ coalesce(v_doc.number, v_doc.supplier_reference),
403
+ v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
404
+ then ' / ' || v_doc.supplier_reference else '' end,
405
+ 'draft', p_document_id, v_doc.currency_code)
406
+ returning * into v_entry;
407
+
408
+ -- ------------------------------------------------------------------ bases
409
+ for r in
410
+ select l.account_id,
411
+ l.tax_id,
412
+ sum(l.amount_untaxed) as base_amount,
413
+ min(l.sequence) as seq,
414
+ string_agg(distinct l.name, ', ') as label
415
+ from document_lines l
416
+ where l.document_id = p_document_id
417
+ and l.line_type = 'product'
418
+ group by l.account_id, l.tax_id
419
+ having sum(l.amount_untaxed) <> 0
420
+ order by 4
421
+ loop
422
+ select tp.declaration_box, tp.factor_percent, tp.box_factor_percent
423
+ into p
424
+ from tax_postings tp
425
+ where tp.tax_id = r.tax_id
426
+ and tp.document_kind = v_kind
427
+ and tp.posting_type = 'base'
428
+ limit 1;
429
+
430
+ v_amount := round(r.base_amount * coalesce(p.factor_percent, 100) / 100, 2);
431
+ v_seq := v_seq + 10;
432
+
433
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
434
+ debit, credit, tax_id, tax_line,
435
+ declaration_box, box_amount, currency_code)
436
+ values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
437
+ case when v_base_credit then 0 else v_amount end,
438
+ case when v_base_credit then v_amount else 0 end,
439
+ r.tax_id, false,
440
+ p.declaration_box,
441
+ case when p.declaration_box is null then null
442
+ else round(r.base_amount * coalesce(p.box_factor_percent, 100) / 100, 2) end,
443
+ v_doc.currency_code);
444
+ end loop;
445
+
446
+ -- ------------------------------------------------------------------ taxes
447
+ for r in
448
+ select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
449
+ from document_tax_summary s
450
+ where s.document_id = p_document_id
451
+ and s.tax_id is not null
452
+ and s.tax_amount <> 0
453
+ order by s.tax_code
454
+ loop
455
+ -- The postings of one side share out the tax of the group; the last of
456
+ -- each side takes what is left. Until `tax_on_base` there was never more
457
+ -- than one posting per side, so this changes no existing tax by a cent —
458
+ -- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
459
+ -- where rounding each half on its own would book 0.64 against a document
460
+ -- that totals 0.63.
461
+ v_side_left := null;
462
+ v_side_left_neg := null;
463
+
464
+ for p in
465
+ select tp.posting_type, tp.factor_percent, tp.account_id,
466
+ tp.declaration_box, tp.box_factor_percent,
467
+ case when tp.factor_percent >= 0 then 1 else -1 end as side,
468
+ sum(abs(tp.factor_percent))
469
+ over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
470
+ as side_factor,
471
+ row_number() over (
472
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end
473
+ order by tp.sequence, tp.id)
474
+ = count(*) over (
475
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end)
476
+ as is_last_of_side
477
+ from tax_postings tp
478
+ where tp.tax_id = r.tax_id
479
+ and tp.document_kind = v_kind
480
+ and tp.posting_type in ('tax', 'tax_on_base')
481
+ order by tp.sequence, tp.id
482
+ loop
483
+ -- The tax of the group was rounded once, in the view. Every posting is
484
+ -- a share of that one figure, never of a re-derived one.
485
+ if p.side >= 0 then
486
+ if v_side_left is null then
487
+ v_side_left := round(r.tax_amount * p.side_factor / 100, 2);
488
+ end if;
489
+ if p.is_last_of_side then
490
+ v_amount := v_side_left;
491
+ else
492
+ v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
493
+ v_side_left := v_side_left - v_amount;
494
+ end if;
495
+ else
496
+ if v_side_left_neg is null then
497
+ v_side_left_neg := round(r.tax_amount * p.side_factor / 100, 2);
498
+ end if;
499
+ if p.is_last_of_side then
500
+ v_amount := v_side_left_neg;
501
+ else
502
+ v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
503
+ v_side_left_neg := v_side_left_neg - v_amount;
504
+ end if;
505
+ end if;
506
+
507
+ if v_amount = 0 then
508
+ continue;
509
+ end if;
510
+ -- A positive factor keeps the side of the base, a negative one flips it.
511
+ v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
512
+ -- The box keeps its own rounding: `box_factor_percent` was always
513
+ -- independent from `factor_percent`, because a declaration figure is
514
+ -- not a ledger figure and only the ledger has to balance.
515
+ v_box_amount := case when p.declaration_box is null then null
516
+ else round(r.tax_amount * p.box_factor_percent / 100, 2) end;
517
+
518
+ if p.posting_type = 'tax' then
519
+ v_seq := v_seq + 10;
520
+
521
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
522
+ debit, credit, tax_id, tax_line,
523
+ declaration_box, box_amount, currency_code)
524
+ values (v_entry.id, v_doc.company_id, p.account_id, v_seq, r.tax_name,
525
+ case when v_side_credit then 0 else v_amount end,
526
+ case when v_side_credit then v_amount else 0 end,
527
+ r.tax_id, true,
528
+ p.declaration_box, v_box_amount, v_doc.currency_code);
529
+ continue;
530
+ end if;
531
+
532
+ -- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
533
+ -- lines it taxes, split in proportion to their base. The last share
534
+ -- takes whatever is left, so the shares add up to the amount that was
535
+ -- rounded once on the group and the entry still balances to the cent.
536
+ v_left := v_amount;
537
+ v_left_box := v_box_amount;
538
+
539
+ for g in
540
+ select account_id,
541
+ base_amount,
542
+ seq,
543
+ sum(base_amount) over () as total_base,
544
+ row_number() over (order by seq) = count(*) over () as is_last
545
+ from (
546
+ select l.account_id,
547
+ sum(l.amount_untaxed) as base_amount,
548
+ min(l.sequence) as seq
549
+ from document_lines l
550
+ where l.document_id = p_document_id
551
+ and l.line_type = 'product'
552
+ and l.tax_id = r.tax_id
553
+ group by l.account_id
554
+ having sum(l.amount_untaxed) <> 0
555
+ ) as groups
556
+ order by seq
557
+ loop
558
+ if g.is_last then
559
+ v_share := v_left;
560
+ v_share_box := v_left_box;
561
+ else
562
+ v_share := round(v_amount * g.base_amount / g.total_base, 2);
563
+ v_share_box := case when v_box_amount is null then null
564
+ else round(v_box_amount * g.base_amount / g.total_base, 2) end;
565
+ v_left := v_left - v_share;
566
+ v_left_box := v_left_box - v_share_box;
567
+ end if;
568
+
569
+ if v_share = 0 then
570
+ continue;
571
+ end if;
572
+
573
+ v_seq := v_seq + 10;
574
+
575
+ -- `tax_line` stays false: the amount is on a base account and belongs
576
+ -- to the base side of the declaration, which is why the Belgian grids
577
+ -- 82 and 83 report it together with the base.
578
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
579
+ debit, credit, tax_id, tax_line,
580
+ declaration_box, box_amount, currency_code)
581
+ values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
582
+ case when v_side_credit then 0 else v_share end,
583
+ case when v_side_credit then v_share else 0 end,
584
+ r.tax_id, false,
585
+ p.declaration_box, v_share_box, v_doc.currency_code);
586
+ end loop;
587
+ end loop;
588
+ end loop;
589
+
590
+ -- ------------------------------------------------------------ counterpart
591
+ select total_debit - total_credit into v_diff from entries where id = v_entry.id;
592
+
593
+ if v_diff = 0 then
594
+ raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
595
+ end if;
596
+
597
+ v_contact := commercial_entity(v_doc.contact_id);
598
+ v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
599
+
600
+ select payment_terms_days into v_terms from contacts where id = v_contact;
601
+ v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
602
+
603
+ v_amount := abs(v_diff);
604
+ v_seq := v_seq + 10;
605
+
606
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
607
+ debit, credit, contact_id, date_maturity, currency_code)
608
+ values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
609
+ case when v_diff > 0 then 0 else v_amount end,
610
+ case when v_diff > 0 then v_amount else 0 end,
611
+ v_contact, v_maturity, v_doc.currency_code);
612
+
613
+ -- The ledger is right by construction. If the header disagrees, the header
614
+ -- is what is wrong, and we say so instead of quietly patching a line.
615
+ if abs(v_amount - abs(v_doc.amount_total)) > 0.005 then
616
+ raise exception 'document_total_mismatch: document % totals % but its lines book %',
617
+ p_document_id, v_doc.amount_total, v_amount;
618
+ end if;
619
+
620
+ -- --------------------------------------------------------------- posting
621
+ v_entry := post_entry(v_entry.id);
622
+
623
+ update documents
624
+ set state = 'posted',
625
+ number = coalesce(number, v_entry.number),
626
+ entry_id = v_entry.id,
627
+ accounting_date = v_date
628
+ where id = p_document_id;
629
+
630
+ return v_entry;
631
+ end;
632
+ $$;
633
+
634
+ comment on function post_document(uuid) is
635
+ 'Books a document: base lines, tax lines from tax_postings — including the non-deductible share, which lands on the accounts of the lines — and a counterpart that balances by construction.';
636
+
637
+ -- ---------------------------------------------------------------------------
638
+ -- install_country_template carries the new columns into a company
639
+ -- ---------------------------------------------------------------------------
640
+
641
+ create or replace function install_country_template(
642
+ p_company_id uuid,
643
+ p_country char(2),
644
+ p_language char(2) default null
645
+ )
646
+ returns void
647
+ language plpgsql
648
+ as $$
649
+ declare
650
+ v_defaults country_defaults%rowtype;
651
+ r record;
652
+ v_tax_id uuid;
653
+ v_language char(2);
654
+ v_version text;
655
+ begin
656
+ if not exists (select 1 from companies where id = p_company_id) then
657
+ raise exception 'unknown_company: %', p_company_id;
658
+ end if;
659
+ if not exists (select 1 from account_templates where country = p_country) then
660
+ raise exception 'unknown_country_template: no chart of accounts seeded for %', p_country;
661
+ end if;
662
+
663
+ select coalesce(p_language, c.language) into v_language
664
+ from companies c where c.id = p_company_id;
665
+
666
+ -- 1. Accounts, without the hierarchy.
667
+ insert into accounts (company_id, code, name, name_i18n, statement_hint,
668
+ account_type, reconcilable)
669
+ select p_company_id, t.code,
670
+ coalesce(nullif(t.name_i18n ->> v_language, ''), t.name),
671
+ t.name_i18n, t.statement_hint, t.account_type, t.reconcilable
672
+ from account_templates t
673
+ where t.country = p_country
674
+ on conflict (company_id, code) do nothing;
675
+
676
+ -- 2. Hierarchy, now that every code exists.
677
+ update accounts a
678
+ set parent_id = p.id
679
+ from account_templates t
680
+ join accounts p on p.company_id = p_company_id and p.code = t.parent_code
681
+ where a.company_id = p_company_id
682
+ and a.code = t.code
683
+ and t.country = p_country
684
+ and t.parent_code is not null
685
+ and a.parent_id is null;
686
+
687
+ -- 3. Journals.
688
+ insert into journals (company_id, code, name, journal_type)
689
+ select p_company_id, t.code, t.name, t.journal_type
690
+ from journal_templates t
691
+ where t.country = p_country
692
+ on conflict (company_id, code) do nothing;
693
+
694
+ -- 4. Taxes and their postings, each posting keeping the form its box is on.
695
+ for r in
696
+ select * from tax_templates where country = p_country order by sequence, code
697
+ loop
698
+ v_tax_id := null;
699
+ insert into taxes (company_id, code, name, description, amount_type, amount,
700
+ applies_to, treatment, country, valid_from, valid_to,
701
+ legal_reference, vat_category, exemption_code, sequence,
702
+ tax_kind, recoverable, jurisdiction, price_include,
703
+ cash_basis, cash_basis_transition_account_id)
704
+ values (p_company_id, r.code, r.name, r.description, r.amount_type, r.amount,
705
+ r.applies_to, r.treatment, p_country, r.valid_from, r.valid_to,
706
+ r.legal_reference, r.vat_category, r.exemption_code, r.sequence,
707
+ r.tax_kind, r.recoverable, r.jurisdiction, r.price_include,
708
+ r.cash_basis, account_id_by_code(p_company_id, r.cash_basis_transition_account_code))
709
+ on conflict (company_id, code) do nothing
710
+ returning id into v_tax_id;
711
+
712
+ if v_tax_id is null then
713
+ continue;
714
+ end if;
715
+
716
+ insert into tax_postings (tax_id, company_id, document_kind, posting_type,
717
+ factor_percent, account_id, declaration_box,
718
+ box_factor_percent, report_code, sequence)
719
+ select v_tax_id, p_company_id, tp.document_kind, tp.posting_type,
720
+ tp.factor_percent, account_id_by_code(p_company_id, tp.account_code),
721
+ tp.declaration_box, tp.box_factor_percent, tp.report_code, tp.sequence
722
+ from tax_posting_templates tp
723
+ where tp.tax_template_id = r.id
724
+ order by tp.sequence;
725
+ end loop;
726
+
727
+ -- 5. Roles.
728
+ select * into v_defaults from country_defaults where country = p_country;
729
+ if found then
730
+ update companies c
731
+ set receivable_account_id = coalesce(c.receivable_account_id, account_id_by_code(p_company_id, v_defaults.receivable_code)),
732
+ payable_account_id = coalesce(c.payable_account_id, account_id_by_code(p_company_id, v_defaults.payable_code)),
733
+ suspense_account_id = coalesce(c.suspense_account_id, account_id_by_code(p_company_id, v_defaults.suspense_code)),
734
+ rounding_account_id = coalesce(c.rounding_account_id, account_id_by_code(p_company_id, v_defaults.rounding_code)),
735
+ retained_earnings_account_id = coalesce(c.retained_earnings_account_id, account_id_by_code(p_company_id, v_defaults.retained_earnings_code)),
736
+ default_sales_account_id = coalesce(c.default_sales_account_id, account_id_by_code(p_company_id, v_defaults.sales_account_code)),
737
+ default_purchase_account_id = coalesce(c.default_purchase_account_id, account_id_by_code(p_company_id, v_defaults.purchase_account_code)),
738
+ sales_journal_id = coalesce(c.sales_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.sales_journal_code)),
739
+ purchase_journal_id = coalesce(c.purchase_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.purchase_journal_code)),
740
+ miscellaneous_journal_id = coalesce(c.miscellaneous_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.misc_journal_code))
741
+ where c.id = p_company_id;
742
+
743
+ update journals j
744
+ set default_account_id = account_id_by_code(p_company_id, v_defaults.bank_account_code)
745
+ where j.company_id = p_company_id
746
+ and j.journal_type = 'bank'
747
+ and j.default_account_id is null
748
+ and v_defaults.bank_account_code is not null;
749
+
750
+ update journals j
751
+ set default_account_id = account_id_by_code(p_company_id, v_defaults.cash_account_code)
752
+ where j.company_id = p_company_id
753
+ and j.journal_type = 'cash'
754
+ and j.default_account_id is null
755
+ and v_defaults.cash_account_code is not null;
756
+ end if;
757
+
758
+ -- 6. What was copied, and from which version.
759
+ v_version := coalesce((select version from country_packs where country = p_country), '1.0.0');
760
+
761
+ insert into company_packs (company_id, country, version)
762
+ values (p_company_id, p_country, v_version)
763
+ on conflict (company_id, country) do update
764
+ set version = excluded.version,
765
+ upgraded_at = now()
766
+ where company_packs.version is distinct from excluded.version;
767
+ end;
768
+ $$;
769
+
770
+ comment on function install_country_template(uuid, char, char) is
771
+ 'Copies a country pack into a company in one language, wires the default roles and the financial journals, and records the pack version in company_packs.';
772
+
773
+ -- A function created now comes out executable by PUBLIC unless this runs, and
774
+ -- on Supabase that is an anonymous RPC endpoint. From PUBLIC only — `anon`
775
+ -- holds explicit grants on the policy helpers. Rule 6 of the README.
776
+ revoke execute on all functions in schema public from public;