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,1211 @@
1
+ -- Ekwo OS — a tax point the law can be written in, and an engine that reads it.
2
+ --
3
+ -- `country_defaults.tax_point_rule` has been a single word since
4
+ -- `20260912111751` — `invoice_date`, `delivery_date`, `payment_date` — and two
5
+ -- things were true of it. No function read it: `docs/decisions.md` recorded
6
+ -- the column as "declared, no reader", and `post_document()` dated a tax by
7
+ -- the day the entry was booked on, which is a different question. And two
8
+ -- packs could not say their own law in one word, which their own citations
9
+ -- already admitted.
10
+ --
11
+ -- **Luxembourg.** Art. 21 of the modified law of 12 February 1979: "Le fait
12
+ -- générateur de la taxe intervient et la taxe devient exigible au moment où la
13
+ -- livraison de biens ou la prestation de services est effectuée." The invoice
14
+ -- is art. 24, par. 1er: "Par dérogation aux articles 21, 22 et 23, lorsqu'il y
15
+ -- a obligation d'émettre une facture, la taxe devient exigible : a) lors de
16
+ -- l'émission de la facture si elle est émise dans le délai visé à l'article
17
+ -- 63, paragraphe 5 ; b) le jour où expire le délai visé au point a) en
18
+ -- l'absence d'émission de la facture dans ce délai." The pack declared
19
+ -- `invoice_date`, which is the derogation given as if it were the principle.
20
+ --
21
+ -- **Belgium is the same shape**, and nobody had looked. Art. 16, § 1er and
22
+ -- art. 22, § 1er put the fait générateur and the exigibility at the moment of
23
+ -- the supply; art. 17, § 1er and art. 22bis, § 1er derogate — "au moment de
24
+ -- l'émission de la facture, à concurrence du montant facturé, peu importe que
25
+ -- l'émission de cette facture ait lieu avant ou après le moment où la
26
+ -- livraison est effectuée", with the fifteenth day of the following month
27
+ -- where no invoice was issued before it. The pack's citation said all of this
28
+ -- and the word said `invoice_date`.
29
+ --
30
+ -- **Estonia.** KMS § 11 lg 1, in the official English translation: "The time
31
+ -- of supply or the time of receipt of services is deemed to be the date on
32
+ -- which the **first** of one of the following acts is performed: 1) the goods
33
+ -- are dispatched or made available to the purchaser, or the services are
34
+ -- provided; 2) full or partial payment is received for the goods or services
35
+ -- […]". Two branches, and the pack declared the first one. The invoice is
36
+ -- **not** a branch of lg 1 — it belongs to lg 2, which is the separate rule
37
+ -- for intra-Community supply — so the rule Estonia keeps is the earliest of a
38
+ -- supply and a payment, and nothing else.
39
+ --
40
+ -- **France was read and is right as it stands.** CGI art. 269, 1, a puts the
41
+ -- fait générateur at the supply and art. 269, 2, a makes the tax chargeable
42
+ -- then, an acompte advancing it to the amount received; services are art. 269,
43
+ -- 2, c, chargeable on collection unless the taxpayer opts for the débits, and
44
+ -- that half is `taxes.cash_basis` on the service taxes by a decision this
45
+ -- repository already wrote down. `delivery_date` is France's principle.
46
+ --
47
+ -- ---------------------------------------------------------------------------
48
+ -- What this migration does
49
+ -- ---------------------------------------------------------------------------
50
+ --
51
+ -- **Two words, and the vocabulary stays closed.** `invoice_if_issued` is the
52
+ -- Luxembourg and Belgian shape: the supply is the principle, an invoice
53
+ -- displaces it where the country requires one and one was issued.
54
+ -- `earliest_of_delivery_or_payment` is the Estonian shape: whichever of the
55
+ -- two happened first. Both say what they mean in their own name, which is the
56
+ -- rule `applies_when` set for a closed vocabulary — an accountant reads the
57
+ -- value and knows what it is — and neither is an expression a pack could
58
+ -- write a condition in.
59
+ --
60
+ -- **One reader, named.** `tax_point_of()` is the only function that reads
61
+ -- `country_defaults.tax_point_rule`, the way `numbering_rules()` is the only
62
+ -- one that reads `number_format`. The vocabulary is a `case` inside it and
63
+ -- nowhere else; `post_document()` calls it and knows no country.
64
+ --
65
+ -- **Two columns, because a tax point is not an accounting date.** A December
66
+ -- invoice booked in January is one entry on one date and a tax due in
67
+ -- December, and the ledger has to be able to say both. `documents.tax_point_date`
68
+ -- is EN 16931's BT-7, which the table lacked and which an e-invoice carries;
69
+ -- where it is filled it is the answer, because a rule is a default and a
70
+ -- stated fact is not. `entry_lines.tax_point_date` is what the declaration
71
+ -- reads, beside the `declaration_box` and the `box_amount` already there.
72
+ --
73
+ -- **Null is the reading of every ledger written before today.** No backfill
74
+ -- and no default: a line with no tax point is declared on its entry's date,
75
+ -- which is what `vat_return()` did for every line until this file. So the
76
+ -- goldens of the six packs do not move — none of their documents carries a
77
+ -- delivery date or an accounting date apart from its own — and the change is
78
+ -- visible only where a document says something the engine could not read
79
+ -- before.
80
+ --
81
+ -- **Where the payment branch stops.** `post_document()` passes no payment
82
+ -- date, and it is not an omission: matching is recorded against the entry
83
+ -- this function is writing, so at posting time no payment of this document
84
+ -- exists. A rule whose earliest branch is a payment therefore falls back on
85
+ -- the branch it can see, and the payment half is honoured only where the tax
86
+ -- itself says `cash_basis` — `settle_cash_basis_tax()`, which now writes the
87
+ -- collection date onto both legs of its transfer instead of leaving the
88
+ -- entry's date to speak for it. A prepayment that is not a cash-basis tax is a
89
+ -- document Ekwo does not have; `docs/international.md` says so under its own
90
+ -- heading rather than this file inventing one.
91
+ --
92
+ -- ---------------------------------------------------------------------------
93
+ -- The other half of ST31: a source that reaches the database
94
+ -- ---------------------------------------------------------------------------
95
+ --
96
+ -- `20260915161842` turned `certification.sources` into a register and gave
97
+ -- `tax_templates` and `tax_report_box_templates` a `source_key`. Three places
98
+ -- were left: a chart, a statement and a statement line each name a `source` in
99
+ -- the pack and had nowhere to put it, so `legal_reference` reached the
100
+ -- database with no way to open the text behind it. Three nullable columns,
101
+ -- filled by the compiled seed like the two before them, no backfill and no
102
+ -- foreign key — `ekwo pack check` is where an unknown key is refused, because
103
+ -- the register lives in a jsonb column of another table.
104
+
105
+ -- ---------------------------------------------------------------------------
106
+ -- The vocabulary widens
107
+ -- ---------------------------------------------------------------------------
108
+ --
109
+ -- Dropped and re-added rather than edited: the constraint of `20260912111751`
110
+ -- is published, and this is the migration that changes it.
111
+
112
+ alter table country_defaults
113
+ drop constraint if exists country_defaults_tax_point_rule_known;
114
+
115
+ alter table country_defaults
116
+ add constraint country_defaults_tax_point_rule_known
117
+ check (tax_point_rule is null or tax_point_rule in (
118
+ 'invoice_date',
119
+ 'delivery_date',
120
+ 'payment_date',
121
+ 'invoice_if_issued',
122
+ 'earliest_of_delivery_or_payment'
123
+ ));
124
+
125
+ comment on column country_defaults.tax_point_rule is
126
+ 'When the tax becomes chargeable under this country''s general rule, in a closed vocabulary: invoice_date (the invoice fixes it), delivery_date (the supply fixes it), payment_date (collection fixes it), invoice_if_issued (the supply is the principle and an invoice displaces it where the country requires one — Belgium art. 17 and 22bis, Luxembourg art. 24), earliest_of_delivery_or_payment (whichever came first — Estonia KMS § 11 lg 1). A tax that departs from the country rule says so itself, with cash_basis. tax_point_of() is the only function that reads this column.';
127
+
128
+ -- ---------------------------------------------------------------------------
129
+ -- Where a tax point is written down
130
+ -- ---------------------------------------------------------------------------
131
+
132
+ alter table documents
133
+ add column if not exists tax_point_date date; -- BT-7
134
+
135
+ comment on column documents.tax_point_date is
136
+ 'EN 16931 BT-7: the day the tax on this document falls due, which is not the day the entry is booked on. Given on the document it is kept — an e-invoice states its own tax point and a stated fact outranks a rule — and left null it is worked out by post_document() from the country rule and written back here.';
137
+
138
+ alter table entry_lines
139
+ add column if not exists tax_point_date date;
140
+
141
+ comment on column entry_lines.tax_point_date is
142
+ 'The day the tax of this line fell due, from the document''s tax point or, on the transfer that makes a cash-basis tax due, from the collection. It is the date vat_return() puts the figure in a period by. Null on a line no tax posting wrote and on every line written before this column existed: the entry''s own date then answers, which is the reading the declaration always had.';
143
+
144
+ -- ---------------------------------------------------------------------------
145
+ -- The one reader of the country rule
146
+ -- ---------------------------------------------------------------------------
147
+ --
148
+ -- Deliberately not `immutable`: it reads two tables. `stable` is what
149
+ -- `numbering_rules()` is, for the same reason.
150
+ --
151
+ -- The dates it is given are the document's own. `p_document_date` is the
152
+ -- invoice date and is never null on a document; `p_delivery_date` is BT-72 and
153
+ -- usually is; `p_payment_date` is the day the price was received where that is
154
+ -- already known, and null where it is not.
155
+ --
156
+ -- A country that declares no rule gets null, not a borrowed one. That is
157
+ -- invariant 2 of `CONTRIBUTING.md`: there is no fallback country, and a reader
158
+ -- that needs a value a pack did not give says so rather than answering with
159
+ -- another country's law. Null here means "the entry's date", which is what the
160
+ -- declaration read before any of this existed, so a pack that says nothing
161
+ -- loses nothing.
162
+
163
+ create or replace function tax_point_of(
164
+ p_company_id uuid,
165
+ p_document_date date,
166
+ p_delivery_date date default null,
167
+ p_payment_date date default null
168
+ )
169
+ returns date
170
+ language sql
171
+ stable
172
+ as $$
173
+ select case d.tax_point_rule
174
+ -- The invoice fixes it, full stop.
175
+ when 'invoice_date' then p_document_date
176
+ -- The supply fixes it. A document that does not say when it was
177
+ -- delivered is one where the invoice is all there is to go on.
178
+ when 'delivery_date' then coalesce(p_delivery_date, p_document_date)
179
+ -- Collection fixes it, and until there is one there is no answer.
180
+ when 'payment_date' then p_payment_date
181
+ -- The supply is the principle and the invoice is the derogation.
182
+ -- A document being booked here is an invoice and carries its date,
183
+ -- so the derogation applies; the delivery is what is left when it
184
+ -- does not.
185
+ when 'invoice_if_issued' then coalesce(p_document_date, p_delivery_date)
186
+ -- Whichever of the two came first. `least` ignores a null, which is
187
+ -- exactly right: a branch nobody can date is a branch that has not
188
+ -- happened.
189
+ when 'earliest_of_delivery_or_payment'
190
+ then least(coalesce(p_delivery_date, p_document_date), p_payment_date)
191
+ else null
192
+ end
193
+ from companies c
194
+ left join country_defaults d on d.country = c.fiscal_country
195
+ where c.id = p_company_id;
196
+ $$;
197
+
198
+ comment on function tax_point_of(uuid, date, date, date) is
199
+ 'The day the tax on a document falls due, under the rule its company''s country declares. The only function that reads country_defaults.tax_point_rule, and the only place the vocabulary of that column is written out. Null where the country declares no rule or where the rule names a date the caller does not have, and null means the entry''s own date to every reader of it.';
200
+
201
+ revoke execute on function tax_point_of(uuid, date, date, date) from public, anon;
202
+ grant execute on function tax_point_of(uuid, date, date, date) to authenticated, service_role;
203
+
204
+ -- ---------------------------------------------------------------------------
205
+ -- Three columns so that a compiled reference can be opened
206
+ -- ---------------------------------------------------------------------------
207
+
208
+ alter table chart_templates
209
+ add column if not exists source_key text;
210
+
211
+ comment on column chart_templates.source_key is
212
+ 'Key of the entry in country_packs.sources where this chart''s legal_reference can be read. Null where the pack names none.';
213
+
214
+ alter table statement_templates
215
+ add column if not exists source_key text;
216
+
217
+ comment on column statement_templates.source_key is
218
+ 'Key of the entry in country_packs.sources where this statement''s legal_reference can be read. Null where the pack names none.';
219
+
220
+ alter table statement_line_templates
221
+ add column if not exists source_key text;
222
+
223
+ comment on column statement_line_templates.source_key is
224
+ 'Key of the entry in country_packs.sources where this line''s legal_reference can be read. Null where the pack names none.';
225
+
226
+ -- No table, view or sequence is created above: a column added to a table is
227
+ -- reachable by whoever could already select it, under the policies it already
228
+ -- has. The one object created is `tax_point_of()`, and it carries its own
229
+ -- revoke and grant.
230
+
231
+ -- ---------------------------------------------------------------------------
232
+ -- post_document() reads the country rule
233
+ -- ---------------------------------------------------------------------------
234
+ --
235
+ -- Replaced whole, because a function is replaced whole in PostgreSQL.
236
+ -- Everything but the tax point is `20260916103000` unchanged.
237
+
238
+ create or replace function post_document(p_document_id uuid)
239
+ returns entries
240
+ language plpgsql
241
+ as $$
242
+ declare
243
+ v_doc documents%rowtype;
244
+ v_entry entries%rowtype;
245
+ v_journal uuid;
246
+ v_date date;
247
+ v_is_sale boolean;
248
+ v_is_credit boolean;
249
+ v_kind tax_document_kind;
250
+ v_base_credit boolean;
251
+ v_seq integer := 0;
252
+ v_contact uuid;
253
+ v_maturity date;
254
+ v_terms smallint;
255
+ v_counterpart uuid;
256
+ -- Plain `numeric`, not `numeric(16, 2)`: a local that carries a scale is a
257
+ -- second rounding rule hiding in a declaration, and it is not the currency's.
258
+ -- The only thing that rounds here is `round_amount`.
259
+ v_diff numeric;
260
+ v_diff_cur numeric;
261
+ v_amount numeric;
262
+ v_book numeric;
263
+ v_box_amount numeric;
264
+ v_share numeric;
265
+ v_share_book numeric;
266
+ v_share_box numeric;
267
+ v_left numeric;
268
+ v_left_box numeric;
269
+ v_side_left numeric;
270
+ v_side_left_neg numeric;
271
+ v_side_credit boolean;
272
+ v_label text;
273
+ v_rate numeric(18, 8);
274
+ v_home char(3);
275
+ v_foreign boolean;
276
+ v_total_cur numeric;
277
+ -- Two currencies, one method: the document is stated in its own and the
278
+ -- ledger keeps the company's, and a yen invoice paid in euros rounds each
279
+ -- side at the decimals that side has.
280
+ v_round money_rounding;
281
+ v_book_round money_rounding;
282
+ v_cash boolean;
283
+ v_transition uuid;
284
+ -- The day the tax falls due, which is not the day the entry is booked on.
285
+ v_tax_point date;
286
+ v_postings integer;
287
+ -- Where the three parties of this document are, resolved once and only when
288
+ -- a tax on it asks. `document_territory()` is the ladder.
289
+ v_terr_seller text;
290
+ v_terr_buyer text;
291
+ v_terr_supply text;
292
+ r record;
293
+ p record;
294
+ g record;
295
+ begin
296
+ select * into v_doc from documents where id = p_document_id for update;
297
+ if not found then
298
+ raise exception 'unknown_document: document % does not exist', p_document_id;
299
+ end if;
300
+ if v_doc.state = 'posted' then
301
+ raise exception 'document_already_posted: document % is already posted', p_document_id;
302
+ end if;
303
+ if v_doc.state = 'cancelled' then
304
+ raise exception 'document_cancelled: document % cannot be posted', p_document_id;
305
+ end if;
306
+ if v_doc.entry_id is not null then
307
+ raise exception 'document_already_booked: document % already points at entry %',
308
+ p_document_id, v_doc.entry_id;
309
+ end if;
310
+
311
+ if v_doc.doc_type in ('sale_quote', 'purchase_order') then
312
+ raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
313
+ end if;
314
+
315
+ v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
316
+ v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
317
+ v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
318
+ -- Sale invoice and purchase credit note credit the base; the other two debit it.
319
+ v_base_credit := (v_is_sale <> v_is_credit);
320
+
321
+ v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
322
+
323
+ if not exists (
324
+ select 1 from document_lines
325
+ where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
326
+ ) then
327
+ raise exception 'document_empty: document % has no billable line', p_document_id;
328
+ end if;
329
+
330
+ -- A fixed-amount tax has no basis to spread over lines; refuse rather than
331
+ -- guess.
332
+ if exists (
333
+ select 1 from document_lines l join taxes t on t.id = l.tax_id
334
+ where l.document_id = p_document_id and t.amount_type <> 'percent'
335
+ ) then
336
+ raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
337
+ end if;
338
+
339
+ -- Every tax used must be in force on the accounting date.
340
+ for r in
341
+ select distinct t.id, t.code, t.valid_from, t.valid_to
342
+ from document_lines l join taxes t on t.id = l.tax_id
343
+ where l.document_id = p_document_id
344
+ loop
345
+ if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
346
+ raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
347
+ end if;
348
+ end loop;
349
+
350
+ -- Every tax that names a territory must find the parties where it says they
351
+ -- are. This is the one thing the core may do with a territory condition: it
352
+ -- refuses a tax that cannot apply, and it never chooses, substitutes or
353
+ -- suggests one — the core picks no tax for anybody, in any country.
354
+ --
355
+ -- The resolution is skipped entirely where nothing asks, which is every
356
+ -- document of every pack written before this migration.
357
+ if exists (
358
+ select 1
359
+ from document_lines l
360
+ join taxes t on t.id = l.tax_id
361
+ where l.document_id = p_document_id
362
+ and (t.applies_seller_territory is not null
363
+ or t.applies_buyer_territory is not null
364
+ or t.applies_supply_territory is not null)
365
+ ) then
366
+ v_terr_seller := document_territory(p_document_id, 'seller');
367
+ v_terr_buyer := document_territory(p_document_id, 'buyer');
368
+ v_terr_supply := document_territory(p_document_id, 'supply');
369
+
370
+ for r in
371
+ select distinct t.code,
372
+ t.applies_seller_territory as seller,
373
+ t.applies_buyer_territory as buyer,
374
+ t.applies_supply_territory as supply
375
+ from document_lines l
376
+ join taxes t on t.id = l.tax_id
377
+ where l.document_id = p_document_id
378
+ and (t.applies_seller_territory is not null
379
+ or t.applies_buyer_territory is not null
380
+ or t.applies_supply_territory is not null)
381
+ order by 1
382
+ loop
383
+ for p in
384
+ select v.party, v.wanted, v.actual, v.hint
385
+ from (values
386
+ ('seller', r.seller, v_terr_seller,
387
+ 'territory_code on the party that sells, or the country beside it'),
388
+ ('buyer', r.buyer, v_terr_buyer,
389
+ 'territory_code on the party that buys, or the country beside it'),
390
+ ('supply', r.supply, v_terr_supply,
391
+ 'supply_territory_code or delivery_country on the document')
392
+ ) as v (party, wanted, actual, hint)
393
+ where v.wanted is not null
394
+ order by v.party
395
+ loop
396
+ if p.actual is null then
397
+ raise exception 'no_party_territory: tax % applies where the % is in %, and nothing on this document says where the % is; set %',
398
+ r.code, p.party, p.wanted, p.party, p.hint;
399
+ end if;
400
+ if not territory_within(p.actual, p.wanted) then
401
+ raise exception 'tax_territory_mismatch: tax % applies where the % is in %; on this document the % is in %',
402
+ r.code, p.party, p.wanted, p.party, p.actual;
403
+ end if;
404
+ end loop;
405
+ end loop;
406
+ end if;
407
+
408
+ -- Totals are derived; make sure they reflect the lines as they stand now.
409
+ perform documents_refresh_totals(p_document_id);
410
+ select * into v_doc from documents where id = p_document_id;
411
+
412
+ select c.currency_code into v_home from companies c where c.id = v_doc.company_id;
413
+ v_rate := v_doc.exchange_rate;
414
+ v_foreign := v_doc.currency_code <> v_home;
415
+ v_round := rounding_of(v_doc.company_id, v_doc.currency_code);
416
+ v_book_round := rounding_of(v_doc.company_id);
417
+
418
+ -- When the tax on this document falls due, under the rule its country
419
+ -- declares. A document that carries BT-7 has been told and nobody overrides
420
+ -- it; otherwise `tax_point_of()` reads the country's word. The payment
421
+ -- argument is null here and says so: matching needs the entry this function
422
+ -- is writing, so at posting time no payment of this document has been
423
+ -- recorded, and a rule whose earliest branch is a payment falls back on the
424
+ -- branch it can see. Null comes back where the rule is `payment_date` or the
425
+ -- country declared none, and null on a line means "the entry's own date",
426
+ -- which is the reading every ledger written before this one had.
427
+ v_tax_point := coalesce(
428
+ v_doc.tax_point_date,
429
+ tax_point_of(v_doc.company_id, v_doc.document_date, v_doc.delivery_date, null));
430
+
431
+ v_journal := coalesce(
432
+ v_doc.journal_id,
433
+ case when v_is_sale
434
+ then (select sales_journal_id from companies where id = v_doc.company_id)
435
+ else (select purchase_journal_id from companies where id = v_doc.company_id)
436
+ end
437
+ );
438
+ if v_journal is null then
439
+ raise exception 'no_journal: set journal_id on the document or a default journal on the company';
440
+ end if;
441
+
442
+ perform assert_period_open(v_doc.company_id, v_date, true);
443
+
444
+ v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
445
+
446
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
447
+ description, state, document_id, currency_code)
448
+ values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
449
+ coalesce(v_doc.number, v_doc.supplier_reference),
450
+ v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
451
+ then ' / ' || v_doc.supplier_reference else '' end,
452
+ 'draft', p_document_id, v_doc.currency_code)
453
+ returning * into v_entry;
454
+
455
+ -- ------------------------------------------------------------------ bases
456
+ for r in
457
+ select l.account_id,
458
+ l.tax_id,
459
+ sum(l.amount_untaxed) as base_amount,
460
+ min(l.sequence) as seq,
461
+ string_agg(distinct l.name, ', ') as label
462
+ from document_lines l
463
+ where l.document_id = p_document_id
464
+ and l.line_type = 'product'
465
+ group by l.account_id, l.tax_id
466
+ having sum(l.amount_untaxed) <> 0
467
+ order by 4
468
+ loop
469
+ select tp.posting_type, tp.declaration_box, tp.factor_percent, tp.box_factor_percent,
470
+ coalesce(t.cash_basis, false) as cash_basis
471
+ into p
472
+ from tax_postings tp
473
+ join taxes t on t.id = tp.tax_id
474
+ where tp.tax_id = r.tax_id
475
+ and tp.document_kind = v_kind
476
+ and tp.posting_type = 'base'
477
+ limit 1;
478
+
479
+ v_amount := round_amount(r.base_amount * coalesce(p.factor_percent, 100) / 100, v_round);
480
+ v_book := round_amount(v_amount / v_rate, v_book_round);
481
+ v_seq := v_seq + 10;
482
+
483
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
484
+ debit, credit, tax_id, tax_line, posting_type,
485
+ declaration_box, box_amount, tax_point_date,
486
+ currency_code, amount_currency)
487
+ values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
488
+ case when v_base_credit then 0 else v_book end,
489
+ case when v_base_credit then v_book else 0 end,
490
+ r.tax_id, false, p.posting_type,
491
+ case when coalesce(p.cash_basis, false) then null else p.declaration_box end,
492
+ case when p.declaration_box is null then null
493
+ else round_amount(r.base_amount * coalesce(p.box_factor_percent, 100) / 100 / v_rate,
494
+ v_book_round) end,
495
+ v_tax_point,
496
+ v_doc.currency_code,
497
+ case when v_foreign then v_amount end);
498
+ end loop;
499
+
500
+ -- ------------------------------------------------------------------ taxes
501
+ for r in
502
+ select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
503
+ from document_tax_summary s
504
+ where s.document_id = p_document_id
505
+ and s.tax_id is not null
506
+ and s.tax_amount <> 0
507
+ order by s.tax_code
508
+ loop
509
+ select t.cash_basis, t.cash_basis_transition_account_id
510
+ into v_cash, v_transition
511
+ from taxes t where t.id = r.tax_id;
512
+
513
+ if v_cash then
514
+ -- A tax that waits needs somewhere to wait. Refuse by name rather than
515
+ -- book it on the account it is due on, which would make it due.
516
+ if v_transition is null then
517
+ raise exception 'no_cash_basis_account: tax % falls due on collection and names no transition account',
518
+ r.tax_code;
519
+ end if;
520
+ -- One posting per side, or the transition lines of a document cannot be
521
+ -- told apart when the matching sends each of them on. A tax whose
522
+ -- postings net out has nothing waiting to collect anyway.
523
+ select count(*) into v_postings
524
+ from tax_postings tp
525
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
526
+ and tp.posting_type = 'tax';
527
+ if v_postings > 1 then
528
+ raise exception 'cash_basis_split_tax: tax % falls due on collection and has % tax postings; it takes one',
529
+ r.tax_code, v_postings;
530
+ end if;
531
+ if exists (select 1 from tax_postings tp
532
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
533
+ and tp.posting_type = 'tax_on_base') then
534
+ raise exception 'cash_basis_tax_on_base: tax % falls due on collection and carries a non-deductible share; a cost is not deferred',
535
+ r.tax_code;
536
+ end if;
537
+ -- And it needs a box to fall due *into*. `settle_cash_basis_tax()` only
538
+ -- ever looks at lines that carry a `box_amount`, and a posting with no
539
+ -- `declaration_box` produces none — so the amount would sit on the
540
+ -- transition account for ever, settled by nothing and reported by
541
+ -- nothing, with no error anywhere. Refuse it here, where the pack can
542
+ -- still be corrected, rather than discover it in a balance years later.
543
+ if not exists (select 1 from tax_postings tp
544
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
545
+ and tp.posting_type = 'tax'
546
+ and tp.declaration_box is not null) then
547
+ 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',
548
+ r.tax_code;
549
+ end if;
550
+ end if;
551
+
552
+ -- The postings of one side share out the tax of the group; the last of
553
+ -- each side takes what is left. Until `tax_on_base` there was never more
554
+ -- than one posting per side, so this changes no existing tax by a cent —
555
+ -- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
556
+ -- where rounding each half on its own would book 0.64 against a document
557
+ -- that totals 0.63.
558
+ v_side_left := null;
559
+ v_side_left_neg := null;
560
+
561
+ for p in
562
+ select tp.posting_type, tp.factor_percent, tp.account_id,
563
+ tp.declaration_box, tp.box_factor_percent,
564
+ case when tp.factor_percent >= 0 then 1 else -1 end as side,
565
+ sum(abs(tp.factor_percent))
566
+ over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
567
+ as side_factor,
568
+ row_number() over (
569
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end
570
+ order by tp.sequence, tp.id)
571
+ = count(*) over (
572
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end)
573
+ as is_last_of_side
574
+ from tax_postings tp
575
+ where tp.tax_id = r.tax_id
576
+ and tp.document_kind = v_kind
577
+ and tp.posting_type in ('tax', 'tax_on_base')
578
+ order by tp.sequence, tp.id
579
+ loop
580
+ -- The tax of the group was rounded once, in the view. Every posting is
581
+ -- a share of that one figure, never of a re-derived one.
582
+ if p.side >= 0 then
583
+ if v_side_left is null then
584
+ v_side_left := round_amount(r.tax_amount * p.side_factor / 100, v_round);
585
+ end if;
586
+ if p.is_last_of_side then
587
+ v_amount := v_side_left;
588
+ else
589
+ v_amount := round_amount(r.tax_amount * abs(p.factor_percent) / 100, v_round);
590
+ v_side_left := v_side_left - v_amount;
591
+ end if;
592
+ else
593
+ if v_side_left_neg is null then
594
+ v_side_left_neg := round_amount(r.tax_amount * p.side_factor / 100, v_round);
595
+ end if;
596
+ if p.is_last_of_side then
597
+ v_amount := v_side_left_neg;
598
+ else
599
+ v_amount := round_amount(r.tax_amount * abs(p.factor_percent) / 100, v_round);
600
+ v_side_left_neg := v_side_left_neg - v_amount;
601
+ end if;
602
+ end if;
603
+
604
+ if v_amount = 0 then
605
+ continue;
606
+ end if;
607
+ -- A positive factor keeps the side of the base, a negative one flips it.
608
+ v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
609
+ -- The box keeps its own rounding: `box_factor_percent` was always
610
+ -- independent from `factor_percent`, because a declaration figure is
611
+ -- not a ledger figure and only the ledger has to balance.
612
+ v_box_amount := case when p.declaration_box is null then null
613
+ else round_amount(r.tax_amount * p.box_factor_percent / 100 / v_rate,
614
+ v_book_round) end;
615
+ v_book := round_amount(v_amount / v_rate, v_book_round);
616
+
617
+ if p.posting_type = 'tax' then
618
+ v_seq := v_seq + 10;
619
+
620
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
621
+ debit, credit, tax_id, tax_line, posting_type,
622
+ declaration_box, box_amount, tax_point_date,
623
+ currency_code, amount_currency)
624
+ values (v_entry.id, v_doc.company_id,
625
+ case when v_cash then v_transition else p.account_id end,
626
+ v_seq, r.tax_name,
627
+ case when v_side_credit then 0 else v_book end,
628
+ case when v_side_credit then v_book else 0 end,
629
+ r.tax_id, true, p.posting_type,
630
+ case when v_cash then null else p.declaration_box end,
631
+ v_box_amount, v_tax_point, v_doc.currency_code,
632
+ case when v_foreign then v_amount end);
633
+ continue;
634
+ end if;
635
+
636
+ -- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
637
+ -- lines it taxes, split in proportion to their base. The last share
638
+ -- takes whatever is left, so the shares add up to the amount that was
639
+ -- rounded once on the group and the entry still balances to the cent.
640
+ v_left := v_amount;
641
+ v_left_box := v_box_amount;
642
+
643
+ for g in
644
+ select account_id,
645
+ base_amount,
646
+ seq,
647
+ sum(base_amount) over () as total_base,
648
+ row_number() over (order by seq) = count(*) over () as is_last
649
+ from (
650
+ select l.account_id,
651
+ sum(l.amount_untaxed) as base_amount,
652
+ min(l.sequence) as seq
653
+ from document_lines l
654
+ where l.document_id = p_document_id
655
+ and l.line_type = 'product'
656
+ and l.tax_id = r.tax_id
657
+ group by l.account_id
658
+ having sum(l.amount_untaxed) <> 0
659
+ ) as groups
660
+ order by seq
661
+ loop
662
+ if g.is_last then
663
+ v_share := v_left;
664
+ v_share_box := v_left_box;
665
+ else
666
+ v_share := round_amount(v_amount * g.base_amount / g.total_base, v_round);
667
+ v_share_box := case when v_box_amount is null then null
668
+ else round_amount(v_box_amount * g.base_amount / g.total_base,
669
+ v_book_round) end;
670
+ v_left := v_left - v_share;
671
+ v_left_box := v_left_box - v_share_box;
672
+ end if;
673
+
674
+ if v_share = 0 then
675
+ continue;
676
+ end if;
677
+
678
+ v_seq := v_seq + 10;
679
+ v_share_book := round_amount(v_share / v_rate, v_book_round);
680
+
681
+ -- `tax_line` stays false: the amount is on a base account and belongs
682
+ -- to the base side of the declaration, which is why the Belgian grids
683
+ -- 82 and 83 report it together with the base.
684
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
685
+ debit, credit, tax_id, tax_line, posting_type,
686
+ declaration_box, box_amount, tax_point_date,
687
+ currency_code, amount_currency)
688
+ values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
689
+ case when v_side_credit then 0 else v_share_book end,
690
+ case when v_side_credit then v_share_book else 0 end,
691
+ r.tax_id, false, p.posting_type,
692
+ p.declaration_box, v_share_box, v_tax_point, v_doc.currency_code,
693
+ case when v_foreign then v_share end);
694
+ end loop;
695
+ end loop;
696
+ end loop;
697
+
698
+ -- ------------------------------------------------------------ counterpart
699
+ select total_debit - total_credit into v_diff from entries where id = v_entry.id;
700
+ select coalesce(sum(case when l.debit > 0 then l.amount_currency else -l.amount_currency end), 0)
701
+ into v_diff_cur
702
+ from entry_lines l where l.entry_id = v_entry.id;
703
+
704
+ if v_diff = 0 then
705
+ raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
706
+ end if;
707
+
708
+ v_contact := commercial_entity(v_doc.contact_id);
709
+ v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
710
+
711
+ select payment_terms_days into v_terms from contacts where id = v_contact;
712
+ v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
713
+
714
+ v_amount := abs(v_diff);
715
+ -- The counterpart balances the entry in both currencies. The total it is
716
+ -- checked against is the document's own, which is the currency
717
+ -- `amount_total` is stated in.
718
+ v_total_cur := case when v_foreign then abs(v_diff_cur) else v_amount end;
719
+ v_seq := v_seq + 10;
720
+
721
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
722
+ debit, credit, contact_id, date_maturity, currency_code,
723
+ amount_currency)
724
+ values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
725
+ case when v_diff > 0 then 0 else v_amount end,
726
+ case when v_diff > 0 then v_amount else 0 end,
727
+ v_contact, v_maturity, v_doc.currency_code,
728
+ case when v_foreign then v_total_cur end);
729
+
730
+ -- The ledger is right by construction. If the header disagrees, the header
731
+ -- is what is wrong, and we say so instead of quietly patching a line.
732
+ -- Half a unit of the document's own currency, which is what `0.005` used to
733
+ -- mean when every currency was assumed to have cents.
734
+ if abs(v_total_cur - abs(v_doc.amount_total)) > currency_unit(v_round) / 2 then
735
+ raise exception 'document_total_mismatch: document % totals % but its lines book %',
736
+ p_document_id, v_doc.amount_total, v_total_cur;
737
+ end if;
738
+
739
+ -- --------------------------------------------------------------- posting
740
+ v_entry := post_entry(v_entry.id);
741
+
742
+ update documents
743
+ set state = 'posted',
744
+ number = coalesce(number, v_entry.number),
745
+ entry_id = v_entry.id,
746
+ accounting_date = v_date,
747
+ tax_point_date = v_tax_point
748
+ where id = p_document_id;
749
+
750
+ return v_entry;
751
+ end;
752
+ $$;
753
+
754
+
755
+ -- ---------------------------------------------------------------------------
756
+ -- The transfer that makes a cash-basis tax due says so on its lines
757
+ -- ---------------------------------------------------------------------------
758
+ --
759
+ -- Replaced whole for the same reason. Both legs carry the collection date:
760
+ -- the one that lands on the account and the box it is declared in, and the one
761
+ -- that empties the transition account the amount waited on. They are one
762
+ -- posting falling due, so they carry one tax point. Everything else is
763
+ -- `20260916103000` unchanged.
764
+
765
+ create or replace function settle_cash_basis_tax(
766
+ p_document_id uuid,
767
+ p_date date
768
+ )
769
+ returns uuid
770
+ language plpgsql
771
+ as $$
772
+ declare
773
+ v_doc documents%rowtype;
774
+ v_company companies%rowtype;
775
+ v_entry entries%rowtype;
776
+ v_kind tax_document_kind;
777
+ v_total numeric;
778
+ v_paid numeric;
779
+ v_ratio numeric;
780
+ v_seq integer := 0;
781
+ v_final uuid;
782
+ v_box text;
783
+ v_target uuid;
784
+ v_due numeric;
785
+ v_done numeric;
786
+ v_delta numeric;
787
+ v_box_due numeric;
788
+ v_box_done numeric;
789
+ v_box_delta numeric;
790
+ -- The transfer is written in the ledger's currency on both sides, which is
791
+ -- the company's own: the entry it produces says so.
792
+ v_round money_rounding;
793
+ v_credit boolean;
794
+ v_type tax_posting_type;
795
+ w record;
796
+ begin
797
+ select * into v_doc from documents where id = p_document_id;
798
+ if not found or v_doc.entry_id is null then
799
+ return null;
800
+ end if;
801
+
802
+ -- Nothing is waiting on this document: the answer for every tax that falls
803
+ -- due when it is invoiced, which is most of them.
804
+ if not exists (
805
+ select 1
806
+ from entry_lines q join taxes t on t.id = q.tax_id
807
+ where q.entry_id = v_doc.entry_id
808
+ and t.cash_basis
809
+ and q.declaration_box is null
810
+ and q.box_amount is not null
811
+ ) then
812
+ return null;
813
+ end if;
814
+
815
+ select * into v_company from companies where id = v_doc.company_id;
816
+ v_round := rounding_of(v_doc.company_id);
817
+ if v_company.miscellaneous_journal_id is null then
818
+ raise exception 'no_miscellaneous_journal: company % has no journal for the transfer of a cash-basis tax',
819
+ v_doc.company_id;
820
+ end if;
821
+
822
+ v_kind := case when v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note')
823
+ then 'credit_note' else 'invoice' end::tax_document_kind;
824
+
825
+ -- What share of this document has been settled, read on its own third-party
826
+ -- lines and in the ledger's currency on both sides of the division.
827
+ select coalesce(sum(abs(tl.debit - tl.credit)), 0), coalesce(sum(tl.matched_amount), 0)
828
+ into v_total, v_paid
829
+ from entry_lines tl join accounts a on a.id = tl.account_id
830
+ where tl.entry_id = v_doc.entry_id
831
+ and a.reconcilable
832
+ and a.account_type in ('asset_receivable', 'liability_payable');
833
+
834
+ if v_total = 0 then
835
+ return null;
836
+ end if;
837
+ -- An overpayment settles the document, and no more: a tax is due on what
838
+ -- was invoiced.
839
+ v_ratio := least(1, greatest(0, v_paid / v_total));
840
+
841
+ for w in
842
+ select l2.id, l2.account_id, l2.tax_id, l2.tax_line, l2.box_amount,
843
+ l2.debit, l2.credit, l2.name, l2.sequence
844
+ from entry_lines l2 join taxes t on t.id = l2.tax_id
845
+ where l2.entry_id = v_doc.entry_id
846
+ and t.cash_basis
847
+ and l2.declaration_box is null
848
+ and l2.box_amount is not null
849
+ order by l2.sequence
850
+ loop
851
+ select tp.account_id, tp.declaration_box, tp.posting_type
852
+ into v_final, v_box, v_type
853
+ from tax_postings tp
854
+ where tp.tax_id = w.tax_id
855
+ and tp.document_kind = v_kind
856
+ and tp.posting_type = (case when w.tax_line then 'tax' else 'base' end)::tax_posting_type
857
+ limit 1;
858
+
859
+ -- The account the transfer of this line lands on, which is also how an
860
+ -- earlier transfer of the same line is recognised.
861
+ v_target := case when w.tax_line then v_final else w.account_id end;
862
+ if v_target is null then
863
+ raise exception 'no_cash_basis_target: the tax of line % names no account to fall due on', w.id;
864
+ end if;
865
+ -- A line only waits because a box was worked out for it, and that box came
866
+ -- from this very posting. If it has none, the amount would wait for ever.
867
+ if v_box is null then
868
+ raise exception 'no_cash_basis_box: the tax of line % holds an amount for a box the posting does not name', w.id;
869
+ end if;
870
+
871
+ if w.tax_line then
872
+ v_due := round_amount((w.debit + w.credit) * v_ratio, v_round);
873
+ select coalesce(sum(case when w.credit > 0 then x.credit - x.debit
874
+ else x.debit - x.credit end), 0)
875
+ into v_done
876
+ from entry_lines x join entries e on e.id = x.entry_id
877
+ where e.document_id = p_document_id
878
+ and e.id <> v_doc.entry_id
879
+ and x.tax_id = w.tax_id
880
+ and x.tax_line
881
+ and x.account_id = v_target;
882
+ else
883
+ v_due := 0;
884
+ v_done := 0;
885
+ end if;
886
+ v_delta := v_due - v_done;
887
+
888
+ v_box_due := round_amount(w.box_amount * v_ratio, v_round);
889
+ select coalesce(sum(x.box_amount), 0)
890
+ into v_box_done
891
+ from entry_lines x join entries e on e.id = x.entry_id
892
+ where e.document_id = p_document_id
893
+ and e.id <> v_doc.entry_id
894
+ and x.tax_id = w.tax_id
895
+ and x.tax_line = w.tax_line
896
+ and x.account_id = v_target;
897
+ v_box_delta := v_box_due - v_box_done;
898
+
899
+ if v_delta = 0 and v_box_delta = 0 then
900
+ continue;
901
+ end if;
902
+
903
+ if v_entry.id is null then
904
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
905
+ description, state, document_id, currency_code)
906
+ values (v_doc.company_id, v_company.miscellaneous_journal_id,
907
+ fiscal_year_at(v_doc.company_id, p_date), p_date,
908
+ v_doc.number,
909
+ coalesce(v_doc.number, 'document') || ' — tax due on settlement',
910
+ 'draft', p_document_id, v_company.currency_code)
911
+ returning * into v_entry;
912
+ end if;
913
+
914
+ -- The side the document put the tax on, kept when the share grows and
915
+ -- flipped when it shrinks.
916
+ v_credit := (w.credit > 0) = (v_delta > 0);
917
+
918
+ v_seq := v_seq + 10;
919
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
920
+ debit, credit, tax_id, tax_line, posting_type,
921
+ declaration_box, box_amount, tax_point_date, currency_code)
922
+ values (v_entry.id, v_doc.company_id, v_target, v_seq, w.name,
923
+ case when v_credit then 0 else abs(v_delta) end,
924
+ case when v_credit then abs(v_delta) else 0 end,
925
+ w.tax_id, w.tax_line, v_type,
926
+ v_box, v_box_delta,
927
+ -- A tax that waits falls due on the day it is collected, and that
928
+ -- is the date this transfer carries. Saying it on the line rather
929
+ -- than leaving the entry's date to speak for it is what lets the
930
+ -- declaration read one column for every tax point there is.
931
+ p_date,
932
+ v_company.currency_code);
933
+
934
+ if v_delta <> 0 then
935
+ v_seq := v_seq + 10;
936
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
937
+ debit, credit, tax_id, tax_line, posting_type,
938
+ tax_point_date, currency_code)
939
+ values (v_entry.id, v_doc.company_id, w.account_id, v_seq, w.name,
940
+ case when v_credit then abs(v_delta) else 0 end,
941
+ case when v_credit then 0 else abs(v_delta) end,
942
+ w.tax_id, true, v_type, p_date, v_company.currency_code);
943
+ end if;
944
+ end loop;
945
+
946
+ if v_entry.id is null then
947
+ return null;
948
+ end if;
949
+
950
+ v_entry := post_entry(v_entry.id);
951
+ return v_entry.id;
952
+ end;
953
+ $$;
954
+
955
+
956
+ -- ---------------------------------------------------------------------------
957
+ -- The declaration reads the day the tax fell due
958
+ -- ---------------------------------------------------------------------------
959
+ --
960
+ -- Replaced whole for the same reason. One filter moves: a figure belongs to
961
+ -- the period its tax fell due in, and the entry's date answers only where the
962
+ -- line has nothing to say. Everything else is `20260916094500` unchanged,
963
+ -- cadence guard included.
964
+ --
965
+ -- `ec_sales_list()` is deliberately **not** changed with it. The recapitulative
966
+ -- statement declares an intra-Community supply, and the moment that arises is
967
+ -- its own article in both countries read here — KMS § 11 lg 2 in Estonia, art.
968
+ -- 17, § 2 in Belgium — neither of which is the rule `tax_point_rule` carries.
969
+ -- Moving the statement onto a date computed from the general rule would make
970
+ -- it wrong in a new way. `docs/international.md` records it as a gap.
971
+
972
+ create or replace function vat_return(
973
+ p_company_id uuid,
974
+ p_from date,
975
+ p_to date,
976
+ p_report_code text default null
977
+ )
978
+ returns table (
979
+ box text,
980
+ kind text,
981
+ amount numeric,
982
+ computed boolean,
983
+ name text,
984
+ sequence integer,
985
+ print_sequence integer,
986
+ hidden boolean,
987
+ report_code text
988
+ )
989
+ language plpgsql
990
+ stable
991
+ as $$
992
+ declare
993
+ -- 'box|kind' -> amount, for every box summed from the ledger, then the
994
+ -- totals `evaluate_totals()` derives from them. The key carries the kind
995
+ -- because the French CA3 puts a base and a tax on line 08 and a formula has
996
+ -- to be able to name one of them.
997
+ v_values jsonb := '{}'::jsonb;
998
+ v_formulas jsonb := '[]'::jsonb;
999
+ v_totals jsonb := '{}'::jsonb;
1000
+ v_rows jsonb := '[]'::jsonb;
1001
+ v_country char(2);
1002
+ v_report text;
1003
+ v_in char(2);
1004
+ v_count integer;
1005
+ v_codes text;
1006
+ -- How often this company files, what the form accepts, and what the two
1007
+ -- dates asked for actually are.
1008
+ v_files declaration_period;
1009
+ v_accepts declaration_period[];
1010
+ v_asked declaration_period;
1011
+ -- A declaration figure is not a ledger figure, but it is written in the
1012
+ -- same currency and with the same decimals.
1013
+ v_round money_rounding;
1014
+ r record;
1015
+ begin
1016
+ select c.fiscal_country into v_country
1017
+ from companies c where c.id = p_company_id;
1018
+ if not found then
1019
+ raise exception 'unknown_company: %', p_company_id;
1020
+ end if;
1021
+ v_round := rounding_of(p_company_id);
1022
+
1023
+ -- Which form. The caller names one, or the country files exactly one on
1024
+ -- that date. Two and no name is a question only the caller can answer — a
1025
+ -- Canadian company files the federal return and the Québec one at once — so
1026
+ -- this asks instead of guessing.
1027
+ if p_report_code is not null then
1028
+ select t.country, t.code into v_in, v_report
1029
+ from tax_report_templates t
1030
+ where t.code = p_report_code
1031
+ and t.valid_from <= p_to
1032
+ and (t.valid_to is null or t.valid_to >= p_to)
1033
+ order by t.country
1034
+ limit 1;
1035
+ if v_report is null then
1036
+ raise exception 'unknown_tax_report: % is not a declaration form in force on %',
1037
+ p_report_code, p_to;
1038
+ end if;
1039
+ else
1040
+ select count(*), min(t.code), string_agg(t.code, ', ' order by t.code)
1041
+ into v_count, v_report, v_codes
1042
+ from tax_report_templates t
1043
+ where t.country = v_country
1044
+ and t.is_periodic_return
1045
+ and t.valid_from <= p_to
1046
+ and (t.valid_to is null or t.valid_to >= p_to);
1047
+ if v_count > 1 then
1048
+ raise exception 'ambiguous_tax_report: % files several declarations on % (%); name one',
1049
+ v_country, p_to, v_codes;
1050
+ end if;
1051
+ v_in := v_country;
1052
+ if v_count = 0 then
1053
+ v_report := null; -- no pack for this country: the ledger boxes, and no total.
1054
+ end if;
1055
+ end if;
1056
+
1057
+ -- The period asked for against the one this company files **this form** on.
1058
+ -- Four things have to hold before this refuses, and the fourth is what keeps
1059
+ -- it out of everybody's way: the company has recorded a cadence for this
1060
+ -- declaration, the form is filed on that cadence, the dates are themselves a
1061
+ -- whole cadence of that form, and the two are not the same. A fortnight, a
1062
+ -- half-year, a form the company has recorded nothing about — none of those is
1063
+ -- a filing on the wrong cadence, and none of them is refused.
1064
+ if v_report is not null then
1065
+ v_files := filing_period(p_company_id, v_report);
1066
+ if v_files is not null then
1067
+ select t.periods into v_accepts
1068
+ from tax_report_templates t
1069
+ where t.country = v_in and t.code = v_report;
1070
+ v_asked := declaration_period_of(p_from, p_to);
1071
+ if v_asked is not null
1072
+ and v_files = any(v_accepts)
1073
+ and v_asked = any(v_accepts)
1074
+ and v_asked <> v_files then
1075
+ raise exception
1076
+ 'wrong_declaration_period: this company files % returns on %; % to % is a %',
1077
+ v_files, v_report, p_from, p_to, v_asked;
1078
+ end if;
1079
+ end if;
1080
+ end if;
1081
+
1082
+ -- 1. What the tax postings wrote on the ledger, in every box each of them
1083
+ -- names. A line carries the box it is known by; the posting behind it
1084
+ -- carries the whole list, which is one box for all but the handful of
1085
+ -- forms that print a figure twice. No country rule here either: the
1086
+ -- expansion is `unnest`, and which boxes there are is the pack's answer.
1087
+ for r in
1088
+ with lines as (
1089
+ select l.declaration_box as lbox,
1090
+ case when l.tax_line then 'tax' else 'base' end as lkind,
1091
+ l.tax_id as ltax,
1092
+ l.box_amount as lamount
1093
+ from entry_lines l
1094
+ join entries e on e.id = l.entry_id
1095
+ where l.company_id = p_company_id
1096
+ and e.state = 'posted'
1097
+ -- The period a figure belongs to is the day its tax fell due, and
1098
+ -- that is the line's own `tax_point_date` where one was worked out.
1099
+ -- Null is every line written before the column existed and every line
1100
+ -- of a country that declares no rule: the entry's date, exactly as
1101
+ -- before.
1102
+ and coalesce(l.tax_point_date, e.entry_date) between p_from and p_to
1103
+ and l.declaration_box is not null
1104
+ ),
1105
+ -- The posting that wrote the line: which form it is on, and every box it
1106
+ -- prints in. A line with no posting behind it — an entry keyed by hand,
1107
+ -- a tax a company wrote itself — is on this form and in the one box it
1108
+ -- names, which is what the left join leaves.
1109
+ sourced as (
1110
+ select ln.lbox, ln.lkind, ln.lamount,
1111
+ coalesce(p.boxes, array[ln.lbox]) as lboxes
1112
+ from lines ln
1113
+ left join lateral (
1114
+ select min(tp.report_code) as report_code,
1115
+ array_agg(distinct b.box) as boxes
1116
+ from tax_postings tp
1117
+ cross join lateral unnest(tp.declaration_boxes) as b(box)
1118
+ where tp.tax_id = ln.ltax
1119
+ and tp.declaration_box = ln.lbox
1120
+ and tp.posting_type = ln.lkind::tax_posting_type
1121
+ ) p on true
1122
+ -- A box number belongs to one form. A line whose posting names
1123
+ -- another form is not on this declaration; one that names none is
1124
+ -- the single-return case every European company is in.
1125
+ where v_report is null or coalesce(p.report_code, v_report) = v_report
1126
+ ),
1127
+ ledger as (
1128
+ select x.box as lbox, s.lkind,
1129
+ round_amount(sum(s.lamount), v_round) as lamount
1130
+ from sourced s
1131
+ cross join lateral unnest(s.lboxes) as x(box)
1132
+ group by 1, 2
1133
+ having round_amount(sum(s.lamount), v_round) <> 0
1134
+ )
1135
+ select g.lbox, g.lkind, g.lamount, b.name as lname,
1136
+ b.sequence as lsequence,
1137
+ coalesce(b.print_sequence, b.sequence) as lprint,
1138
+ coalesce(b.hidden, false) as lhidden
1139
+ from ledger g
1140
+ left join tax_report_box_templates b
1141
+ on b.country = v_in and b.report_code = v_report
1142
+ and b.box = g.lbox and b.kind = g.lkind
1143
+ order by coalesce(b.sequence, 2147483647), g.lbox, g.lkind
1144
+ loop
1145
+ v_values := v_values || jsonb_build_object(r.lbox || '|' || r.lkind, r.lamount);
1146
+ v_rows := v_rows || jsonb_build_array(jsonb_build_object(
1147
+ 'box', r.lbox, 'kind', r.lkind, 'amount', r.lamount, 'computed', false,
1148
+ 'name', r.lname, 'sequence', r.lsequence, 'print_sequence', r.lprint,
1149
+ 'hidden', r.lhidden, 'report_code', v_report));
1150
+ end loop;
1151
+
1152
+ -- 2. The totals of the form, through the evaluator the statements use. A
1153
+ -- return prints what it has, so a nil total is left out of the answer —
1154
+ -- and kept in the working set, so a later total that names it reads a
1155
+ -- zero rather than a gap.
1156
+ if v_report is not null then
1157
+ select coalesce(jsonb_agg(jsonb_build_object(
1158
+ 'key', b.box || '|total', 'plus', to_jsonb(b.plus_boxes),
1159
+ 'minus', to_jsonb(b.minus_boxes), 'floor_zero', b.floor_zero,
1160
+ 'rate', b.rate, 'rate_of', b.rate_of_box,
1161
+ 'sequence', b.sequence
1162
+ ) order by b.sequence, b.box), '[]'::jsonb)
1163
+ into v_formulas
1164
+ from tax_report_box_templates b
1165
+ where b.country = v_in
1166
+ and b.report_code = v_report
1167
+ and b.kind = 'total'
1168
+ and (b.valid_from is null or b.valid_from <= p_to)
1169
+ and (b.valid_to is null or b.valid_to >= p_to);
1170
+
1171
+ v_totals := evaluate_totals(v_values, v_formulas, v_round, false);
1172
+
1173
+ for r in
1174
+ select b.box as tbox, b.name as tname, b.sequence as tsequence,
1175
+ coalesce(b.print_sequence, b.sequence) as tprint, b.hidden as thidden
1176
+ from tax_report_box_templates b
1177
+ where b.country = v_in
1178
+ and b.report_code = v_report
1179
+ and b.kind = 'total'
1180
+ and (b.valid_from is null or b.valid_from <= p_to)
1181
+ and (b.valid_to is null or b.valid_to >= p_to)
1182
+ and v_totals ? (b.box || '|total')
1183
+ order by b.sequence, b.box
1184
+ loop
1185
+ v_rows := v_rows || jsonb_build_array(jsonb_build_object(
1186
+ 'box', r.tbox, 'kind', 'total',
1187
+ 'amount', (v_totals ->> (r.tbox || '|total'))::numeric, 'computed', true,
1188
+ 'name', r.tname, 'sequence', r.tsequence, 'print_sequence', r.tprint,
1189
+ 'hidden', r.thidden, 'report_code', v_report));
1190
+ end loop;
1191
+ end if;
1192
+
1193
+ return query
1194
+ select (x ->> 'box')::text,
1195
+ (x ->> 'kind')::text,
1196
+ (x ->> 'amount')::numeric,
1197
+ (x ->> 'computed')::boolean,
1198
+ (x ->> 'name')::text,
1199
+ (x ->> 'sequence')::integer,
1200
+ (x ->> 'print_sequence')::integer,
1201
+ (x ->> 'hidden')::boolean,
1202
+ (x ->> 'report_code')::text
1203
+ from jsonb_array_elements(v_rows) as x;
1204
+ end;
1205
+ $$;
1206
+
1207
+ comment on function vat_return(uuid, date, date, text) is
1208
+ 'Declaration boxes for a period: summed from the ledger by the day each figure''s tax fell due — `entry_lines.tax_point_date`, the entry''s own date where the line carries none — then the totals of the country''s form worked out by evaluate_totals(), the same evaluator financial_statement() uses. Refuses a period the company does not file **this form** on, when it has recorded one for it. No country rule lives in this function.';
1209
+
1210
+ revoke execute on function vat_return(uuid, date, date, text) from public, anon;
1211
+ grant execute on function vat_return(uuid, date, date, text) to authenticated, service_role;