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,1189 @@
1
+ -- Ekwo OS — VAT when the cash moves, and the exchange difference when it settles.
2
+ --
3
+ -- Two things a country decides that the core could not say yet, and they meet
4
+ -- in the same place: the moment a document is settled.
5
+ --
6
+ -- 1. **A tax that falls due on collection.** `taxes.cash_basis` and
7
+ -- `cash_basis_transition_account_id` landed with the tax engine and nothing
8
+ -- read them. From here `post_document` books such a tax on the transition
9
+ -- account **and on no declaration box**, because nothing is due yet, and
10
+ -- the matching moves the settled share to the final account with the box.
11
+ -- The share is pro rata of what has been settled, cumulative, so a partial
12
+ -- payment carries its part and the last one carries the remainder to the
13
+ -- cent. The whole operation waits, base and tax together: a return whose
14
+ -- base is declared in one period and whose tax is declared in the next is
15
+ -- a return that does not tie out, and the base column of a line is the
16
+ -- base *collected*.
17
+ --
18
+ -- This is not cash accounting as a ledger. Revenue and expense are booked
19
+ -- when the document is, which is what every chart in scope expects; only
20
+ -- the declaration waits. A cash-basis *report* is derived from matched
21
+ -- payments, and stays derived.
22
+ --
23
+ -- 2. **The exchange difference realised at matching.** A receivable booked at
24
+ -- one rate and collected at another leaves a residual in the company's
25
+ -- currency that no payment will ever clear. It is a gain or a loss, and it
26
+ -- is booked as such on the accounts the pack names, so the third-party
27
+ -- account goes to nil. Revaluation of open items — the difference that is
28
+ -- not realised, at a closing date — stays out of scope.
29
+ --
30
+ -- What had to be fixed on the way: **the ledger did not convert**. A document
31
+ -- in a foreign currency booked its foreign figures as if they were the
32
+ -- company's own, and `entry_lines.amount_currency` — which the FEC exports and
33
+ -- which this change needs — was never written by anything. So `post_document`
34
+ -- and `post_payment` now book the company's currency in `debit`/`credit` and
35
+ -- the document's in `amount_currency`, at the rate the document or the payment
36
+ -- carries. There is deliberately no rate feed: the rate is an input, as
37
+ -- `documents.exchange_rate` has been since the first migration.
38
+
39
+ -- ---------------------------------------------------------------------------
40
+ -- Where an exchange difference lands
41
+ --
42
+ -- Two columns, nullable, with no default. A country that has not named them
43
+ -- gets a refusal the day a difference actually arises, and never somebody
44
+ -- else's account: an exchange gain sits in the financial income of the chart
45
+ -- the company keeps, and that number is a fact about a chart, not a constant.
46
+ -- ---------------------------------------------------------------------------
47
+
48
+ alter table country_defaults
49
+ add column if not exists fx_gain_code text,
50
+ add column if not exists fx_loss_code text;
51
+
52
+ comment on column country_defaults.fx_gain_code is
53
+ 'Account a realised exchange gain is booked on, from the pack. Null until the pack names one, and then a matching that realises a gain is refused rather than booked somewhere plausible.';
54
+ comment on column country_defaults.fx_loss_code is
55
+ 'The same for a realised loss. A pair, because every chart in scope keeps the gain and the loss apart.';
56
+
57
+ -- ---------------------------------------------------------------------------
58
+ -- The rate a payment was made at
59
+ --
60
+ -- `documents.exchange_rate` has existed since the first migration and nothing
61
+ -- read it. A payment had no equivalent at all, and without one a payment in a
62
+ -- foreign currency cannot differ from the invoice — which is the entire
63
+ -- subject. Same convention on both, stated here once.
64
+ -- ---------------------------------------------------------------------------
65
+
66
+ alter table payments
67
+ add column if not exists exchange_rate numeric(18, 8) not null default 1;
68
+
69
+ alter table payments
70
+ drop constraint if exists payments_rate_positive;
71
+ alter table payments
72
+ add constraint payments_rate_positive check (exchange_rate > 0);
73
+
74
+ comment on column payments.exchange_rate is
75
+ 'Units of the payment currency for one unit of the company currency, as currency_rates states it. The ledger amount is the payment amount divided by it. 1 when the payment is in the company currency.';
76
+ comment on column documents.exchange_rate is
77
+ 'Units of the document currency for one unit of the company currency, as currency_rates states it. The ledger amount is the document amount divided by it. 1 when the document is in the company currency.';
78
+ comment on column entry_lines.amount_currency is
79
+ 'The amount of this line in its own currency, written whenever that currency is not the company''s. Positive like debit and credit; the side carries the sign.';
80
+ comment on column entry_lines.box_amount is
81
+ 'Amount to report in that box, in the sign the form expects. A line of a cash-basis tax carries the amount with no box: it is computed when the document is posted and waits for the matching that names the box it is finally reported in.';
82
+
83
+ -- ---------------------------------------------------------------------------
84
+ -- What a matching had to book
85
+ --
86
+ -- A matching is not supposed to touch the accounts — that is the sentence the
87
+ -- MCP server prints when it undoes one. It stays true of the matching itself
88
+ -- and stops being true of what a matching *reveals*: a tax that falls due, a
89
+ -- difference that is realised. Both are entries of their own, and the row that
90
+ -- caused them says so, so a client can show them and `unreconcile` can find
91
+ -- them.
92
+ -- ---------------------------------------------------------------------------
93
+
94
+ alter table reconciliations
95
+ add column if not exists fx_entry_id uuid references entries(id) on delete set null,
96
+ add column if not exists tax_transfer_entry_id uuid references entries(id) on delete set null;
97
+
98
+ comment on column reconciliations.fx_entry_id is
99
+ 'Entry that booked the exchange difference this matching realised, when there was one.';
100
+ comment on column reconciliations.tax_transfer_entry_id is
101
+ 'Entry that moved the cash-basis tax this matching made due, when there was one.';
102
+
103
+ create index if not exists reconciliations_fx_entry_idx on reconciliations (fx_entry_id);
104
+ create index if not exists reconciliations_tax_entry_idx on reconciliations (tax_transfer_entry_id);
105
+
106
+ -- ---------------------------------------------------------------------------
107
+ -- What a document has been settled by, when it is not in the company currency
108
+ --
109
+ -- `amount_paid` is compared with `amount_total`, which is the document's own
110
+ -- currency, and it was summing `matched_amount`, which is the ledger's. The
111
+ -- two were the same number as long as nothing converted. Now that something
112
+ -- does, the settled share is read back through the currency amount of the line
113
+ -- it was matched on. A line with no currency amount contributes what it always
114
+ -- did, so a document in the company's currency answers exactly as before.
115
+ -- ---------------------------------------------------------------------------
116
+
117
+ create or replace function documents_refresh_amount_paid(p_document_id uuid)
118
+ returns void
119
+ language plpgsql
120
+ as $$
121
+ begin
122
+ -- The statement names amount_paid so that documents_refresh_payment_state,
123
+ -- which watches that column, fires in turn.
124
+ update documents d
125
+ set amount_paid = coalesce((
126
+ select sum(case
127
+ when l.amount_currency is null then l.matched_amount
128
+ else round(l.matched_amount * l.amount_currency
129
+ / nullif(abs(l.debit - l.credit), 0), 2)
130
+ end)
131
+ from entry_lines l
132
+ join accounts a on a.id = l.account_id
133
+ where l.entry_id = d.entry_id
134
+ and a.reconcilable
135
+ and a.account_type in ('asset_receivable', 'liability_payable')
136
+ ), 0)
137
+ where d.id = p_document_id
138
+ and d.entry_id is not null;
139
+ end;
140
+ $$;
141
+
142
+ -- ---------------------------------------------------------------------------
143
+ -- post_document — the same function, converting, and deferring what is not due
144
+ --
145
+ -- Three changes, and nothing else moves:
146
+ --
147
+ -- * every amount is worked out in the document's currency exactly as
148
+ -- before, then divided by the rate to give the ledger amount. At a rate
149
+ -- of 1 the division is the identity, which is why `posting.test.ts` is
150
+ -- untouched and green.
151
+ -- * the counterpart still balances by construction, in both currencies; the
152
+ -- total it is checked against is the one in the document's currency,
153
+ -- which is the currency `documents.amount_total` is stated in.
154
+ -- * a tax on a cash basis lands on its transition account and names no box,
155
+ -- and so does the base it is computed on. The box amount is worked out
156
+ -- here all the same and waits on the line: it is what the matching will
157
+ -- report, pro rata, and working it out twice is how two answers appear.
158
+ -- ---------------------------------------------------------------------------
159
+
160
+ create or replace function post_document(p_document_id uuid)
161
+ returns entries
162
+ language plpgsql
163
+ as $$
164
+ declare
165
+ v_doc documents%rowtype;
166
+ v_entry entries%rowtype;
167
+ v_journal uuid;
168
+ v_date date;
169
+ v_is_sale boolean;
170
+ v_is_credit boolean;
171
+ v_kind tax_document_kind;
172
+ v_base_credit boolean;
173
+ v_seq integer := 0;
174
+ v_contact uuid;
175
+ v_maturity date;
176
+ v_terms smallint;
177
+ v_counterpart uuid;
178
+ v_diff numeric(16, 2);
179
+ v_diff_cur numeric(16, 2);
180
+ v_amount numeric(16, 2);
181
+ v_book numeric(16, 2);
182
+ v_box_amount numeric(16, 2);
183
+ v_share numeric(16, 2);
184
+ v_share_book numeric(16, 2);
185
+ v_share_box numeric(16, 2);
186
+ v_left numeric(16, 2);
187
+ v_left_box numeric(16, 2);
188
+ v_side_left numeric(16, 2);
189
+ v_side_left_neg numeric(16, 2);
190
+ v_side_credit boolean;
191
+ v_label text;
192
+ v_rate numeric(18, 8);
193
+ v_home char(3);
194
+ v_foreign boolean;
195
+ v_total_cur numeric(16, 2);
196
+ v_cash boolean;
197
+ v_transition uuid;
198
+ v_postings integer;
199
+ r record;
200
+ p record;
201
+ g record;
202
+ begin
203
+ select * into v_doc from documents where id = p_document_id for update;
204
+ if not found then
205
+ raise exception 'unknown_document: document % does not exist', p_document_id;
206
+ end if;
207
+ if v_doc.state = 'posted' then
208
+ raise exception 'document_already_posted: document % is already posted', p_document_id;
209
+ end if;
210
+ if v_doc.state = 'cancelled' then
211
+ raise exception 'document_cancelled: document % cannot be posted', p_document_id;
212
+ end if;
213
+ if v_doc.entry_id is not null then
214
+ raise exception 'document_already_booked: document % already points at entry %',
215
+ p_document_id, v_doc.entry_id;
216
+ end if;
217
+
218
+ if v_doc.doc_type in ('sale_quote', 'purchase_order') then
219
+ raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
220
+ end if;
221
+
222
+ v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
223
+ v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
224
+ v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
225
+ -- Sale invoice and purchase credit note credit the base; the other two debit it.
226
+ v_base_credit := (v_is_sale <> v_is_credit);
227
+
228
+ v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
229
+
230
+ if not exists (
231
+ select 1 from document_lines
232
+ where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
233
+ ) then
234
+ raise exception 'document_empty: document % has no billable line', p_document_id;
235
+ end if;
236
+
237
+ -- A fixed-amount tax has no basis to spread over lines; refuse rather than
238
+ -- guess.
239
+ if exists (
240
+ select 1 from document_lines l join taxes t on t.id = l.tax_id
241
+ where l.document_id = p_document_id and t.amount_type <> 'percent'
242
+ ) then
243
+ raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
244
+ end if;
245
+
246
+ -- Every tax used must be in force on the accounting date.
247
+ for r in
248
+ select distinct t.id, t.code, t.valid_from, t.valid_to
249
+ from document_lines l join taxes t on t.id = l.tax_id
250
+ where l.document_id = p_document_id
251
+ loop
252
+ if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
253
+ raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
254
+ end if;
255
+ end loop;
256
+
257
+ -- Totals are derived; make sure they reflect the lines as they stand now.
258
+ perform documents_refresh_totals(p_document_id);
259
+ select * into v_doc from documents where id = p_document_id;
260
+
261
+ select c.currency_code into v_home from companies c where c.id = v_doc.company_id;
262
+ v_rate := v_doc.exchange_rate;
263
+ v_foreign := v_doc.currency_code <> v_home;
264
+
265
+ v_journal := coalesce(
266
+ v_doc.journal_id,
267
+ case when v_is_sale
268
+ then (select sales_journal_id from companies where id = v_doc.company_id)
269
+ else (select purchase_journal_id from companies where id = v_doc.company_id)
270
+ end
271
+ );
272
+ if v_journal is null then
273
+ raise exception 'no_journal: set journal_id on the document or a default journal on the company';
274
+ end if;
275
+
276
+ perform assert_period_open(v_doc.company_id, v_date, true);
277
+
278
+ v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
279
+
280
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
281
+ description, state, document_id, currency_code)
282
+ values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
283
+ coalesce(v_doc.number, v_doc.supplier_reference),
284
+ v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
285
+ then ' / ' || v_doc.supplier_reference else '' end,
286
+ 'draft', p_document_id, v_doc.currency_code)
287
+ returning * into v_entry;
288
+
289
+ -- ------------------------------------------------------------------ bases
290
+ for r in
291
+ select l.account_id,
292
+ l.tax_id,
293
+ sum(l.amount_untaxed) as base_amount,
294
+ min(l.sequence) as seq,
295
+ string_agg(distinct l.name, ', ') as label
296
+ from document_lines l
297
+ where l.document_id = p_document_id
298
+ and l.line_type = 'product'
299
+ group by l.account_id, l.tax_id
300
+ having sum(l.amount_untaxed) <> 0
301
+ order by 4
302
+ loop
303
+ select tp.declaration_box, tp.factor_percent, tp.box_factor_percent,
304
+ coalesce(t.cash_basis, false) as cash_basis
305
+ into p
306
+ from tax_postings tp
307
+ join taxes t on t.id = tp.tax_id
308
+ where tp.tax_id = r.tax_id
309
+ and tp.document_kind = v_kind
310
+ and tp.posting_type = 'base'
311
+ limit 1;
312
+
313
+ v_amount := round(r.base_amount * coalesce(p.factor_percent, 100) / 100, 2);
314
+ v_book := round(v_amount / v_rate, 2);
315
+ v_seq := v_seq + 10;
316
+
317
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
318
+ debit, credit, tax_id, tax_line,
319
+ declaration_box, box_amount, currency_code, amount_currency)
320
+ values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
321
+ case when v_base_credit then 0 else v_book end,
322
+ case when v_base_credit then v_book else 0 end,
323
+ r.tax_id, false,
324
+ case when coalesce(p.cash_basis, false) then null else p.declaration_box end,
325
+ case when p.declaration_box is null then null
326
+ else round(r.base_amount * coalesce(p.box_factor_percent, 100) / 100 / v_rate, 2) end,
327
+ v_doc.currency_code,
328
+ case when v_foreign then v_amount end);
329
+ end loop;
330
+
331
+ -- ------------------------------------------------------------------ taxes
332
+ for r in
333
+ select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
334
+ from document_tax_summary s
335
+ where s.document_id = p_document_id
336
+ and s.tax_id is not null
337
+ and s.tax_amount <> 0
338
+ order by s.tax_code
339
+ loop
340
+ select t.cash_basis, t.cash_basis_transition_account_id
341
+ into v_cash, v_transition
342
+ from taxes t where t.id = r.tax_id;
343
+
344
+ if v_cash then
345
+ -- A tax that waits needs somewhere to wait. Refuse by name rather than
346
+ -- book it on the account it is due on, which would make it due.
347
+ if v_transition is null then
348
+ raise exception 'no_cash_basis_account: tax % falls due on collection and names no transition account',
349
+ r.tax_code;
350
+ end if;
351
+ -- One posting per side, or the transition lines of a document cannot be
352
+ -- told apart when the matching sends each of them on. A tax whose
353
+ -- postings net out has nothing waiting to collect anyway.
354
+ select count(*) into v_postings
355
+ from tax_postings tp
356
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
357
+ and tp.posting_type = 'tax';
358
+ if v_postings > 1 then
359
+ raise exception 'cash_basis_split_tax: tax % falls due on collection and has % tax postings; it takes one',
360
+ r.tax_code, v_postings;
361
+ end if;
362
+ if exists (select 1 from tax_postings tp
363
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
364
+ and tp.posting_type = 'tax_on_base') then
365
+ raise exception 'cash_basis_tax_on_base: tax % falls due on collection and carries a non-deductible share; a cost is not deferred',
366
+ r.tax_code;
367
+ end if;
368
+ end if;
369
+
370
+ -- The postings of one side share out the tax of the group; the last of
371
+ -- each side takes what is left. Until `tax_on_base` there was never more
372
+ -- than one posting per side, so this changes no existing tax by a cent —
373
+ -- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
374
+ -- where rounding each half on its own would book 0.64 against a document
375
+ -- that totals 0.63.
376
+ v_side_left := null;
377
+ v_side_left_neg := null;
378
+
379
+ for p in
380
+ select tp.posting_type, tp.factor_percent, tp.account_id,
381
+ tp.declaration_box, tp.box_factor_percent,
382
+ case when tp.factor_percent >= 0 then 1 else -1 end as side,
383
+ sum(abs(tp.factor_percent))
384
+ over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
385
+ as side_factor,
386
+ row_number() over (
387
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end
388
+ order by tp.sequence, tp.id)
389
+ = count(*) over (
390
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end)
391
+ as is_last_of_side
392
+ from tax_postings tp
393
+ where tp.tax_id = r.tax_id
394
+ and tp.document_kind = v_kind
395
+ and tp.posting_type in ('tax', 'tax_on_base')
396
+ order by tp.sequence, tp.id
397
+ loop
398
+ -- The tax of the group was rounded once, in the view. Every posting is
399
+ -- a share of that one figure, never of a re-derived one.
400
+ if p.side >= 0 then
401
+ if v_side_left is null then
402
+ v_side_left := round(r.tax_amount * p.side_factor / 100, 2);
403
+ end if;
404
+ if p.is_last_of_side then
405
+ v_amount := v_side_left;
406
+ else
407
+ v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
408
+ v_side_left := v_side_left - v_amount;
409
+ end if;
410
+ else
411
+ if v_side_left_neg is null then
412
+ v_side_left_neg := round(r.tax_amount * p.side_factor / 100, 2);
413
+ end if;
414
+ if p.is_last_of_side then
415
+ v_amount := v_side_left_neg;
416
+ else
417
+ v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
418
+ v_side_left_neg := v_side_left_neg - v_amount;
419
+ end if;
420
+ end if;
421
+
422
+ if v_amount = 0 then
423
+ continue;
424
+ end if;
425
+ -- A positive factor keeps the side of the base, a negative one flips it.
426
+ v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
427
+ -- The box keeps its own rounding: `box_factor_percent` was always
428
+ -- independent from `factor_percent`, because a declaration figure is
429
+ -- not a ledger figure and only the ledger has to balance.
430
+ v_box_amount := case when p.declaration_box is null then null
431
+ else round(r.tax_amount * p.box_factor_percent / 100 / v_rate, 2) end;
432
+ v_book := round(v_amount / v_rate, 2);
433
+
434
+ if p.posting_type = 'tax' then
435
+ v_seq := v_seq + 10;
436
+
437
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
438
+ debit, credit, tax_id, tax_line,
439
+ declaration_box, box_amount, currency_code, amount_currency)
440
+ values (v_entry.id, v_doc.company_id,
441
+ case when v_cash then v_transition else p.account_id end,
442
+ v_seq, r.tax_name,
443
+ case when v_side_credit then 0 else v_book end,
444
+ case when v_side_credit then v_book else 0 end,
445
+ r.tax_id, true,
446
+ case when v_cash then null else p.declaration_box end,
447
+ v_box_amount, v_doc.currency_code,
448
+ case when v_foreign then v_amount end);
449
+ continue;
450
+ end if;
451
+
452
+ -- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
453
+ -- lines it taxes, split in proportion to their base. The last share
454
+ -- takes whatever is left, so the shares add up to the amount that was
455
+ -- rounded once on the group and the entry still balances to the cent.
456
+ v_left := v_amount;
457
+ v_left_box := v_box_amount;
458
+
459
+ for g in
460
+ select account_id,
461
+ base_amount,
462
+ seq,
463
+ sum(base_amount) over () as total_base,
464
+ row_number() over (order by seq) = count(*) over () as is_last
465
+ from (
466
+ select l.account_id,
467
+ sum(l.amount_untaxed) as base_amount,
468
+ min(l.sequence) as seq
469
+ from document_lines l
470
+ where l.document_id = p_document_id
471
+ and l.line_type = 'product'
472
+ and l.tax_id = r.tax_id
473
+ group by l.account_id
474
+ having sum(l.amount_untaxed) <> 0
475
+ ) as groups
476
+ order by seq
477
+ loop
478
+ if g.is_last then
479
+ v_share := v_left;
480
+ v_share_box := v_left_box;
481
+ else
482
+ v_share := round(v_amount * g.base_amount / g.total_base, 2);
483
+ v_share_box := case when v_box_amount is null then null
484
+ else round(v_box_amount * g.base_amount / g.total_base, 2) end;
485
+ v_left := v_left - v_share;
486
+ v_left_box := v_left_box - v_share_box;
487
+ end if;
488
+
489
+ if v_share = 0 then
490
+ continue;
491
+ end if;
492
+
493
+ v_seq := v_seq + 10;
494
+ v_share_book := round(v_share / v_rate, 2);
495
+
496
+ -- `tax_line` stays false: the amount is on a base account and belongs
497
+ -- to the base side of the declaration, which is why the Belgian grids
498
+ -- 82 and 83 report it together with the base.
499
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
500
+ debit, credit, tax_id, tax_line,
501
+ declaration_box, box_amount, currency_code, amount_currency)
502
+ values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
503
+ case when v_side_credit then 0 else v_share_book end,
504
+ case when v_side_credit then v_share_book else 0 end,
505
+ r.tax_id, false,
506
+ p.declaration_box, v_share_box, v_doc.currency_code,
507
+ case when v_foreign then v_share end);
508
+ end loop;
509
+ end loop;
510
+ end loop;
511
+
512
+ -- ------------------------------------------------------------ counterpart
513
+ select total_debit - total_credit into v_diff from entries where id = v_entry.id;
514
+ select coalesce(sum(case when l.debit > 0 then l.amount_currency else -l.amount_currency end), 0)
515
+ into v_diff_cur
516
+ from entry_lines l where l.entry_id = v_entry.id;
517
+
518
+ if v_diff = 0 then
519
+ raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
520
+ end if;
521
+
522
+ v_contact := commercial_entity(v_doc.contact_id);
523
+ v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
524
+
525
+ select payment_terms_days into v_terms from contacts where id = v_contact;
526
+ v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
527
+
528
+ v_amount := abs(v_diff);
529
+ -- The counterpart balances the entry in both currencies. The total it is
530
+ -- checked against is the document's own, which is the currency
531
+ -- `amount_total` is stated in.
532
+ v_total_cur := case when v_foreign then abs(v_diff_cur) else v_amount end;
533
+ v_seq := v_seq + 10;
534
+
535
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
536
+ debit, credit, contact_id, date_maturity, currency_code,
537
+ amount_currency)
538
+ values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
539
+ case when v_diff > 0 then 0 else v_amount end,
540
+ case when v_diff > 0 then v_amount else 0 end,
541
+ v_contact, v_maturity, v_doc.currency_code,
542
+ case when v_foreign then v_total_cur end);
543
+
544
+ -- The ledger is right by construction. If the header disagrees, the header
545
+ -- is what is wrong, and we say so instead of quietly patching a line.
546
+ if abs(v_total_cur - abs(v_doc.amount_total)) > 0.005 then
547
+ raise exception 'document_total_mismatch: document % totals % but its lines book %',
548
+ p_document_id, v_doc.amount_total, v_total_cur;
549
+ end if;
550
+
551
+ -- --------------------------------------------------------------- posting
552
+ v_entry := post_entry(v_entry.id);
553
+
554
+ update documents
555
+ set state = 'posted',
556
+ number = coalesce(number, v_entry.number),
557
+ entry_id = v_entry.id,
558
+ accounting_date = v_date
559
+ where id = p_document_id;
560
+
561
+ return v_entry;
562
+ end;
563
+ $$;
564
+
565
+ comment on function post_document(uuid) is
566
+ 'Books a document: base lines, tax lines from tax_postings — the non-deductible share on the accounts of the lines, a cash-basis tax on its transition account and on no box — a counterpart that balances by construction, and the company currency in the ledger at the rate the document carries.';
567
+
568
+ -- ---------------------------------------------------------------------------
569
+ -- post_payment — the same two lines, in the company's currency
570
+ -- ---------------------------------------------------------------------------
571
+
572
+ create or replace function post_payment(p_payment_id uuid)
573
+ returns entries
574
+ language plpgsql
575
+ as $$
576
+ declare
577
+ v_pay payments%rowtype;
578
+ v_entry entries%rowtype;
579
+ v_money uuid;
580
+ v_third uuid;
581
+ v_contact uuid;
582
+ v_inbound boolean;
583
+ v_label text;
584
+ v_home char(3);
585
+ v_foreign boolean;
586
+ v_book numeric(16, 2);
587
+ begin
588
+ select * into v_pay from payments where id = p_payment_id for update;
589
+ if not found then
590
+ raise exception 'unknown_payment: payment % does not exist', p_payment_id;
591
+ end if;
592
+ if v_pay.entry_id is not null then
593
+ raise exception 'payment_already_booked: payment % already points at entry %',
594
+ p_payment_id, v_pay.entry_id;
595
+ end if;
596
+ if v_pay.state = 'cancelled' then
597
+ raise exception 'payment_cancelled: payment % cannot be booked', p_payment_id;
598
+ end if;
599
+
600
+ v_inbound := v_pay.direction = 'inbound';
601
+
602
+ -- The bank side: the account behind the payment's bank account, or the
603
+ -- default account of the journal it goes through.
604
+ select b.account_id into v_money
605
+ from bank_accounts b
606
+ where b.id = v_pay.bank_account_id;
607
+
608
+ if v_money is null then
609
+ select coalesce(j.default_account_id, b.account_id) into v_money
610
+ from journals j
611
+ left join bank_accounts b on b.id = j.bank_account_id
612
+ where j.id = v_pay.journal_id;
613
+ end if;
614
+
615
+ -- A bank account wired to the journal from its own side counts too: that is
616
+ -- the direction an operator fills in first.
617
+ if v_money is null then
618
+ select b.account_id into v_money
619
+ from bank_accounts b
620
+ where b.journal_id = v_pay.journal_id
621
+ and b.active
622
+ and b.account_id is not null
623
+ order by b.created_at
624
+ limit 1;
625
+ end if;
626
+
627
+ if v_money is null then
628
+ raise exception 'no_bank_account: set bank_account_id on the payment, or default_account_id on journal %',
629
+ v_pay.journal_id;
630
+ end if;
631
+
632
+ -- The third-party side, by role.
633
+ v_contact := commercial_entity(v_pay.contact_id);
634
+ v_third := resolve_counterpart_account(v_pay.company_id, v_contact, v_inbound);
635
+
636
+ v_label := coalesce(v_pay.reference, v_pay.memo, 'payment');
637
+
638
+ select c.currency_code into v_home from companies c where c.id = v_pay.company_id;
639
+ v_foreign := v_pay.currency_code <> v_home;
640
+ v_book := round(v_pay.amount / v_pay.exchange_rate, 2);
641
+
642
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
643
+ description, state, currency_code)
644
+ values (v_pay.company_id, v_pay.journal_id,
645
+ fiscal_year_at(v_pay.company_id, v_pay.payment_date), v_pay.payment_date,
646
+ v_pay.reference, v_label, 'draft', v_pay.currency_code)
647
+ returning * into v_entry;
648
+
649
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
650
+ debit, credit, currency_code, amount_currency)
651
+ values (v_entry.id, v_pay.company_id, v_money, 10, v_label,
652
+ case when v_inbound then v_book else 0 end,
653
+ case when v_inbound then 0 else v_book end,
654
+ v_pay.currency_code,
655
+ case when v_foreign then v_pay.amount end);
656
+
657
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
658
+ debit, credit, contact_id, currency_code, amount_currency)
659
+ values (v_entry.id, v_pay.company_id, v_third, 20, v_label,
660
+ case when v_inbound then 0 else v_book end,
661
+ case when v_inbound then v_book else 0 end,
662
+ v_contact, v_pay.currency_code,
663
+ case when v_foreign then v_pay.amount end);
664
+
665
+ v_entry := post_entry(v_entry.id);
666
+
667
+ update payments
668
+ set entry_id = v_entry.id,
669
+ state = 'posted'
670
+ where id = p_payment_id;
671
+
672
+ return v_entry;
673
+ end;
674
+ $$;
675
+
676
+ comment on function post_payment(uuid) is
677
+ 'Books a payment: the bank side from the payment''s bank account or its journal, the third-party side by role, both in the company currency at the payment''s rate. Matches nothing.';
678
+
679
+ -- ---------------------------------------------------------------------------
680
+ -- settle_cash_basis_tax — the share of a waiting tax that has become due
681
+ --
682
+ -- Called by `reconcile` and by `unreconcile`, and derived entirely from the
683
+ -- ledger: what is waiting on the document, what share of the document has been
684
+ -- settled, and what earlier matchings already sent on. The difference is the
685
+ -- entry this posts, so the function is idempotent and works the same in both
686
+ -- directions — undoing a matching lowers the share and books the mirror.
687
+ --
688
+ -- The share is cumulative and rounded once against the whole, never payment by
689
+ -- payment: three thirds of 210,00 come to 70,00 + 70,00 + 70,00, and a tax of
690
+ -- 0,63 settled in halves comes to 0,32 then 0,31. The last matching always
691
+ -- carries the remainder because at full settlement the share is the whole.
692
+ --
693
+ -- The base of the operation travels with its tax. A cash-basis return declares
694
+ -- the base collected, so the base line of the document waits too — with no
695
+ -- ledger movement, because revenue was earned when it was invoiced. That is
696
+ -- what a line with a box and no amount is: a figure on a declaration, which
697
+ -- this schema has kept apart from a figure in the ledger since the tax engine.
698
+ -- ---------------------------------------------------------------------------
699
+
700
+ create or replace function settle_cash_basis_tax(
701
+ p_document_id uuid,
702
+ p_date date
703
+ )
704
+ returns uuid
705
+ language plpgsql
706
+ as $$
707
+ declare
708
+ v_doc documents%rowtype;
709
+ v_company companies%rowtype;
710
+ v_entry entries%rowtype;
711
+ v_kind tax_document_kind;
712
+ v_total numeric(16, 2);
713
+ v_paid numeric(16, 2);
714
+ v_ratio numeric;
715
+ v_seq integer := 0;
716
+ v_final uuid;
717
+ v_box text;
718
+ v_target uuid;
719
+ v_due numeric(16, 2);
720
+ v_done numeric(16, 2);
721
+ v_delta numeric(16, 2);
722
+ v_box_due numeric(16, 2);
723
+ v_box_done numeric(16, 2);
724
+ v_box_delta numeric(16, 2);
725
+ v_credit boolean;
726
+ w record;
727
+ begin
728
+ select * into v_doc from documents where id = p_document_id;
729
+ if not found or v_doc.entry_id is null then
730
+ return null;
731
+ end if;
732
+
733
+ -- Nothing is waiting on this document: the answer for every tax that falls
734
+ -- due when it is invoiced, which is most of them.
735
+ if not exists (
736
+ select 1
737
+ from entry_lines q join taxes t on t.id = q.tax_id
738
+ where q.entry_id = v_doc.entry_id
739
+ and t.cash_basis
740
+ and q.declaration_box is null
741
+ and q.box_amount is not null
742
+ ) then
743
+ return null;
744
+ end if;
745
+
746
+ select * into v_company from companies where id = v_doc.company_id;
747
+ if v_company.miscellaneous_journal_id is null then
748
+ raise exception 'no_miscellaneous_journal: company % has no journal for the transfer of a cash-basis tax',
749
+ v_doc.company_id;
750
+ end if;
751
+
752
+ v_kind := case when v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note')
753
+ then 'credit_note' else 'invoice' end::tax_document_kind;
754
+
755
+ -- What share of this document has been settled, read on its own third-party
756
+ -- lines and in the ledger's currency on both sides of the division.
757
+ select coalesce(sum(abs(tl.debit - tl.credit)), 0), coalesce(sum(tl.matched_amount), 0)
758
+ into v_total, v_paid
759
+ from entry_lines tl join accounts a on a.id = tl.account_id
760
+ where tl.entry_id = v_doc.entry_id
761
+ and a.reconcilable
762
+ and a.account_type in ('asset_receivable', 'liability_payable');
763
+
764
+ if v_total = 0 then
765
+ return null;
766
+ end if;
767
+ -- An overpayment settles the document, and no more: a tax is due on what
768
+ -- was invoiced.
769
+ v_ratio := least(1, greatest(0, v_paid / v_total));
770
+
771
+ for w in
772
+ select l2.id, l2.account_id, l2.tax_id, l2.tax_line, l2.box_amount,
773
+ l2.debit, l2.credit, l2.name, l2.sequence
774
+ from entry_lines l2 join taxes t on t.id = l2.tax_id
775
+ where l2.entry_id = v_doc.entry_id
776
+ and t.cash_basis
777
+ and l2.declaration_box is null
778
+ and l2.box_amount is not null
779
+ order by l2.sequence
780
+ loop
781
+ select tp.account_id, tp.declaration_box
782
+ into v_final, v_box
783
+ from tax_postings tp
784
+ where tp.tax_id = w.tax_id
785
+ and tp.document_kind = v_kind
786
+ and tp.posting_type = (case when w.tax_line then 'tax' else 'base' end)::tax_posting_type
787
+ limit 1;
788
+
789
+ -- The account the transfer of this line lands on, which is also how an
790
+ -- earlier transfer of the same line is recognised.
791
+ v_target := case when w.tax_line then v_final else w.account_id end;
792
+ if v_target is null then
793
+ raise exception 'no_cash_basis_target: the tax of line % names no account to fall due on', w.id;
794
+ end if;
795
+ -- A line only waits because a box was worked out for it, and that box came
796
+ -- from this very posting. If it has none, the amount would wait for ever.
797
+ if v_box is null then
798
+ raise exception 'no_cash_basis_box: the tax of line % holds an amount for a box the posting does not name', w.id;
799
+ end if;
800
+
801
+ if w.tax_line then
802
+ v_due := round((w.debit + w.credit) * v_ratio, 2);
803
+ select coalesce(sum(case when w.credit > 0 then x.credit - x.debit
804
+ else x.debit - x.credit end), 0)
805
+ into v_done
806
+ from entry_lines x join entries e on e.id = x.entry_id
807
+ where e.document_id = p_document_id
808
+ and e.id <> v_doc.entry_id
809
+ and x.tax_id = w.tax_id
810
+ and x.tax_line
811
+ and x.account_id = v_target;
812
+ else
813
+ v_due := 0;
814
+ v_done := 0;
815
+ end if;
816
+ v_delta := v_due - v_done;
817
+
818
+ v_box_due := round(w.box_amount * v_ratio, 2);
819
+ select coalesce(sum(x.box_amount), 0)
820
+ into v_box_done
821
+ from entry_lines x join entries e on e.id = x.entry_id
822
+ where e.document_id = p_document_id
823
+ and e.id <> v_doc.entry_id
824
+ and x.tax_id = w.tax_id
825
+ and x.tax_line = w.tax_line
826
+ and x.account_id = v_target;
827
+ v_box_delta := v_box_due - v_box_done;
828
+
829
+ if v_delta = 0 and v_box_delta = 0 then
830
+ continue;
831
+ end if;
832
+
833
+ if v_entry.id is null then
834
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
835
+ description, state, document_id, currency_code)
836
+ values (v_doc.company_id, v_company.miscellaneous_journal_id,
837
+ fiscal_year_at(v_doc.company_id, p_date), p_date,
838
+ v_doc.number,
839
+ coalesce(v_doc.number, 'document') || ' — tax due on settlement',
840
+ 'draft', p_document_id, v_company.currency_code)
841
+ returning * into v_entry;
842
+ end if;
843
+
844
+ -- The side the document put the tax on, kept when the share grows and
845
+ -- flipped when it shrinks.
846
+ v_credit := (w.credit > 0) = (v_delta > 0);
847
+
848
+ v_seq := v_seq + 10;
849
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
850
+ debit, credit, tax_id, tax_line,
851
+ declaration_box, box_amount, currency_code)
852
+ values (v_entry.id, v_doc.company_id, v_target, v_seq, w.name,
853
+ case when v_credit then 0 else abs(v_delta) end,
854
+ case when v_credit then abs(v_delta) else 0 end,
855
+ w.tax_id, w.tax_line,
856
+ v_box, v_box_delta,
857
+ v_company.currency_code);
858
+
859
+ if v_delta <> 0 then
860
+ v_seq := v_seq + 10;
861
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
862
+ debit, credit, tax_id, tax_line, currency_code)
863
+ values (v_entry.id, v_doc.company_id, w.account_id, v_seq, w.name,
864
+ case when v_credit then abs(v_delta) else 0 end,
865
+ case when v_credit then 0 else abs(v_delta) end,
866
+ w.tax_id, true, v_company.currency_code);
867
+ end if;
868
+ end loop;
869
+
870
+ if v_entry.id is null then
871
+ return null;
872
+ end if;
873
+
874
+ v_entry := post_entry(v_entry.id);
875
+ return v_entry.id;
876
+ end;
877
+ $$;
878
+
879
+ comment on function settle_cash_basis_tax(uuid, date) is
880
+ 'Moves the share of a cash-basis tax that settlement has made due, from the transition account to the account and the box it is declared on. Derived from the ledger, so it is the same call whether a matching was made or undone.';
881
+
882
+ -- ---------------------------------------------------------------------------
883
+ -- reconcile — the same matching, plus what the matching reveals
884
+ --
885
+ -- Two lines in the same currency, and that currency is not the company's: the
886
+ -- matching is worked out in *their* currency, because that is where they are
887
+ -- equal, and each side turns it back into the company's at the rate it was
888
+ -- booked with. The two figures differ by the movement of the rate, and that
889
+ -- difference is realised the moment the money arrives. It is booked on the
890
+ -- accounts the pack names, on the third-party account so that account goes to
891
+ -- nil, and matched under the same letter.
892
+ --
893
+ -- `p_amount` is read in the currency the two lines share, which is the
894
+ -- currency an operator is looking at when a dollar invoice is settled by a
895
+ -- dollar payment. Nothing else changes: two lines in the company's currency
896
+ -- are matched exactly as before, and every line booked before this migration
897
+ -- carries no currency amount at all.
898
+ --
899
+ -- The unmatched residual that a rate creates has no other way out. Leaving it
900
+ -- on the receivable would mean a customer who has paid in full still owes a
901
+ -- few cents of a currency they never used.
902
+ -- ---------------------------------------------------------------------------
903
+
904
+ create or replace function reconcile(
905
+ p_line_a uuid,
906
+ p_line_b uuid,
907
+ p_amount numeric default null
908
+ )
909
+ returns reconciliations
910
+ language plpgsql
911
+ as $$
912
+ declare
913
+ v_a entry_lines%rowtype;
914
+ v_b entry_lines%rowtype;
915
+ v_debit entry_lines%rowtype;
916
+ v_credit entry_lines%rowtype;
917
+ v_amount numeric(16, 2);
918
+ v_letter text;
919
+ v_result reconciliations%rowtype;
920
+ v_reconcilable boolean;
921
+ v_home char(3);
922
+ v_country char(2);
923
+ v_journal uuid;
924
+ v_fx boolean := false;
925
+ v_open_d numeric(16, 2);
926
+ v_open_c numeric(16, 2);
927
+ v_cur_d numeric(16, 2);
928
+ v_cur_c numeric(16, 2);
929
+ v_cur numeric(16, 2);
930
+ v_comp_d numeric(16, 2);
931
+ v_comp_c numeric(16, 2);
932
+ v_gap numeric(16, 2) := 0;
933
+ v_gain uuid;
934
+ v_loss uuid;
935
+ v_result_account uuid;
936
+ v_fx_entry entries%rowtype;
937
+ v_fx_line uuid;
938
+ v_date date;
939
+ v_transfer uuid;
940
+ v_doc uuid;
941
+ begin
942
+ select * into v_a from entry_lines where id = p_line_a for update;
943
+ if not found then raise exception 'unknown_entry_line: %', p_line_a; end if;
944
+ select * into v_b from entry_lines where id = p_line_b for update;
945
+ if not found then raise exception 'unknown_entry_line: %', p_line_b; end if;
946
+
947
+ if v_a.account_id <> v_b.account_id then
948
+ raise exception 'reconcile_account_mismatch: lines are on different accounts';
949
+ end if;
950
+ if v_a.company_id <> v_b.company_id then
951
+ raise exception 'reconcile_company_mismatch: lines belong to different companies';
952
+ end if;
953
+
954
+ select a.reconcilable into v_reconcilable from accounts a where a.id = v_a.account_id;
955
+ if not v_reconcilable then
956
+ raise exception 'account_not_reconcilable: account of line % is not reconcilable', p_line_a;
957
+ end if;
958
+
959
+ if (v_a.debit > 0) = (v_b.debit > 0) then
960
+ raise exception 'reconcile_same_side: a debit must be matched against a credit';
961
+ end if;
962
+
963
+ if v_a.debit > 0 then
964
+ v_debit := v_a; v_credit := v_b;
965
+ else
966
+ v_debit := v_b; v_credit := v_a;
967
+ end if;
968
+
969
+ select c.currency_code, c.country, c.miscellaneous_journal_id
970
+ into v_home, v_country, v_journal
971
+ from companies c where c.id = v_debit.company_id;
972
+
973
+ v_open_d := abs(v_debit.debit - v_debit.credit) - v_debit.matched_amount;
974
+ v_open_c := abs(v_credit.debit - v_credit.credit) - v_credit.matched_amount;
975
+
976
+ v_fx := v_debit.currency_code is not null
977
+ and v_debit.currency_code = v_credit.currency_code
978
+ and v_debit.currency_code <> v_home
979
+ and coalesce(v_debit.amount_currency, 0) <> 0
980
+ and coalesce(v_credit.amount_currency, 0) <> 0;
981
+
982
+ if v_fx then
983
+ -- What is still open on each line, in its own currency, in proportion to
984
+ -- what is still open in the ledger.
985
+ v_cur_d := round(v_debit.amount_currency * v_open_d
986
+ / nullif(abs(v_debit.debit - v_debit.credit), 0), 2);
987
+ v_cur_c := round(v_credit.amount_currency * v_open_c
988
+ / nullif(abs(v_credit.debit - v_credit.credit), 0), 2);
989
+ v_cur := coalesce(p_amount, least(v_cur_d, v_cur_c));
990
+
991
+ if v_cur is null or v_cur <= 0 then
992
+ raise exception 'reconcile_nothing_left: no open amount to match';
993
+ end if;
994
+ if v_cur > v_cur_d + 0.001 or v_cur > v_cur_c + 0.001 then
995
+ raise exception 'reconcile_over_currency: % exceeds what is open in %',
996
+ v_cur, v_debit.currency_code;
997
+ end if;
998
+
999
+ v_comp_d := round(v_cur * abs(v_debit.debit - v_debit.credit) / v_debit.amount_currency, 2);
1000
+ v_comp_c := round(v_cur * abs(v_credit.debit - v_credit.credit) / v_credit.amount_currency, 2);
1001
+ v_amount := least(v_comp_d, v_comp_c);
1002
+ v_gap := v_comp_d - v_comp_c;
1003
+ else
1004
+ v_amount := coalesce(p_amount, least(v_open_d, v_open_c));
1005
+ end if;
1006
+
1007
+ if v_amount is null or v_amount <= 0 then
1008
+ raise exception 'reconcile_nothing_left: no open amount to match';
1009
+ end if;
1010
+ if v_amount > v_open_d + 0.001 then
1011
+ raise exception 'reconcile_over_debit: % exceeds the open amount of the debit line', v_amount;
1012
+ end if;
1013
+ if v_amount > v_open_c + 0.001 then
1014
+ raise exception 'reconcile_over_credit: % exceeds the open amount of the credit line', v_amount;
1015
+ end if;
1016
+
1017
+ -- Reuse a letter already carried by either side, otherwise draw a new one.
1018
+ v_letter := coalesce(v_debit.matching_number, v_credit.matching_number,
1019
+ next_matching_number(v_debit.company_id));
1020
+
1021
+ -- Two chains meeting: merge them under a single letter.
1022
+ if v_debit.matching_number is not null
1023
+ and v_credit.matching_number is not null
1024
+ and v_debit.matching_number <> v_credit.matching_number then
1025
+ update reconciliations
1026
+ set matching_number = v_letter
1027
+ where company_id = v_debit.company_id
1028
+ and matching_number = v_credit.matching_number;
1029
+ end if;
1030
+
1031
+ -- The date the settlement is complete on: the later of the two entries.
1032
+ select greatest(ed.entry_date, ec.entry_date) into v_date
1033
+ from entries ed, entries ec
1034
+ where ed.id = v_debit.entry_id and ec.id = v_credit.entry_id;
1035
+
1036
+ -- ------------------------------------------------------- exchange difference
1037
+ if v_gap <> 0 then
1038
+ select account_id_by_code(v_debit.company_id, cd.fx_gain_code),
1039
+ account_id_by_code(v_debit.company_id, cd.fx_loss_code)
1040
+ into v_gain, v_loss
1041
+ from country_defaults cd where cd.country = v_country;
1042
+
1043
+ -- A debit side short of the credit side means more of the company's money
1044
+ -- came in than the receivable was booked at: a gain.
1045
+ v_result_account := case when v_gap < 0 then v_gain else v_loss end;
1046
+ if v_result_account is null then
1047
+ raise exception 'no_fx_accounts: matching % with % realises % and the country model names no exchange gain and loss account',
1048
+ p_line_a, p_line_b, v_gap;
1049
+ end if;
1050
+ if v_journal is null then
1051
+ raise exception 'no_miscellaneous_journal: company % has no journal for an exchange difference',
1052
+ v_debit.company_id;
1053
+ end if;
1054
+
1055
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date,
1056
+ description, state, currency_code)
1057
+ values (v_debit.company_id, v_journal, fiscal_year_at(v_debit.company_id, v_date), v_date,
1058
+ 'Exchange difference on ' || v_letter, 'draft', v_home)
1059
+ returning * into v_fx_entry;
1060
+
1061
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
1062
+ debit, credit, contact_id, currency_code)
1063
+ values (v_fx_entry.id, v_debit.company_id, v_debit.account_id, 10,
1064
+ 'Exchange difference',
1065
+ case when v_gap < 0 then abs(v_gap) else 0 end,
1066
+ case when v_gap < 0 then 0 else abs(v_gap) end,
1067
+ coalesce(v_debit.contact_id, v_credit.contact_id), v_home)
1068
+ returning id into v_fx_line;
1069
+
1070
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
1071
+ debit, credit, currency_code)
1072
+ values (v_fx_entry.id, v_debit.company_id, v_result_account, 20,
1073
+ 'Exchange difference',
1074
+ case when v_gap < 0 then 0 else abs(v_gap) end,
1075
+ case when v_gap < 0 then abs(v_gap) else 0 end,
1076
+ v_home);
1077
+
1078
+ v_fx_entry := post_entry(v_fx_entry.id);
1079
+
1080
+ -- And it is matched with the side it completes, under the same letter.
1081
+ if v_gap < 0 then
1082
+ insert into reconciliations (company_id, debit_line_id, credit_line_id, amount, matching_number)
1083
+ values (v_debit.company_id, v_fx_line, v_credit.id, abs(v_gap), v_letter);
1084
+ else
1085
+ insert into reconciliations (company_id, debit_line_id, credit_line_id, amount, matching_number)
1086
+ values (v_debit.company_id, v_debit.id, v_fx_line, abs(v_gap), v_letter);
1087
+ end if;
1088
+ end if;
1089
+
1090
+ insert into reconciliations (company_id, debit_line_id, credit_line_id, amount,
1091
+ matching_number, fx_entry_id)
1092
+ values (v_debit.company_id, v_debit.id, v_credit.id, v_amount, v_letter, v_fx_entry.id)
1093
+ returning * into v_result;
1094
+
1095
+ -- ------------------------------------------------------- a tax falling due
1096
+ for v_doc in
1097
+ select distinct d.id
1098
+ from entry_lines l join documents d on d.entry_id = l.entry_id
1099
+ where l.id in (v_debit.id, v_credit.id)
1100
+ loop
1101
+ v_transfer := coalesce(settle_cash_basis_tax(v_doc, v_date), v_transfer);
1102
+ end loop;
1103
+
1104
+ if v_transfer is not null then
1105
+ update reconciliations set tax_transfer_entry_id = v_transfer where id = v_result.id
1106
+ returning * into v_result;
1107
+ end if;
1108
+
1109
+ return v_result;
1110
+ end;
1111
+ $$;
1112
+
1113
+ comment on function reconcile(uuid, uuid, numeric) is
1114
+ 'Matches a debit line against a credit line, in the currency the two share when it is not the company''s, and books what the matching reveals: the realised exchange difference, and the share of a cash-basis tax that has become due.';
1115
+
1116
+ -- ---------------------------------------------------------------------------
1117
+ -- unreconcile — and what has to be taken back with the matching
1118
+ -- ---------------------------------------------------------------------------
1119
+
1120
+ create or replace function unreconcile(p_reconciliation_id uuid)
1121
+ returns void
1122
+ language plpgsql
1123
+ as $$
1124
+ declare
1125
+ v_row reconciliations%rowtype;
1126
+ v_date date;
1127
+ v_fx entries%rowtype;
1128
+ v_mirror entries%rowtype;
1129
+ v_doc uuid;
1130
+ fl record;
1131
+ begin
1132
+ select * into v_row from reconciliations where id = p_reconciliation_id;
1133
+ if not found then
1134
+ raise exception 'unknown_reconciliation: %', p_reconciliation_id;
1135
+ end if;
1136
+
1137
+ select greatest(ed.entry_date, ec.entry_date) into v_date
1138
+ from entry_lines ld join entries ed on ed.id = ld.entry_id,
1139
+ entry_lines lc join entries ec on ec.id = lc.entry_id
1140
+ where ld.id = v_row.debit_line_id and lc.id = v_row.credit_line_id;
1141
+
1142
+ delete from reconciliations where id = p_reconciliation_id;
1143
+
1144
+ -- An exchange difference was realised by this matching and by nothing else,
1145
+ -- so undoing the matching takes it back: its own matching goes, and a mirror
1146
+ -- entry cancels it on the same date. The period has to be open, which is the
1147
+ -- honest refusal — a difference that has been declared is not unmade quietly.
1148
+ if v_row.fx_entry_id is not null then
1149
+ select * into v_fx from entries where id = v_row.fx_entry_id;
1150
+
1151
+ delete from reconciliations r
1152
+ where r.company_id = v_row.company_id
1153
+ and (r.debit_line_id in (select id from entry_lines where entry_id = v_fx.id)
1154
+ or r.credit_line_id in (select id from entry_lines where entry_id = v_fx.id));
1155
+
1156
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date,
1157
+ description, state, currency_code, reversed_entry_id)
1158
+ values (v_fx.company_id, v_fx.journal_id, v_fx.fiscal_year_id, v_fx.entry_date,
1159
+ 'Exchange difference undone', 'draft', v_fx.currency_code, v_fx.id)
1160
+ returning * into v_mirror;
1161
+
1162
+ for fl in select * from entry_lines where entry_id = v_fx.id order by sequence loop
1163
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
1164
+ debit, credit, contact_id, currency_code)
1165
+ values (v_mirror.id, fl.company_id, fl.account_id, fl.sequence, fl.name,
1166
+ fl.credit, fl.debit, fl.contact_id, fl.currency_code);
1167
+ end loop;
1168
+
1169
+ perform post_entry(v_mirror.id);
1170
+ end if;
1171
+
1172
+ -- The share of a cash-basis tax that is due has just fallen; the same
1173
+ -- function books the difference, which is now the other way round.
1174
+ for v_doc in
1175
+ select distinct d.id
1176
+ from entry_lines l join documents d on d.entry_id = l.entry_id
1177
+ where l.id in (v_row.debit_line_id, v_row.credit_line_id)
1178
+ loop
1179
+ perform settle_cash_basis_tax(v_doc, v_date);
1180
+ end loop;
1181
+ end;
1182
+ $$;
1183
+
1184
+ comment on function unreconcile(uuid) is
1185
+ 'Undoes a matching, and with it what the matching had booked: the exchange difference it realised and the share of a cash-basis tax it had made due.';
1186
+
1187
+ -- Rule 6 of supabase/migrations/README.md: a function created here comes out
1188
+ -- executable by PUBLIC otherwise.
1189
+ revoke execute on all functions in schema public from public;