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,902 @@
1
+ -- Ekwo OS — a ledger line says which posting wrote it.
2
+ --
3
+ -- `entry_lines` carries `tax_id` and `tax_line`, and between them they cannot
4
+ -- tell a `base` line from a `tax_on_base` line. Both belong to the same tax,
5
+ -- both are booked on a base account, and both have `tax_line = false` — which
6
+ -- is right, because a non-deductible share of tax is a cost and belongs on the
7
+ -- base side of the declaration, and it is exactly why the Belgian grids 82 and
8
+ -- 83 report it together with the base. What it cost is that anything reading
9
+ -- the ledger *by tax* rather than *by box* had to know that one of the two
10
+ -- could not occur.
11
+ --
12
+ -- `ec_sales_list()` is the reader that found it. It takes the lines of a tax
13
+ -- that are not tax lines, which is exact today because an intra-Community
14
+ -- supply is exempt and has no tax to capitalise — and would stop being exact
15
+ -- the day a pack said otherwise, silently, by adding a supply's `tax_on_base`
16
+ -- amount to what the statement declares. `docs/international.md` recorded it
17
+ -- under *A ledger line does not say which posting wrote it*, and proposed
18
+ -- exactly what is here: the posting's own type, copied onto the line beside the
19
+ -- `declaration_box` that is already copied there.
20
+ --
21
+ -- **The vocabulary is `tax_postings`'s own.** `tax_posting_type` — `base`,
22
+ -- `tax`, `tax_on_base` — because the column answers *which posting wrote this
23
+ -- line*, and a posting's type is the only correct answer. A second enum
24
+ -- spelling the same three words would be the same fact written twice.
25
+ --
26
+ -- **A line that is no posting is null, not a fourth word.** The counterpart of
27
+ -- an invoice, the two legs of a payment, the lines of a closing entry: none of
28
+ -- them was written by a tax posting, and null is what a column says about a
29
+ -- question that does not apply to the row. Adding a `none` to
30
+ -- `tax_posting_type` would put a value meaning *not a posting* inside the
31
+ -- vocabulary of what a posting is, and `tax_postings.posting_type` would then
32
+ -- have to refuse it — a constraint existing only to undo an enum value. A
33
+ -- check constraint says the useful half instead: a line with a posting type
34
+ -- has a tax, so the two are never read apart.
35
+ --
36
+ -- Null therefore carries two readings, and `tax_id` separates them: null with
37
+ -- no tax is *not a tax posting*, and null with a tax is *written before this
38
+ -- migration and not identified*. Which brings us to the backfill.
39
+ --
40
+ -- **The backfill only writes what is certain.** A ledger is not rewritten by
41
+ -- deduction. A line is filled when its tax and its declaration box name one
42
+ -- posting type and no other: `tax_postings` is asked which types write a line
43
+ -- of that tax into that box on that side of `tax_line`, and the line is filled
44
+ -- only where the answer is a single word. Everything else — a line whose
45
+ -- posting names no box, the cash-basis lines that wait with their box held back
46
+ -- until settlement, a box two posting types both write to — stays null, and the
47
+ -- `tax_id` beside it says that null means unknown there rather than
48
+ -- inapplicable. Nothing guesses, and nothing that reads this column afterwards
49
+ -- can mistake a guess for a fact.
50
+ --
51
+ -- **Who fills it from now on.** `post_document()`, on all three of the lines a
52
+ -- tax produces, from the posting row it is already reading; and
53
+ -- `settle_cash_basis_tax()`, on both legs of the transfer that makes a
54
+ -- cash-basis tax due — the leg that carries the box and the leg that empties
55
+ -- the account the amount waited on are one posting falling due, so they carry
56
+ -- one posting type. Both functions are replaced whole, because a function is
57
+ -- replaced whole in PostgreSQL, and nothing else in either of them moves.
58
+ --
59
+ -- Nothing else writes a tax line. `post_payment()` books money against a
60
+ -- third-party account, `close_fiscal_year()` and `reopen_fiscal_year()` move
61
+ -- results between accounts the pack names, and `post_module_entry()` takes the
62
+ -- lines a module hands it and knows no tax at all.
63
+
64
+ -- ---------------------------------------------------------------------------
65
+ -- The column
66
+ -- ---------------------------------------------------------------------------
67
+
68
+ alter table entry_lines
69
+ add column if not exists posting_type tax_posting_type;
70
+
71
+ comment on column entry_lines.posting_type is
72
+ 'Which tax posting wrote this line: base, tax, or tax_on_base, from tax_postings.posting_type. Null on a line no tax posting wrote — a counterpart, a payment, a closing line — and null on a line written before this column existed whose posting could not be identified beyond doubt; tax_id tells the two apart. It is what makes a base line and a tax_on_base line of the same tax on the same account distinguishable, which tax_id and tax_line together cannot do.';
73
+
74
+ alter table entry_lines
75
+ add constraint entry_lines_posting_type_has_a_tax check (
76
+ posting_type is null or tax_id is not null
77
+ );
78
+
79
+ -- ---------------------------------------------------------------------------
80
+ -- What the existing ledger can be told for certain
81
+ -- ---------------------------------------------------------------------------
82
+ --
83
+ -- One posting type, or nothing. `writes_tax_line` is derived from the type
84
+ -- rather than stored, because it is: a `tax` posting is the only one that books
85
+ -- a tax line, and `base` and `tax_on_base` both book a line that is not one.
86
+ -- Two postings of the same tax writing the same box on the same side is an
87
+ -- ambiguity nothing on the line can settle, so those lines stay null.
88
+
89
+ with candidate as (
90
+ select distinct
91
+ tp.tax_id,
92
+ tp.declaration_box,
93
+ (tp.posting_type = 'tax'::tax_posting_type) as writes_tax_line,
94
+ tp.posting_type
95
+ from tax_postings tp
96
+ where tp.declaration_box is not null
97
+ ),
98
+ unambiguous as (
99
+ select c.tax_id, c.declaration_box, c.writes_tax_line, min(c.posting_type) as posting_type
100
+ from candidate c
101
+ group by c.tax_id, c.declaration_box, c.writes_tax_line
102
+ having count(*) = 1
103
+ )
104
+ update entry_lines l
105
+ set posting_type = u.posting_type
106
+ from unambiguous u
107
+ where l.tax_id = u.tax_id
108
+ and l.declaration_box = u.declaration_box
109
+ and l.tax_line = u.writes_tax_line
110
+ and l.posting_type is null;
111
+
112
+ -- ---------------------------------------------------------------------------
113
+ -- Everything that writes a tax line, writing it
114
+ -- ---------------------------------------------------------------------------
115
+
116
+ create or replace function post_document(p_document_id uuid)
117
+ returns entries
118
+ language plpgsql
119
+ as $$
120
+ declare
121
+ v_doc documents%rowtype;
122
+ v_entry entries%rowtype;
123
+ v_journal uuid;
124
+ v_date date;
125
+ v_is_sale boolean;
126
+ v_is_credit boolean;
127
+ v_kind tax_document_kind;
128
+ v_base_credit boolean;
129
+ v_seq integer := 0;
130
+ v_contact uuid;
131
+ v_maturity date;
132
+ v_terms smallint;
133
+ v_counterpart uuid;
134
+ -- Plain `numeric`, not `numeric(16, 2)`: a local that carries a scale is a
135
+ -- second rounding rule hiding in a declaration, and it is not the currency's.
136
+ -- The only thing that rounds here is `round_amount`.
137
+ v_diff numeric;
138
+ v_diff_cur numeric;
139
+ v_amount numeric;
140
+ v_book numeric;
141
+ v_box_amount numeric;
142
+ v_share numeric;
143
+ v_share_book numeric;
144
+ v_share_box numeric;
145
+ v_left numeric;
146
+ v_left_box numeric;
147
+ v_side_left numeric;
148
+ v_side_left_neg numeric;
149
+ v_side_credit boolean;
150
+ v_label text;
151
+ v_rate numeric(18, 8);
152
+ v_home char(3);
153
+ v_foreign boolean;
154
+ v_total_cur numeric;
155
+ -- Two currencies, one method: the document is stated in its own and the
156
+ -- ledger keeps the company's, and a yen invoice paid in euros rounds each
157
+ -- side at the decimals that side has.
158
+ v_round money_rounding;
159
+ v_book_round money_rounding;
160
+ v_cash boolean;
161
+ v_transition uuid;
162
+ v_postings integer;
163
+ r record;
164
+ p record;
165
+ g record;
166
+ begin
167
+ select * into v_doc from documents where id = p_document_id for update;
168
+ if not found then
169
+ raise exception 'unknown_document: document % does not exist', p_document_id;
170
+ end if;
171
+ if v_doc.state = 'posted' then
172
+ raise exception 'document_already_posted: document % is already posted', p_document_id;
173
+ end if;
174
+ if v_doc.state = 'cancelled' then
175
+ raise exception 'document_cancelled: document % cannot be posted', p_document_id;
176
+ end if;
177
+ if v_doc.entry_id is not null then
178
+ raise exception 'document_already_booked: document % already points at entry %',
179
+ p_document_id, v_doc.entry_id;
180
+ end if;
181
+
182
+ if v_doc.doc_type in ('sale_quote', 'purchase_order') then
183
+ raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
184
+ end if;
185
+
186
+ v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
187
+ v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
188
+ v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
189
+ -- Sale invoice and purchase credit note credit the base; the other two debit it.
190
+ v_base_credit := (v_is_sale <> v_is_credit);
191
+
192
+ v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
193
+
194
+ if not exists (
195
+ select 1 from document_lines
196
+ where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
197
+ ) then
198
+ raise exception 'document_empty: document % has no billable line', p_document_id;
199
+ end if;
200
+
201
+ -- A fixed-amount tax has no basis to spread over lines; refuse rather than
202
+ -- guess.
203
+ if exists (
204
+ select 1 from document_lines l join taxes t on t.id = l.tax_id
205
+ where l.document_id = p_document_id and t.amount_type <> 'percent'
206
+ ) then
207
+ raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
208
+ end if;
209
+
210
+ -- Every tax used must be in force on the accounting date.
211
+ for r in
212
+ select distinct t.id, t.code, t.valid_from, t.valid_to
213
+ from document_lines l join taxes t on t.id = l.tax_id
214
+ where l.document_id = p_document_id
215
+ loop
216
+ if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
217
+ raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
218
+ end if;
219
+ end loop;
220
+
221
+ -- Totals are derived; make sure they reflect the lines as they stand now.
222
+ perform documents_refresh_totals(p_document_id);
223
+ select * into v_doc from documents where id = p_document_id;
224
+
225
+ select c.currency_code into v_home from companies c where c.id = v_doc.company_id;
226
+ v_rate := v_doc.exchange_rate;
227
+ v_foreign := v_doc.currency_code <> v_home;
228
+ v_round := rounding_of(v_doc.company_id, v_doc.currency_code);
229
+ v_book_round := rounding_of(v_doc.company_id);
230
+
231
+ v_journal := coalesce(
232
+ v_doc.journal_id,
233
+ case when v_is_sale
234
+ then (select sales_journal_id from companies where id = v_doc.company_id)
235
+ else (select purchase_journal_id from companies where id = v_doc.company_id)
236
+ end
237
+ );
238
+ if v_journal is null then
239
+ raise exception 'no_journal: set journal_id on the document or a default journal on the company';
240
+ end if;
241
+
242
+ perform assert_period_open(v_doc.company_id, v_date, true);
243
+
244
+ v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
245
+
246
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
247
+ description, state, document_id, currency_code)
248
+ values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
249
+ coalesce(v_doc.number, v_doc.supplier_reference),
250
+ v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
251
+ then ' / ' || v_doc.supplier_reference else '' end,
252
+ 'draft', p_document_id, v_doc.currency_code)
253
+ returning * into v_entry;
254
+
255
+ -- ------------------------------------------------------------------ bases
256
+ for r in
257
+ select l.account_id,
258
+ l.tax_id,
259
+ sum(l.amount_untaxed) as base_amount,
260
+ min(l.sequence) as seq,
261
+ string_agg(distinct l.name, ', ') as label
262
+ from document_lines l
263
+ where l.document_id = p_document_id
264
+ and l.line_type = 'product'
265
+ group by l.account_id, l.tax_id
266
+ having sum(l.amount_untaxed) <> 0
267
+ order by 4
268
+ loop
269
+ select tp.posting_type, tp.declaration_box, tp.factor_percent, tp.box_factor_percent,
270
+ coalesce(t.cash_basis, false) as cash_basis
271
+ into p
272
+ from tax_postings tp
273
+ join taxes t on t.id = tp.tax_id
274
+ where tp.tax_id = r.tax_id
275
+ and tp.document_kind = v_kind
276
+ and tp.posting_type = 'base'
277
+ limit 1;
278
+
279
+ v_amount := round_amount(r.base_amount * coalesce(p.factor_percent, 100) / 100, v_round);
280
+ v_book := round_amount(v_amount / v_rate, v_book_round);
281
+ v_seq := v_seq + 10;
282
+
283
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
284
+ debit, credit, tax_id, tax_line, posting_type,
285
+ declaration_box, box_amount, currency_code, amount_currency)
286
+ values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
287
+ case when v_base_credit then 0 else v_book end,
288
+ case when v_base_credit then v_book else 0 end,
289
+ r.tax_id, false, p.posting_type,
290
+ case when coalesce(p.cash_basis, false) then null else p.declaration_box end,
291
+ case when p.declaration_box is null then null
292
+ else round_amount(r.base_amount * coalesce(p.box_factor_percent, 100) / 100 / v_rate,
293
+ v_book_round) end,
294
+ v_doc.currency_code,
295
+ case when v_foreign then v_amount end);
296
+ end loop;
297
+
298
+ -- ------------------------------------------------------------------ taxes
299
+ for r in
300
+ select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
301
+ from document_tax_summary s
302
+ where s.document_id = p_document_id
303
+ and s.tax_id is not null
304
+ and s.tax_amount <> 0
305
+ order by s.tax_code
306
+ loop
307
+ select t.cash_basis, t.cash_basis_transition_account_id
308
+ into v_cash, v_transition
309
+ from taxes t where t.id = r.tax_id;
310
+
311
+ if v_cash then
312
+ -- A tax that waits needs somewhere to wait. Refuse by name rather than
313
+ -- book it on the account it is due on, which would make it due.
314
+ if v_transition is null then
315
+ raise exception 'no_cash_basis_account: tax % falls due on collection and names no transition account',
316
+ r.tax_code;
317
+ end if;
318
+ -- One posting per side, or the transition lines of a document cannot be
319
+ -- told apart when the matching sends each of them on. A tax whose
320
+ -- postings net out has nothing waiting to collect anyway.
321
+ select count(*) into v_postings
322
+ from tax_postings tp
323
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
324
+ and tp.posting_type = 'tax';
325
+ if v_postings > 1 then
326
+ raise exception 'cash_basis_split_tax: tax % falls due on collection and has % tax postings; it takes one',
327
+ r.tax_code, v_postings;
328
+ end if;
329
+ if exists (select 1 from tax_postings tp
330
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
331
+ and tp.posting_type = 'tax_on_base') then
332
+ raise exception 'cash_basis_tax_on_base: tax % falls due on collection and carries a non-deductible share; a cost is not deferred',
333
+ r.tax_code;
334
+ end if;
335
+ -- And it needs a box to fall due *into*. `settle_cash_basis_tax()` only
336
+ -- ever looks at lines that carry a `box_amount`, and a posting with no
337
+ -- `declaration_box` produces none — so the amount would sit on the
338
+ -- transition account for ever, settled by nothing and reported by
339
+ -- nothing, with no error anywhere. Refuse it here, where the pack can
340
+ -- still be corrected, rather than discover it in a balance years later.
341
+ if not exists (select 1 from tax_postings tp
342
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
343
+ and tp.posting_type = 'tax'
344
+ and tp.declaration_box is not null) then
345
+ 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',
346
+ r.tax_code;
347
+ end if;
348
+ end if;
349
+
350
+ -- The postings of one side share out the tax of the group; the last of
351
+ -- each side takes what is left. Until `tax_on_base` there was never more
352
+ -- than one posting per side, so this changes no existing tax by a cent —
353
+ -- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
354
+ -- where rounding each half on its own would book 0.64 against a document
355
+ -- that totals 0.63.
356
+ v_side_left := null;
357
+ v_side_left_neg := null;
358
+
359
+ for p in
360
+ select tp.posting_type, tp.factor_percent, tp.account_id,
361
+ tp.declaration_box, tp.box_factor_percent,
362
+ case when tp.factor_percent >= 0 then 1 else -1 end as side,
363
+ sum(abs(tp.factor_percent))
364
+ over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
365
+ as side_factor,
366
+ row_number() over (
367
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end
368
+ order by tp.sequence, tp.id)
369
+ = count(*) over (
370
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end)
371
+ as is_last_of_side
372
+ from tax_postings tp
373
+ where tp.tax_id = r.tax_id
374
+ and tp.document_kind = v_kind
375
+ and tp.posting_type in ('tax', 'tax_on_base')
376
+ order by tp.sequence, tp.id
377
+ loop
378
+ -- The tax of the group was rounded once, in the view. Every posting is
379
+ -- a share of that one figure, never of a re-derived one.
380
+ if p.side >= 0 then
381
+ if v_side_left is null then
382
+ v_side_left := round_amount(r.tax_amount * p.side_factor / 100, v_round);
383
+ end if;
384
+ if p.is_last_of_side then
385
+ v_amount := v_side_left;
386
+ else
387
+ v_amount := round_amount(r.tax_amount * abs(p.factor_percent) / 100, v_round);
388
+ v_side_left := v_side_left - v_amount;
389
+ end if;
390
+ else
391
+ if v_side_left_neg is null then
392
+ v_side_left_neg := round_amount(r.tax_amount * p.side_factor / 100, v_round);
393
+ end if;
394
+ if p.is_last_of_side then
395
+ v_amount := v_side_left_neg;
396
+ else
397
+ v_amount := round_amount(r.tax_amount * abs(p.factor_percent) / 100, v_round);
398
+ v_side_left_neg := v_side_left_neg - v_amount;
399
+ end if;
400
+ end if;
401
+
402
+ if v_amount = 0 then
403
+ continue;
404
+ end if;
405
+ -- A positive factor keeps the side of the base, a negative one flips it.
406
+ v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
407
+ -- The box keeps its own rounding: `box_factor_percent` was always
408
+ -- independent from `factor_percent`, because a declaration figure is
409
+ -- not a ledger figure and only the ledger has to balance.
410
+ v_box_amount := case when p.declaration_box is null then null
411
+ else round_amount(r.tax_amount * p.box_factor_percent / 100 / v_rate,
412
+ v_book_round) end;
413
+ v_book := round_amount(v_amount / v_rate, v_book_round);
414
+
415
+ if p.posting_type = 'tax' then
416
+ v_seq := v_seq + 10;
417
+
418
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
419
+ debit, credit, tax_id, tax_line, posting_type,
420
+ declaration_box, box_amount, currency_code, amount_currency)
421
+ values (v_entry.id, v_doc.company_id,
422
+ case when v_cash then v_transition else p.account_id end,
423
+ v_seq, r.tax_name,
424
+ case when v_side_credit then 0 else v_book end,
425
+ case when v_side_credit then v_book else 0 end,
426
+ r.tax_id, true, p.posting_type,
427
+ case when v_cash then null else p.declaration_box end,
428
+ v_box_amount, v_doc.currency_code,
429
+ case when v_foreign then v_amount end);
430
+ continue;
431
+ end if;
432
+
433
+ -- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
434
+ -- lines it taxes, split in proportion to their base. The last share
435
+ -- takes whatever is left, so the shares add up to the amount that was
436
+ -- rounded once on the group and the entry still balances to the cent.
437
+ v_left := v_amount;
438
+ v_left_box := v_box_amount;
439
+
440
+ for g in
441
+ select account_id,
442
+ base_amount,
443
+ seq,
444
+ sum(base_amount) over () as total_base,
445
+ row_number() over (order by seq) = count(*) over () as is_last
446
+ from (
447
+ select l.account_id,
448
+ sum(l.amount_untaxed) as base_amount,
449
+ min(l.sequence) as seq
450
+ from document_lines l
451
+ where l.document_id = p_document_id
452
+ and l.line_type = 'product'
453
+ and l.tax_id = r.tax_id
454
+ group by l.account_id
455
+ having sum(l.amount_untaxed) <> 0
456
+ ) as groups
457
+ order by seq
458
+ loop
459
+ if g.is_last then
460
+ v_share := v_left;
461
+ v_share_box := v_left_box;
462
+ else
463
+ v_share := round_amount(v_amount * g.base_amount / g.total_base, v_round);
464
+ v_share_box := case when v_box_amount is null then null
465
+ else round_amount(v_box_amount * g.base_amount / g.total_base,
466
+ v_book_round) end;
467
+ v_left := v_left - v_share;
468
+ v_left_box := v_left_box - v_share_box;
469
+ end if;
470
+
471
+ if v_share = 0 then
472
+ continue;
473
+ end if;
474
+
475
+ v_seq := v_seq + 10;
476
+ v_share_book := round_amount(v_share / v_rate, v_book_round);
477
+
478
+ -- `tax_line` stays false: the amount is on a base account and belongs
479
+ -- to the base side of the declaration, which is why the Belgian grids
480
+ -- 82 and 83 report it together with the base.
481
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
482
+ debit, credit, tax_id, tax_line, posting_type,
483
+ declaration_box, box_amount, currency_code, amount_currency)
484
+ values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
485
+ case when v_side_credit then 0 else v_share_book end,
486
+ case when v_side_credit then v_share_book else 0 end,
487
+ r.tax_id, false, p.posting_type,
488
+ p.declaration_box, v_share_box, v_doc.currency_code,
489
+ case when v_foreign then v_share end);
490
+ end loop;
491
+ end loop;
492
+ end loop;
493
+
494
+ -- ------------------------------------------------------------ counterpart
495
+ select total_debit - total_credit into v_diff from entries where id = v_entry.id;
496
+ select coalesce(sum(case when l.debit > 0 then l.amount_currency else -l.amount_currency end), 0)
497
+ into v_diff_cur
498
+ from entry_lines l where l.entry_id = v_entry.id;
499
+
500
+ if v_diff = 0 then
501
+ raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
502
+ end if;
503
+
504
+ v_contact := commercial_entity(v_doc.contact_id);
505
+ v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
506
+
507
+ select payment_terms_days into v_terms from contacts where id = v_contact;
508
+ v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
509
+
510
+ v_amount := abs(v_diff);
511
+ -- The counterpart balances the entry in both currencies. The total it is
512
+ -- checked against is the document's own, which is the currency
513
+ -- `amount_total` is stated in.
514
+ v_total_cur := case when v_foreign then abs(v_diff_cur) else v_amount end;
515
+ v_seq := v_seq + 10;
516
+
517
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
518
+ debit, credit, contact_id, date_maturity, currency_code,
519
+ amount_currency)
520
+ values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
521
+ case when v_diff > 0 then 0 else v_amount end,
522
+ case when v_diff > 0 then v_amount else 0 end,
523
+ v_contact, v_maturity, v_doc.currency_code,
524
+ case when v_foreign then v_total_cur end);
525
+
526
+ -- The ledger is right by construction. If the header disagrees, the header
527
+ -- is what is wrong, and we say so instead of quietly patching a line.
528
+ -- Half a unit of the document's own currency, which is what `0.005` used to
529
+ -- mean when every currency was assumed to have cents.
530
+ if abs(v_total_cur - abs(v_doc.amount_total)) > currency_unit(v_round) / 2 then
531
+ raise exception 'document_total_mismatch: document % totals % but its lines book %',
532
+ p_document_id, v_doc.amount_total, v_total_cur;
533
+ end if;
534
+
535
+ -- --------------------------------------------------------------- posting
536
+ v_entry := post_entry(v_entry.id);
537
+
538
+ update documents
539
+ set state = 'posted',
540
+ number = coalesce(number, v_entry.number),
541
+ entry_id = v_entry.id,
542
+ accounting_date = v_date
543
+ where id = p_document_id;
544
+
545
+ return v_entry;
546
+ end;
547
+ $$;
548
+
549
+ -- The transfer that makes a cash-basis tax due. Both legs carry the posting
550
+ -- that falls due: the one that lands on the account and the box it is declared
551
+ -- on, and the one that empties the transition account it waited on.
552
+
553
+ create or replace function settle_cash_basis_tax(
554
+ p_document_id uuid,
555
+ p_date date
556
+ )
557
+ returns uuid
558
+ language plpgsql
559
+ as $$
560
+ declare
561
+ v_doc documents%rowtype;
562
+ v_company companies%rowtype;
563
+ v_entry entries%rowtype;
564
+ v_kind tax_document_kind;
565
+ v_total numeric;
566
+ v_paid numeric;
567
+ v_ratio numeric;
568
+ v_seq integer := 0;
569
+ v_final uuid;
570
+ v_box text;
571
+ v_target uuid;
572
+ v_due numeric;
573
+ v_done numeric;
574
+ v_delta numeric;
575
+ v_box_due numeric;
576
+ v_box_done numeric;
577
+ v_box_delta numeric;
578
+ -- The transfer is written in the ledger's currency on both sides, which is
579
+ -- the company's own: the entry it produces says so.
580
+ v_round money_rounding;
581
+ v_credit boolean;
582
+ v_type tax_posting_type;
583
+ w record;
584
+ begin
585
+ select * into v_doc from documents where id = p_document_id;
586
+ if not found or v_doc.entry_id is null then
587
+ return null;
588
+ end if;
589
+
590
+ -- Nothing is waiting on this document: the answer for every tax that falls
591
+ -- due when it is invoiced, which is most of them.
592
+ if not exists (
593
+ select 1
594
+ from entry_lines q join taxes t on t.id = q.tax_id
595
+ where q.entry_id = v_doc.entry_id
596
+ and t.cash_basis
597
+ and q.declaration_box is null
598
+ and q.box_amount is not null
599
+ ) then
600
+ return null;
601
+ end if;
602
+
603
+ select * into v_company from companies where id = v_doc.company_id;
604
+ v_round := rounding_of(v_doc.company_id);
605
+ if v_company.miscellaneous_journal_id is null then
606
+ raise exception 'no_miscellaneous_journal: company % has no journal for the transfer of a cash-basis tax',
607
+ v_doc.company_id;
608
+ end if;
609
+
610
+ v_kind := case when v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note')
611
+ then 'credit_note' else 'invoice' end::tax_document_kind;
612
+
613
+ -- What share of this document has been settled, read on its own third-party
614
+ -- lines and in the ledger's currency on both sides of the division.
615
+ select coalesce(sum(abs(tl.debit - tl.credit)), 0), coalesce(sum(tl.matched_amount), 0)
616
+ into v_total, v_paid
617
+ from entry_lines tl join accounts a on a.id = tl.account_id
618
+ where tl.entry_id = v_doc.entry_id
619
+ and a.reconcilable
620
+ and a.account_type in ('asset_receivable', 'liability_payable');
621
+
622
+ if v_total = 0 then
623
+ return null;
624
+ end if;
625
+ -- An overpayment settles the document, and no more: a tax is due on what
626
+ -- was invoiced.
627
+ v_ratio := least(1, greatest(0, v_paid / v_total));
628
+
629
+ for w in
630
+ select l2.id, l2.account_id, l2.tax_id, l2.tax_line, l2.box_amount,
631
+ l2.debit, l2.credit, l2.name, l2.sequence
632
+ from entry_lines l2 join taxes t on t.id = l2.tax_id
633
+ where l2.entry_id = v_doc.entry_id
634
+ and t.cash_basis
635
+ and l2.declaration_box is null
636
+ and l2.box_amount is not null
637
+ order by l2.sequence
638
+ loop
639
+ select tp.account_id, tp.declaration_box, tp.posting_type
640
+ into v_final, v_box, v_type
641
+ from tax_postings tp
642
+ where tp.tax_id = w.tax_id
643
+ and tp.document_kind = v_kind
644
+ and tp.posting_type = (case when w.tax_line then 'tax' else 'base' end)::tax_posting_type
645
+ limit 1;
646
+
647
+ -- The account the transfer of this line lands on, which is also how an
648
+ -- earlier transfer of the same line is recognised.
649
+ v_target := case when w.tax_line then v_final else w.account_id end;
650
+ if v_target is null then
651
+ raise exception 'no_cash_basis_target: the tax of line % names no account to fall due on', w.id;
652
+ end if;
653
+ -- A line only waits because a box was worked out for it, and that box came
654
+ -- from this very posting. If it has none, the amount would wait for ever.
655
+ if v_box is null then
656
+ raise exception 'no_cash_basis_box: the tax of line % holds an amount for a box the posting does not name', w.id;
657
+ end if;
658
+
659
+ if w.tax_line then
660
+ v_due := round_amount((w.debit + w.credit) * v_ratio, v_round);
661
+ select coalesce(sum(case when w.credit > 0 then x.credit - x.debit
662
+ else x.debit - x.credit end), 0)
663
+ into v_done
664
+ from entry_lines x join entries e on e.id = x.entry_id
665
+ where e.document_id = p_document_id
666
+ and e.id <> v_doc.entry_id
667
+ and x.tax_id = w.tax_id
668
+ and x.tax_line
669
+ and x.account_id = v_target;
670
+ else
671
+ v_due := 0;
672
+ v_done := 0;
673
+ end if;
674
+ v_delta := v_due - v_done;
675
+
676
+ v_box_due := round_amount(w.box_amount * v_ratio, v_round);
677
+ select coalesce(sum(x.box_amount), 0)
678
+ into v_box_done
679
+ from entry_lines x join entries e on e.id = x.entry_id
680
+ where e.document_id = p_document_id
681
+ and e.id <> v_doc.entry_id
682
+ and x.tax_id = w.tax_id
683
+ and x.tax_line = w.tax_line
684
+ and x.account_id = v_target;
685
+ v_box_delta := v_box_due - v_box_done;
686
+
687
+ if v_delta = 0 and v_box_delta = 0 then
688
+ continue;
689
+ end if;
690
+
691
+ if v_entry.id is null then
692
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
693
+ description, state, document_id, currency_code)
694
+ values (v_doc.company_id, v_company.miscellaneous_journal_id,
695
+ fiscal_year_at(v_doc.company_id, p_date), p_date,
696
+ v_doc.number,
697
+ coalesce(v_doc.number, 'document') || ' — tax due on settlement',
698
+ 'draft', p_document_id, v_company.currency_code)
699
+ returning * into v_entry;
700
+ end if;
701
+
702
+ -- The side the document put the tax on, kept when the share grows and
703
+ -- flipped when it shrinks.
704
+ v_credit := (w.credit > 0) = (v_delta > 0);
705
+
706
+ v_seq := v_seq + 10;
707
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
708
+ debit, credit, tax_id, tax_line, posting_type,
709
+ declaration_box, box_amount, currency_code)
710
+ values (v_entry.id, v_doc.company_id, v_target, v_seq, w.name,
711
+ case when v_credit then 0 else abs(v_delta) end,
712
+ case when v_credit then abs(v_delta) else 0 end,
713
+ w.tax_id, w.tax_line, v_type,
714
+ v_box, v_box_delta,
715
+ v_company.currency_code);
716
+
717
+ if v_delta <> 0 then
718
+ v_seq := v_seq + 10;
719
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
720
+ debit, credit, tax_id, tax_line, posting_type, currency_code)
721
+ values (v_entry.id, v_doc.company_id, w.account_id, v_seq, w.name,
722
+ case when v_credit then abs(v_delta) else 0 end,
723
+ case when v_credit then 0 else abs(v_delta) end,
724
+ w.tax_id, true, v_type, v_company.currency_code);
725
+ end if;
726
+ end loop;
727
+
728
+ if v_entry.id is null then
729
+ return null;
730
+ end if;
731
+
732
+ v_entry := post_entry(v_entry.id);
733
+ return v_entry.id;
734
+ end;
735
+ $$;
736
+
737
+ -- ---------------------------------------------------------------------------
738
+ -- The reader that asked for the column
739
+ -- ---------------------------------------------------------------------------
740
+ --
741
+ -- `ec_sales_list()` took every line of an intra-Community tax that is not a tax
742
+ -- line, and a `tax_on_base` line is one of those. It is exact on every pack of
743
+ -- this repository — an exempt supply has no tax to capitalise — and it was
744
+ -- exact by luck. Now it says what it means, and it says it in a way that is
745
+ -- also right about a ledger written before the column existed: a line is
746
+ -- excluded when it is **known** to be a `tax_on_base` line, and a null keeps
747
+ -- the reading it had, which is the one the backfill above refused to improve
748
+ -- on. The rest of the function is `20260916094500` unchanged, period guard
749
+ -- included.
750
+
751
+ create or replace function ec_sales_list(
752
+ p_company_id uuid,
753
+ p_from date,
754
+ p_to date,
755
+ p_report_code text default null
756
+ )
757
+ returns table (
758
+ vat_country char(2),
759
+ vat_number text,
760
+ nature text,
761
+ amount numeric,
762
+ currency_code char(3),
763
+ documents integer,
764
+ contact_ids uuid[],
765
+ contact_names text[],
766
+ issue text
767
+ )
768
+ language plpgsql
769
+ stable
770
+ as $$
771
+ declare
772
+ v_country char(2);
773
+ v_prefix char(2);
774
+ v_currency char(3);
775
+ v_round money_rounding;
776
+ v_files declaration_period;
777
+ v_accepts declaration_period[];
778
+ v_asked declaration_period;
779
+ begin
780
+ select c.fiscal_country, c.currency_code into v_country, v_currency
781
+ from companies c where c.id = p_company_id;
782
+ if not found then
783
+ raise exception 'unknown_company: %', p_company_id;
784
+ end if;
785
+ if not exists (select 1 from territories) then
786
+ raise exception 'no_territories: apply supabase/seed/00_territories.sql';
787
+ end if;
788
+
789
+ -- Which period this is, when the caller said which statement they are
790
+ -- filing. Five things have to hold: a form was named, it exists, the company
791
+ -- has recorded a cadence for it, the dates are themselves a whole cadence the
792
+ -- form is filed on, and the two differ. A monthly statement from a quarterly
793
+ -- filer is the ordinary case in three of the four countries read while this
794
+ -- was written, and nothing here touches it: the cadence read is the
795
+ -- statement's own.
796
+ if p_report_code is not null then
797
+ select t.periods into v_accepts
798
+ from tax_report_templates t
799
+ where t.code = p_report_code
800
+ and t.valid_from <= p_to
801
+ and (t.valid_to is null or t.valid_to >= p_to)
802
+ order by t.country
803
+ limit 1;
804
+ if v_accepts is null then
805
+ raise exception 'unknown_tax_report: % is not a declaration form in force on %',
806
+ p_report_code, p_to;
807
+ end if;
808
+ v_files := filing_period(p_company_id, p_report_code);
809
+ v_asked := declaration_period_of(p_from, p_to);
810
+ if v_files is not null
811
+ and v_asked is not null
812
+ and v_files = any(v_accepts)
813
+ and v_asked = any(v_accepts)
814
+ and v_asked <> v_files then
815
+ raise exception
816
+ 'wrong_declaration_period: this company files % on %; % to % is a %',
817
+ v_files, p_report_code, p_from, p_to, v_asked;
818
+ end if;
819
+ end if;
820
+
821
+ v_prefix := vat_prefix_of(v_country);
822
+ v_round := rounding_of(p_company_id);
823
+
824
+ return query
825
+ with supply as (
826
+ select d.contact_id,
827
+ ct.name as contact_name,
828
+ regexp_replace(t.treatment::text, '^intracom_', '') as nature,
829
+ upper(regexp_replace(coalesce(ct.vat_number, ''), '[^A-Za-z0-9]', '', 'g')) as vat_raw,
830
+ ct.country as contact_country,
831
+ e.entry_date,
832
+ e.document_id,
833
+ l.credit - l.debit as amount
834
+ from entry_lines l
835
+ join entries e on e.id = l.entry_id
836
+ join taxes t on t.id = l.tax_id
837
+ left join documents d on d.id = e.document_id
838
+ left join contacts ct on ct.id = d.contact_id
839
+ where l.company_id = p_company_id
840
+ and e.state = 'posted'
841
+ and e.entry_date between p_from and p_to
842
+ and not l.tax_line
843
+ -- A line known to have been written by a `tax_on_base` posting is a
844
+ -- cost and not a supply. Null is left in, because a ledger written
845
+ -- before `posting_type` existed reads exactly as it did.
846
+ and l.posting_type is distinct from 'tax_on_base'::tax_posting_type
847
+ and t.treatment::text like 'intracom!_%' escape '!'
848
+ and t.treatment::text not like 'intracom!_acquisition!_%' escape '!'
849
+ ),
850
+ keyed as (
851
+ select s.*,
852
+ vat_prefix_of(
853
+ case when s.vat_raw ~ '^[A-Z]{2}' then substring(s.vat_raw from 1 for 2)
854
+ when s.vat_raw = '' then null
855
+ else s.contact_country
856
+ end
857
+ ) as vat_country,
858
+ case when s.vat_raw = '' then null
859
+ when s.vat_raw ~ '^[A-Z]{2}' then substring(s.vat_raw from 3)
860
+ else s.vat_raw
861
+ end as vat_number
862
+ from supply s
863
+ ),
864
+ scoped as (
865
+ select k.*, eu_vat_scope_of(k.vat_country, k.entry_date) as scope
866
+ from keyed k
867
+ ),
868
+ judged as (
869
+ select s.*,
870
+ case
871
+ when s.contact_id is null then 'no_customer'
872
+ when s.vat_number is null or s.vat_number = '' then 'no_vat_number'
873
+ when s.vat_country is null then 'no_vat_country'
874
+ when s.vat_country = v_prefix then 'vat_country_is_the_company_country'
875
+ when s.scope = 'none' then 'vat_country_outside_the_union'
876
+ when s.scope <> 'full' and s.nature = 'services'
877
+ then 'vat_country_outside_the_union_for_this_supply'
878
+ end as issue
879
+ from scoped s
880
+ )
881
+ select j.vat_country::char(2),
882
+ j.vat_number,
883
+ j.nature,
884
+ round_amount(sum(j.amount), v_round),
885
+ v_currency,
886
+ count(distinct j.document_id)::integer,
887
+ array_agg(distinct j.contact_id) filter (where j.contact_id is not null),
888
+ array_agg(distinct j.contact_name) filter (where j.contact_name is not null),
889
+ j.issue
890
+ from judged j
891
+ group by j.vat_country, j.vat_number, j.nature, j.issue,
892
+ case when j.issue is not null then j.contact_id end
893
+ having round_amount(sum(j.amount), v_round) <> 0
894
+ order by (j.issue is not null), j.vat_country, j.vat_number, j.nature;
895
+ end;
896
+ $$;
897
+
898
+ comment on function ec_sales_list(uuid, date, date, text) is
899
+ 'The recapitulative statement of intra-Community supplies for a period: one line per customer VAT identification number and per nature — goods, services, and whatever the treatment vocabulary gains next — summed from the posted ledger in the company''s currency, credit notes deducted. The country of a line is the prefix the customer''s numbers carry, read from `territories`, so a Greek customer is listed under EL. A supply that cannot be declared comes back with the reason in `issue` rather than being left out. Name the form in p_report_code to have the period checked against the cadence this company files **that statement** on, which is not the cadence of its periodic return in any country read so far; name none and nothing is refused. A supply is a base line of the tax: a line a `tax_on_base` posting wrote is a cost and is left out. No country rule lives in this function.';
900
+
901
+ revoke execute on function ec_sales_list(uuid, date, date, text) from public, anon;
902
+ grant execute on function ec_sales_list(uuid, date, date, text) to authenticated, service_role;