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,1318 @@
1
+ -- Ekwo OS — a tax follows the territory of the parties.
2
+ --
3
+ -- Two packs stopped at this wall from opposite sides, and `docs/international.md`
4
+ -- records both. `packs/gb/` cannot carry the Northern Ireland taxes, because a
5
+ -- company in Manchester would be offered them; `packs/us/` offers a Californian
6
+ -- company the New York and Oregon codes, because `jurisdiction` is a label on
7
+ -- the tax and nothing reads it. Both notes proposed the same fix — let a tax
8
+ -- name a territory, and let a party record the one it is in — and the American
9
+ -- one added the half the British one did not need: a sale is taxed where the
10
+ -- goods are delivered, so the condition is on the **buyer** and on the place of
11
+ -- supply, not only on the seller.
12
+ --
13
+ -- The whole argument, with the three shapes weighed against this one, is in
14
+ -- `docs/decisions.md`, "A tax follows the territory of the parties". What
15
+ -- follows is the schema.
16
+ --
17
+ -- ---------------------------------------------------------------------------
18
+ -- A territory is a column, not a composition
19
+ -- ---------------------------------------------------------------------------
20
+ --
21
+ -- A company and a contact each carry a country, and a country is not a
22
+ -- territory: the tax that matters here is levied by California and not by the
23
+ -- United States, and Northern Ireland is `XI` — a code of the Union's own
24
+ -- systems, which ISO 3166-2 does not carry and which no concatenation of a
25
+ -- country and a region reaches. `fiscal_country || '-' || region` gets `US-CA`
26
+ -- and never gets `XI`, and a rule with one exception written into it collects
27
+ -- the second exception in silence.
28
+ --
29
+ -- `region` stays what it is: ISO 3166-2 **without** the prefix, for the
30
+ -- Canadian pack that will want to ask "which province" as a question with a
31
+ -- short list of answers. That is a different question from "which body of tax
32
+ -- law is this party under", and this migration answers the second one.
33
+ --
34
+ -- Four columns, all of them foreign keys into `territories`, which is what
35
+ -- makes them reviewable: a code nobody can look up is a string, and a string
36
+ -- is what `jurisdiction` has been since the day it was added.
37
+ --
38
+ -- `companies.territory_code` where this company is established
39
+ -- `contacts.territory_code` where this party is
40
+ -- `documents.supply_territory_code` where the supply takes place
41
+ -- `taxes.applies_seller_territory` where the seller has to be
42
+ -- `taxes.applies_buyer_territory` where the buyer has to be
43
+ -- `taxes.applies_supply_territory` where the supply has to be
44
+ --
45
+ -- All six are nullable, and null is the ordinary case. A Belgian company will
46
+ -- never set one; asking every installation in Europe to answer a question the
47
+ -- common system does not pose would be the country-default mistake with its
48
+ -- sign flipped.
49
+
50
+ alter table companies add column if not exists territory_code text references territories(code);
51
+ alter table contacts add column if not exists territory_code text references territories(code);
52
+ alter table documents add column if not exists supply_territory_code text references territories(code);
53
+
54
+ -- A foreign key is read the way `currency_code` is: by the row that holds it,
55
+ -- and by the check that a territory is still referenced before it is taken
56
+ -- away. One index per key, leading with the key's own column.
57
+ create index if not exists companies_territory_idx on companies (territory_code);
58
+ create index if not exists contacts_territory_idx on contacts (territory_code);
59
+ create index if not exists documents_supply_territory_idx on documents (supply_territory_code);
60
+
61
+ comment on column companies.territory_code is
62
+ 'Territory of `territories` this company is established in for tax, where the country is not precise enough: US-CA for a Californian filer, XI for a Northern Irish one. Null everywhere the country is the answer, which is every country of the common system of VAT — the resolution then reads fiscal_country. Distinct from `region`, which is a province code without a prefix and answers a different question.';
63
+ comment on column contacts.territory_code is
64
+ 'Territory of `territories` this party is in, where its country is not precise enough. Null resolves to `country`.';
65
+ comment on column documents.supply_territory_code is
66
+ 'Territory of `territories` the supply takes place in, where `delivery_country` is not precise enough — a delivery to US-CA and one to US-NY are both US. Null resolves to delivery_country, and then to the buyer''s territory: a supply nobody said anything else about is delivered to the person who bought it.';
67
+
68
+ alter table tax_templates
69
+ add column if not exists applies_seller_territory text references territories(code),
70
+ add column if not exists applies_buyer_territory text references territories(code),
71
+ add column if not exists applies_supply_territory text references territories(code);
72
+
73
+ alter table taxes
74
+ add column if not exists applies_seller_territory text references territories(code),
75
+ add column if not exists applies_buyer_territory text references territories(code),
76
+ add column if not exists applies_supply_territory text references territories(code);
77
+
78
+ create index if not exists tax_templates_applies_seller_territory_idx on tax_templates (applies_seller_territory);
79
+ create index if not exists tax_templates_applies_buyer_territory_idx on tax_templates (applies_buyer_territory);
80
+ create index if not exists tax_templates_applies_supply_territory_idx on tax_templates (applies_supply_territory);
81
+ create index if not exists taxes_applies_seller_territory_idx on taxes (applies_seller_territory);
82
+ create index if not exists taxes_applies_buyer_territory_idx on taxes (applies_buyer_territory);
83
+ create index if not exists taxes_applies_supply_territory_idx on taxes (applies_supply_territory);
84
+
85
+ comment on column tax_templates.applies_seller_territory is
86
+ 'Territory the seller has to be in for this tax to apply, from `applies_when.seller_in` of the pack. Null means the tax says nothing about the seller.';
87
+ comment on column tax_templates.applies_buyer_territory is
88
+ 'Territory the buyer has to be in, from `applies_when.buyer_in`. Null means the tax says nothing about the buyer.';
89
+ comment on column tax_templates.applies_supply_territory is
90
+ 'Territory the supply has to take place in, from `applies_when.supply_in`. A sale is taxed where the goods are delivered, which is why this is not the same column as the buyer''s.';
91
+ comment on column taxes.applies_seller_territory is
92
+ 'Territory the seller has to be in for this tax to apply. post_document() refuses a document that contradicts it; it never chooses a tax for anybody.';
93
+ comment on column taxes.applies_buyer_territory is
94
+ 'Territory the buyer has to be in for this tax to apply.';
95
+ comment on column taxes.applies_supply_territory is
96
+ 'Territory the supply has to take place in for this tax to apply.';
97
+
98
+ -- ---------------------------------------------------------------------------
99
+ -- A condition is satisfied by the territory named and by everything inside it
100
+ --
101
+ -- `territories.parent_code` already draws the tree: `XI` hangs off `GB`,
102
+ -- `ES-CE` off `ES`, `US-CA` off `US`. So `seller_in: 'GB'` covers a seller in
103
+ -- Northern Ireland and `seller_in: 'XI'` does not cover a seller in Great
104
+ -- Britain, which is exactly what lets one country's pack carry two sets of
105
+ -- taxes and offer each where it applies.
106
+ --
107
+ -- The walk is bounded by the tree and the tree is two deep everywhere today;
108
+ -- the recursion is written all the same, because a table nobody can add a
109
+ -- third level to is a table somebody will add a third level to.
110
+ --
111
+ -- A code the table does not carry answers false rather than raising: the
112
+ -- foreign keys above mean the only way to get one here is a party column
113
+ -- resolving to a country that has no row, and a supply to a place the
114
+ -- reference data has never heard of does not satisfy a condition naming a
115
+ -- place it has.
116
+ -- ---------------------------------------------------------------------------
117
+
118
+ create or replace function territory_within(p_code text, p_of text)
119
+ returns boolean
120
+ language sql
121
+ stable
122
+ as $$
123
+ with recursive up as (
124
+ select t.code, t.parent_code
125
+ from territories t
126
+ where t.code = upper(btrim(p_code))
127
+ union all
128
+ select t.code, t.parent_code
129
+ from territories t
130
+ join up on up.parent_code = t.code
131
+ )
132
+ select exists (select 1 from up where up.code = upper(btrim(p_of)));
133
+ $$;
134
+
135
+ comment on function territory_within(text, text) is
136
+ 'True when the first territory is the second one or lies inside it, following territories.parent_code: US-CA is within US, XI is within GB, and neither is within the other. False for a code this table does not carry.';
137
+
138
+ -- ---------------------------------------------------------------------------
139
+ -- Where the three parties of a document are
140
+ --
141
+ -- `p_party` is a closed vocabulary of three values and raises on a fourth,
142
+ -- which is how every other vocabulary of this schema behaves.
143
+ --
144
+ -- `seller` the company on a sale, the contact on a purchase. Decided by the
145
+ -- kind of document and never by the row: a purchase invoice is
146
+ -- somebody else's sale.
147
+ -- `buyer` the other one.
148
+ -- `supply` the place of supply: `supply_territory_code`, failing that
149
+ -- `delivery_country` — BG-15 of EN 16931, which `documents` has
150
+ -- carried since the day it existed and which is exactly the
151
+ -- statement that the place of delivery is not the billing address
152
+ -- — and failing that the buyer's territory.
153
+ --
154
+ -- The last rung is the one worth arguing about and the argument is that the
155
+ -- alternative is worse: a supply with no delivery address is not an unknown
156
+ -- place, it is the ordinary case of a seller and a buyer in one room. Refusing
157
+ -- it would refuse every invoice in Europe the day a pack there conditioned one
158
+ -- tax on a territory.
159
+ --
160
+ -- Null comes back where the ladder runs out — a contact with no country and no
161
+ -- territory, on a document with no delivery address. `post_document()` turns
162
+ -- that into a refusal that names the party, the tax and the column that would
163
+ -- answer, and only for a tax that actually asked.
164
+ -- ---------------------------------------------------------------------------
165
+
166
+ create or replace function document_territory(p_document_id uuid, p_party text)
167
+ returns text
168
+ language plpgsql
169
+ stable
170
+ as $$
171
+ declare
172
+ v_doc documents%rowtype;
173
+ v_is_sale boolean;
174
+ v_company text;
175
+ v_contact text;
176
+ v_buyer text;
177
+ begin
178
+ if p_party not in ('seller', 'buyer', 'supply') then
179
+ raise exception 'unknown_party: % is not seller, buyer or supply', p_party;
180
+ end if;
181
+
182
+ select * into v_doc from documents where id = p_document_id;
183
+ if not found then
184
+ raise exception 'unknown_document: document % does not exist', p_document_id;
185
+ end if;
186
+
187
+ v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note', 'sale_quote');
188
+
189
+ select coalesce(c.territory_code, c.fiscal_country) into v_company
190
+ from companies c where c.id = v_doc.company_id;
191
+ select coalesce(k.territory_code, k.country) into v_contact
192
+ from contacts k where k.id = v_doc.contact_id;
193
+
194
+ v_buyer := case when v_is_sale then v_contact else v_company end;
195
+
196
+ if p_party = 'seller' then
197
+ return case when v_is_sale then v_company else v_contact end;
198
+ elsif p_party = 'buyer' then
199
+ return v_buyer;
200
+ end if;
201
+
202
+ return coalesce(v_doc.supply_territory_code, v_doc.delivery_country, v_buyer);
203
+ end;
204
+ $$;
205
+
206
+ comment on function document_territory(uuid, text) is
207
+ 'The territory of one party to a document: seller, buyer or supply. The seller is the company on a sale and the contact on a purchase, each resolving to its own territory_code and failing that to its country. The supply is the document''s supply_territory_code, failing that its delivery_country (BG-15), failing that the buyer''s territory. Null where none of those was ever recorded.';
208
+
209
+ revoke execute on function territory_within(text, text) from public, anon;
210
+ revoke execute on function document_territory(uuid, text) from public, anon;
211
+ grant execute on function territory_within(text, text) to authenticated, service_role;
212
+ grant execute on function document_territory(uuid, text) to authenticated, service_role;
213
+
214
+ -- ---------------------------------------------------------------------------
215
+ -- The readers
216
+ --
217
+ -- Four functions are replaced, and three of them only to carry three columns
218
+ -- through a copy. `post_document()` is the one that gains a behaviour.
219
+ --
220
+ -- `install_country_template()` and `pack_upgrade()` copy a template into a
221
+ -- company, and a column missing on one side of a copy is the defect
222
+ -- `price_include` was for a whole release. `pack_upgrade_diff()` compares the
223
+ -- two, so that a pack which moves a tax from one territory to another shows up
224
+ -- as a difference a company is asked about rather than as a silent divergence.
225
+ -- ---------------------------------------------------------------------------
226
+
227
+ create or replace function install_country_template(
228
+ p_company_id uuid,
229
+ p_country char(2),
230
+ p_language char(2) default null,
231
+ p_chart_code text default null
232
+ )
233
+ returns void
234
+ language plpgsql
235
+ as $$
236
+ declare
237
+ v_defaults country_defaults%rowtype;
238
+ r record;
239
+ v_tax_id uuid;
240
+ v_language char(2);
241
+ v_version text;
242
+ v_chart text;
243
+ v_charts text;
244
+ begin
245
+ if not exists (select 1 from companies where id = p_company_id) then
246
+ raise exception 'unknown_company: %', p_company_id;
247
+ end if;
248
+
249
+ -- Which chart. The caller names one, or the pack's default stands. A named
250
+ -- chart that does not exist is an error that lists what does: guessing here
251
+ -- would install the wrong plan of accounts and nobody would notice for a
252
+ -- year.
253
+ if p_chart_code is null then
254
+ select c.code into v_chart
255
+ from chart_templates c
256
+ where c.country = p_country and c.is_default;
257
+ if v_chart is null then
258
+ -- No pack for this country, or a pack that names no default chart.
259
+ raise exception 'unknown_country_template: no chart of accounts seeded for %', p_country;
260
+ end if;
261
+ else
262
+ select c.code into v_chart
263
+ from chart_templates c
264
+ where c.country = p_country and c.code = p_chart_code;
265
+ if v_chart is null then
266
+ select string_agg(c.code, ', ' order by c.code) into v_charts
267
+ from chart_templates c where c.country = p_country;
268
+ raise exception 'unknown_chart: % has no chart %. It has: %',
269
+ p_country, p_chart_code, coalesce(v_charts, 'none');
270
+ end if;
271
+ end if;
272
+
273
+ if not exists (
274
+ select 1 from account_templates
275
+ where country = p_country and chart_code = v_chart
276
+ ) then
277
+ raise exception 'unknown_country_template: chart % of % holds no account', v_chart, p_country;
278
+ end if;
279
+
280
+ -- The language of the copy: what the caller asked for, else what the
281
+ -- company keeps its books in.
282
+ select coalesce(p_language, c.language) into v_language
283
+ from companies c where c.id = p_company_id;
284
+
285
+ -- 1. Accounts, without the hierarchy.
286
+ insert into accounts (company_id, code, name, name_i18n, statement_hint,
287
+ account_type, reconcilable)
288
+ select p_company_id, t.code,
289
+ label_for(t.name, t.name_i18n, array[v_language]),
290
+ t.name_i18n, t.statement_hint, t.account_type, t.reconcilable
291
+ from account_templates t
292
+ where t.country = p_country
293
+ and t.chart_code = v_chart
294
+ on conflict (company_id, code) do nothing;
295
+
296
+ -- 2. Hierarchy, now that every code exists.
297
+ update accounts a
298
+ set parent_id = p.id
299
+ from account_templates t
300
+ join accounts p on p.company_id = p_company_id and p.code = t.parent_code
301
+ where a.company_id = p_company_id
302
+ and a.code = t.code
303
+ and t.country = p_country
304
+ and t.chart_code = v_chart
305
+ and t.parent_code is not null
306
+ and a.parent_id is null;
307
+
308
+ -- 3. Journals. Common to every chart of the country.
309
+ insert into journals (company_id, code, name, name_i18n, journal_type)
310
+ select p_company_id, t.code,
311
+ label_for(t.name, t.name_i18n, array[v_language]),
312
+ t.name_i18n, t.journal_type
313
+ from journal_templates t
314
+ where t.country = p_country
315
+ on conflict (company_id, code) do nothing;
316
+
317
+ -- 4. Taxes and their postings. Also common: a chart changes how a company
318
+ -- presents itself, not what it owes.
319
+ for r in
320
+ select * from tax_templates where country = p_country order by sequence, code
321
+ loop
322
+ v_tax_id := null;
323
+ insert into taxes (company_id, code, name, name_i18n, description, amount_type, amount,
324
+ applies_to, treatment, country, valid_from, valid_to,
325
+ legal_reference, vat_category, exemption_code, sequence,
326
+ tax_kind, recoverable, jurisdiction, price_include,
327
+ cash_basis, cash_basis_transition_account_id,
328
+ applies_seller_territory, applies_buyer_territory,
329
+ applies_supply_territory)
330
+ values (p_company_id, r.code,
331
+ label_for(r.name, r.name_i18n, array[v_language]),
332
+ r.name_i18n, r.description, r.amount_type, r.amount,
333
+ r.applies_to, r.treatment, p_country, r.valid_from, r.valid_to,
334
+ r.legal_reference, r.vat_category, r.exemption_code, r.sequence,
335
+ r.tax_kind, r.recoverable, r.jurisdiction, r.price_include,
336
+ r.cash_basis, account_id_by_code(p_company_id, r.cash_basis_transition_account_code),
337
+ r.applies_seller_territory, r.applies_buyer_territory,
338
+ r.applies_supply_territory)
339
+ on conflict (company_id, code) do nothing
340
+ returning id into v_tax_id;
341
+
342
+ if v_tax_id is null then
343
+ continue;
344
+ end if;
345
+
346
+ insert into tax_postings (tax_id, company_id, document_kind, posting_type,
347
+ factor_percent, account_id, declaration_box,
348
+ declaration_boxes, box_factor_percent, report_code,
349
+ sequence)
350
+ select v_tax_id, p_company_id, tp.document_kind, tp.posting_type,
351
+ tp.factor_percent, account_id_by_code(p_company_id, tp.account_code),
352
+ tp.declaration_box, tp.declaration_boxes, tp.box_factor_percent,
353
+ tp.report_code, tp.sequence
354
+ from tax_posting_templates tp
355
+ where tp.tax_template_id = r.id
356
+ order by tp.sequence;
357
+ end loop;
358
+
359
+ -- 5. Roles.
360
+ select * into v_defaults from country_defaults where country = p_country;
361
+ if found then
362
+ update companies c
363
+ set receivable_account_id = coalesce(c.receivable_account_id, account_id_by_code(p_company_id, v_defaults.receivable_code)),
364
+ payable_account_id = coalesce(c.payable_account_id, account_id_by_code(p_company_id, v_defaults.payable_code)),
365
+ suspense_account_id = coalesce(c.suspense_account_id, account_id_by_code(p_company_id, v_defaults.suspense_code)),
366
+ rounding_account_id = coalesce(c.rounding_account_id, account_id_by_code(p_company_id, v_defaults.rounding_code)),
367
+ retained_earnings_account_id = coalesce(c.retained_earnings_account_id, account_id_by_code(p_company_id, v_defaults.retained_earnings_code)),
368
+ default_sales_account_id = coalesce(c.default_sales_account_id, account_id_by_code(p_company_id, v_defaults.sales_account_code)),
369
+ default_purchase_account_id = coalesce(c.default_purchase_account_id, account_id_by_code(p_company_id, v_defaults.purchase_account_code)),
370
+ sales_journal_id = coalesce(c.sales_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.sales_journal_code)),
371
+ purchase_journal_id = coalesce(c.purchase_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.purchase_journal_code)),
372
+ miscellaneous_journal_id = coalesce(c.miscellaneous_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.misc_journal_code))
373
+ where c.id = p_company_id;
374
+
375
+ -- 6. The financial journals point at their account.
376
+ update journals j
377
+ set default_account_id = account_id_by_code(p_company_id, v_defaults.bank_account_code)
378
+ where j.company_id = p_company_id
379
+ and j.journal_type = 'bank'
380
+ and j.default_account_id is null
381
+ and v_defaults.bank_account_code is not null;
382
+
383
+ update journals j
384
+ set default_account_id = account_id_by_code(p_company_id, v_defaults.cash_account_code)
385
+ where j.company_id = p_company_id
386
+ and j.journal_type = 'cash'
387
+ and j.default_account_id is null
388
+ and v_defaults.cash_account_code is not null;
389
+ end if;
390
+
391
+ -- 7. What was copied, from which version, and on which chart.
392
+ v_version := coalesce((select version from country_packs where country = p_country), '1.0.0');
393
+
394
+ insert into company_packs (company_id, country, version, chart_code)
395
+ values (p_company_id, p_country, v_version, v_chart)
396
+ on conflict (company_id, country) do update
397
+ set version = excluded.version,
398
+ chart_code = excluded.chart_code,
399
+ upgraded_at = now()
400
+ where company_packs.version is distinct from excluded.version
401
+ or company_packs.chart_code is distinct from excluded.chart_code;
402
+
403
+ -- 8. The working chart. Everything wired above is pinned, so a company opens
404
+ -- on the accounts its country model actually points at rather than on the
405
+ -- whole transcription.
406
+ perform pin_referenced_accounts(p_company_id);
407
+ end;
408
+ $$;
409
+
410
+
411
+ create or replace function pack_upgrade_diff(
412
+ p_company_id uuid,
413
+ p_country char(2) default null
414
+ )
415
+ returns table (
416
+ object text,
417
+ key text,
418
+ change text,
419
+ rule pack_change_rule,
420
+ detail jsonb
421
+ )
422
+ language plpgsql
423
+ stable
424
+ as $$
425
+ declare
426
+ v_country char(2);
427
+ v_chart text;
428
+ begin
429
+ select coalesce(p_country, c.country) into v_country
430
+ from companies c where c.id = p_company_id;
431
+ if v_country is null then
432
+ raise exception 'unknown_company: %', p_company_id;
433
+ end if;
434
+
435
+ select cp.chart_code into v_chart
436
+ from company_packs cp
437
+ where cp.company_id = p_company_id and cp.country = v_country;
438
+ if v_chart is null then
439
+ raise exception 'no_pack_installed: company % holds no % pack, so there is nothing to upgrade from', p_company_id, v_country;
440
+ end if;
441
+
442
+ -- Accounts -----------------------------------------------------------------
443
+ return query
444
+ select 'account', t.code, 'missing', 'addition'::pack_change_rule,
445
+ jsonb_build_object('pack', jsonb_build_object('name', t.name, 'account_type', t.account_type))
446
+ from account_templates t
447
+ where t.country = v_country and t.chart_code = v_chart
448
+ and not exists (select 1 from accounts a where a.company_id = p_company_id and a.code = t.code);
449
+
450
+ return query
451
+ select 'account', a.code, 'differs', 'review'::pack_change_rule,
452
+ jsonb_build_object(
453
+ 'pack', jsonb_build_object('name', t.name, 'account_type', t.account_type),
454
+ 'company', jsonb_build_object('name', a.name, 'account_type', a.account_type))
455
+ from accounts a
456
+ join account_templates t
457
+ on t.country = v_country and t.chart_code = v_chart and t.code = a.code
458
+ where a.company_id = p_company_id
459
+ and (label_for(t.name, t.name_i18n, preferred_languages(p_company_id)) is distinct from a.name
460
+ or t.account_type is distinct from a.account_type);
461
+
462
+ return query
463
+ select 'account', a.code, 'company_only', 'review'::pack_change_rule,
464
+ jsonb_build_object('company', jsonb_build_object('name', a.name, 'account_type', a.account_type))
465
+ from accounts a
466
+ where a.company_id = p_company_id
467
+ and not exists (select 1 from account_templates t
468
+ where t.country = v_country and t.chart_code = v_chart and t.code = a.code);
469
+
470
+ -- Journals -----------------------------------------------------------------
471
+ return query
472
+ select 'journal', t.code, 'missing', 'addition'::pack_change_rule,
473
+ jsonb_build_object('pack', jsonb_build_object('name', t.name, 'journal_type', t.journal_type))
474
+ from journal_templates t
475
+ where t.country = v_country
476
+ and not exists (select 1 from journals j where j.company_id = p_company_id and j.code = t.code);
477
+
478
+ return query
479
+ select 'journal', j.code, 'differs', 'review'::pack_change_rule,
480
+ jsonb_build_object(
481
+ 'pack', jsonb_build_object('name', t.name, 'journal_type', t.journal_type),
482
+ 'company', jsonb_build_object('name', j.name, 'journal_type', j.journal_type))
483
+ from journals j
484
+ join journal_templates t on t.country = v_country and t.code = j.code
485
+ where j.company_id = p_company_id
486
+ and (t.name is distinct from j.name or t.journal_type is distinct from j.journal_type);
487
+
488
+ -- Taxes --------------------------------------------------------------------
489
+ return query
490
+ select 'tax', t.code, 'missing', 'addition'::pack_change_rule,
491
+ jsonb_build_object('pack', jsonb_build_object(
492
+ 'name', t.name, 'amount', t.amount, 'amount_type', t.amount_type,
493
+ 'valid_from', t.valid_from, 'valid_to', t.valid_to))
494
+ from tax_templates t
495
+ where t.country = v_country
496
+ and not exists (select 1 from taxes x where x.company_id = p_company_id and x.code = t.code);
497
+
498
+ -- A validity the pack has closed and the company still holds open, or holds
499
+ -- open longer. This is a rate change arriving: the old tax stops on the day
500
+ -- the pack says, and the new rate is an addition above.
501
+ return query
502
+ select 'tax', x.code, 'valid_to', 'closure'::pack_change_rule,
503
+ jsonb_build_object('pack', jsonb_build_object('valid_to', t.valid_to),
504
+ 'company', jsonb_build_object('valid_to', x.valid_to))
505
+ from taxes x
506
+ join tax_templates t on t.country = v_country and t.code = x.code
507
+ where x.company_id = p_company_id
508
+ and t.valid_to is not null
509
+ and (x.valid_to is null or x.valid_to > t.valid_to);
510
+
511
+ return query
512
+ select 'tax', x.code, 'differs', 'review'::pack_change_rule,
513
+ jsonb_build_object(
514
+ 'pack', jsonb_build_object('name', t.name, 'amount', t.amount,
515
+ 'amount_type', t.amount_type, 'treatment', t.treatment,
516
+ 'applies_to', t.applies_to, 'tax_kind', t.tax_kind,
517
+ 'recoverable', t.recoverable, 'valid_from', t.valid_from,
518
+ 'applies_seller_territory', t.applies_seller_territory,
519
+ 'applies_buyer_territory', t.applies_buyer_territory,
520
+ 'applies_supply_territory', t.applies_supply_territory),
521
+ 'company', jsonb_build_object('name', x.name, 'amount', x.amount,
522
+ 'amount_type', x.amount_type, 'treatment', x.treatment,
523
+ 'applies_to', x.applies_to, 'tax_kind', x.tax_kind,
524
+ 'recoverable', x.recoverable, 'valid_from', x.valid_from,
525
+ 'applies_seller_territory', x.applies_seller_territory,
526
+ 'applies_buyer_territory', x.applies_buyer_territory,
527
+ 'applies_supply_territory', x.applies_supply_territory))
528
+ from taxes x
529
+ join tax_templates t on t.country = v_country and t.code = x.code
530
+ where x.company_id = p_company_id
531
+ and (label_for(t.name, null, preferred_languages(p_company_id)) is distinct from x.name
532
+ or t.amount is distinct from x.amount
533
+ or t.amount_type is distinct from x.amount_type
534
+ or t.treatment is distinct from x.treatment
535
+ or t.applies_to is distinct from x.applies_to
536
+ or t.tax_kind is distinct from x.tax_kind
537
+ or t.recoverable is distinct from x.recoverable
538
+ or t.valid_from is distinct from x.valid_from
539
+ -- Where a tax applies is a fact about the tax, and a pack that moves
540
+ -- it is a pack the company has to be shown.
541
+ or t.applies_seller_territory is distinct from x.applies_seller_territory
542
+ or t.applies_buyer_territory is distinct from x.applies_buyer_territory
543
+ or t.applies_supply_territory is distinct from x.applies_supply_territory);
544
+
545
+ return query
546
+ select 'tax', x.code, 'company_only', 'review'::pack_change_rule,
547
+ jsonb_build_object('company', jsonb_build_object('name', x.name, 'amount', x.amount))
548
+ from taxes x
549
+ where x.company_id = p_company_id
550
+ and x.country = v_country
551
+ and not exists (select 1 from tax_templates t where t.country = v_country and t.code = x.code);
552
+
553
+ -- What a tax books. Compared as a whole per tax rather than line by line: a
554
+ -- posting has no identity of its own, and "this tax now books its
555
+ -- non-deductible share somewhere else" is one fact, not four.
556
+ return query
557
+ with pack as (
558
+ select t.code,
559
+ jsonb_agg(jsonb_build_object('document_kind', tp.document_kind,
560
+ 'posting_type', tp.posting_type,
561
+ 'factor_percent', tp.factor_percent,
562
+ 'account_code', tp.account_code,
563
+ 'declaration_box', tp.declaration_box,
564
+ 'declaration_boxes', to_jsonb(tp.declaration_boxes),
565
+ 'box_factor_percent', tp.box_factor_percent,
566
+ 'report_code', tp.report_code)
567
+ order by tp.document_kind, tp.posting_type, tp.sequence) as postings
568
+ from tax_templates t
569
+ join tax_posting_templates tp on tp.tax_template_id = t.id
570
+ where t.country = v_country
571
+ group by t.code
572
+ ), held as (
573
+ select x.code,
574
+ jsonb_agg(jsonb_build_object('document_kind', p.document_kind,
575
+ 'posting_type', p.posting_type,
576
+ 'factor_percent', p.factor_percent,
577
+ 'account_code', a.code,
578
+ 'declaration_box', p.declaration_box,
579
+ 'declaration_boxes', to_jsonb(p.declaration_boxes),
580
+ 'box_factor_percent', p.box_factor_percent,
581
+ 'report_code', p.report_code)
582
+ order by p.document_kind, p.posting_type, p.sequence) as postings
583
+ from taxes x
584
+ join tax_postings p on p.tax_id = x.id
585
+ left join accounts a on a.id = p.account_id
586
+ where x.company_id = p_company_id
587
+ group by x.code
588
+ )
589
+ select 'tax_posting', pack.code, 'differs', 'review'::pack_change_rule,
590
+ jsonb_build_object('pack', pack.postings, 'company', held.postings)
591
+ from pack
592
+ join held on held.code = pack.code
593
+ where pack.postings is distinct from held.postings;
594
+ end;
595
+ $$;
596
+
597
+
598
+ create or replace function pack_upgrade(
599
+ p_company_id uuid,
600
+ p_country char(2) default null,
601
+ p_apply boolean default false
602
+ )
603
+ returns jsonb
604
+ language plpgsql
605
+ security definer
606
+ set search_path = public, pg_temp
607
+ as $$
608
+ declare
609
+ v_country char(2);
610
+ v_chart text;
611
+ v_from text;
612
+ v_to text;
613
+ v_applied jsonb := '[]'::jsonb;
614
+ v_listed jsonb := '[]'::jsonb;
615
+ v_refused jsonb := '[]'::jsonb;
616
+ d record;
617
+ v_tax_id uuid;
618
+ v_template tax_templates%rowtype;
619
+ begin
620
+ select coalesce(p_country, c.country) into v_country
621
+ from companies c where c.id = p_company_id;
622
+ if v_country is null then
623
+ raise exception 'unknown_company: %', p_company_id;
624
+ end if;
625
+
626
+ if auth.uid() is not null and not has_capability(p_company_id, 'company.write') then
627
+ raise exception 'not_allowed: upgrading a country pack needs company.write';
628
+ end if;
629
+
630
+ select cp.version, cp.chart_code into v_from, v_chart
631
+ from company_packs cp
632
+ where cp.company_id = p_company_id and cp.country = v_country;
633
+ if v_chart is null then
634
+ raise exception 'no_pack_installed: company % holds no % pack, so there is nothing to upgrade from', p_company_id, v_country;
635
+ end if;
636
+
637
+ select version into v_to from country_packs where country = v_country;
638
+ if v_to is null then
639
+ raise exception 'no_pack_loaded: this installation holds no % pack. Apply the seeds of this release first.', v_country;
640
+ end if;
641
+
642
+ for d in select * from pack_upgrade_diff(p_company_id, v_country) loop
643
+ -- Never, whatever the caller asked. An upgrade adds and closes; it does
644
+ -- not take a row out of a company's books.
645
+ if d.change = 'company_only' then
646
+ v_refused := v_refused || jsonb_build_object('object', d.object, 'key', d.key,
647
+ 'change', d.change, 'rule', d.rule,
648
+ 'detail', d.detail);
649
+ continue;
650
+ end if;
651
+
652
+ if d.rule = 'review' and not p_apply then
653
+ v_listed := v_listed || jsonb_build_object('object', d.object, 'key', d.key,
654
+ 'change', d.change, 'rule', d.rule,
655
+ 'detail', d.detail);
656
+ continue;
657
+ end if;
658
+
659
+ if d.object = 'account' and d.change = 'missing' then
660
+ insert into accounts (company_id, code, name, name_i18n, statement_hint, account_type, reconcilable)
661
+ select p_company_id, t.code, label_for(t.name, t.name_i18n, preferred_languages(p_company_id)),
662
+ t.name_i18n, t.statement_hint, t.account_type, t.reconcilable
663
+ from account_templates t
664
+ where t.country = v_country and t.chart_code = v_chart and t.code = d.key
665
+ on conflict (company_id, code) do nothing;
666
+
667
+ elsif d.object = 'account' and d.change = 'differs' then
668
+ update accounts a
669
+ set name = label_for(t.name, t.name_i18n, preferred_languages(p_company_id)),
670
+ name_i18n = t.name_i18n,
671
+ account_type = t.account_type
672
+ from account_templates t
673
+ where t.country = v_country and t.chart_code = v_chart and t.code = a.code
674
+ and a.company_id = p_company_id and a.code = d.key;
675
+
676
+ elsif d.object = 'journal' and d.change = 'missing' then
677
+ insert into journals (company_id, code, name, journal_type)
678
+ select p_company_id, t.code, t.name, t.journal_type
679
+ from journal_templates t
680
+ where t.country = v_country and t.code = d.key
681
+ on conflict (company_id, code) do nothing;
682
+
683
+ elsif d.object = 'journal' and d.change = 'differs' then
684
+ update journals j
685
+ set name = t.name, journal_type = t.journal_type
686
+ from journal_templates t
687
+ where t.country = v_country and t.code = j.code
688
+ and j.company_id = p_company_id and j.code = d.key;
689
+
690
+ elsif d.object = 'tax' and d.change = 'missing' then
691
+ select * into v_template from tax_templates where country = v_country and code = d.key;
692
+ insert into taxes (company_id, code, name, description, amount_type, amount,
693
+ applies_to, treatment, country, valid_from, valid_to,
694
+ legal_reference, vat_category, exemption_code, sequence,
695
+ tax_kind, recoverable, jurisdiction, price_include,
696
+ cash_basis, cash_basis_transition_account_id,
697
+ applies_seller_territory, applies_buyer_territory,
698
+ applies_supply_territory)
699
+ values (p_company_id, v_template.code, v_template.name, v_template.description,
700
+ v_template.amount_type, v_template.amount, v_template.applies_to,
701
+ v_template.treatment, v_country, v_template.valid_from, v_template.valid_to,
702
+ v_template.legal_reference, v_template.vat_category, v_template.exemption_code,
703
+ v_template.sequence, v_template.tax_kind, v_template.recoverable,
704
+ v_template.jurisdiction, v_template.price_include, v_template.cash_basis,
705
+ account_id_by_code(p_company_id, v_template.cash_basis_transition_account_code),
706
+ v_template.applies_seller_territory, v_template.applies_buyer_territory,
707
+ v_template.applies_supply_territory)
708
+ on conflict (company_id, code) do nothing
709
+ returning id into v_tax_id;
710
+
711
+ if v_tax_id is not null then
712
+ insert into tax_postings (tax_id, company_id, document_kind, posting_type,
713
+ factor_percent, account_id, declaration_box,
714
+ declaration_boxes, box_factor_percent, report_code,
715
+ sequence)
716
+ select v_tax_id, p_company_id, tp.document_kind, tp.posting_type,
717
+ tp.factor_percent, account_id_by_code(p_company_id, tp.account_code),
718
+ tp.declaration_box, tp.declaration_boxes, tp.box_factor_percent,
719
+ tp.report_code, tp.sequence
720
+ from tax_posting_templates tp
721
+ where tp.tax_template_id = v_template.id
722
+ order by tp.sequence;
723
+ end if;
724
+
725
+ elsif d.object = 'tax' and d.change = 'valid_to' then
726
+ update taxes x
727
+ set valid_to = t.valid_to
728
+ from tax_templates t
729
+ where t.country = v_country and t.code = x.code
730
+ and x.company_id = p_company_id and x.code = d.key;
731
+
732
+ elsif d.object = 'tax' and d.change = 'differs' then
733
+ update taxes x
734
+ set name = t.name, description = t.description, amount = t.amount,
735
+ amount_type = t.amount_type, applies_to = t.applies_to,
736
+ treatment = t.treatment, valid_from = t.valid_from,
737
+ legal_reference = t.legal_reference, vat_category = t.vat_category,
738
+ exemption_code = t.exemption_code, tax_kind = t.tax_kind,
739
+ recoverable = t.recoverable, jurisdiction = t.jurisdiction,
740
+ price_include = t.price_include, cash_basis = t.cash_basis,
741
+ applies_seller_territory = t.applies_seller_territory,
742
+ applies_buyer_territory = t.applies_buyer_territory,
743
+ applies_supply_territory = t.applies_supply_territory
744
+ from tax_templates t
745
+ where t.country = v_country and t.code = x.code
746
+ and x.company_id = p_company_id and x.code = d.key;
747
+
748
+ elsif d.object = 'tax_posting' and d.change = 'differs' then
749
+ select id into v_tax_id from taxes where company_id = p_company_id and code = d.key;
750
+ delete from tax_postings where tax_id = v_tax_id;
751
+ insert into tax_postings (tax_id, company_id, document_kind, posting_type,
752
+ factor_percent, account_id, declaration_box,
753
+ declaration_boxes, box_factor_percent, report_code,
754
+ sequence)
755
+ select v_tax_id, p_company_id, tp.document_kind, tp.posting_type,
756
+ tp.factor_percent, account_id_by_code(p_company_id, tp.account_code),
757
+ tp.declaration_box, tp.declaration_boxes, tp.box_factor_percent,
758
+ tp.report_code, tp.sequence
759
+ from tax_posting_templates tp
760
+ join tax_templates t on t.id = tp.tax_template_id
761
+ where t.country = v_country and t.code = d.key
762
+ order by tp.sequence;
763
+
764
+ else
765
+ -- A difference this function does not know how to apply is listed
766
+ -- rather than guessed at. Raising here would make a new kind of
767
+ -- difference break every upgrade.
768
+ v_listed := v_listed || jsonb_build_object('object', d.object, 'key', d.key,
769
+ 'change', d.change, 'rule', d.rule,
770
+ 'detail', d.detail);
771
+ continue;
772
+ end if;
773
+
774
+ v_applied := v_applied || jsonb_build_object('object', d.object, 'key', d.key,
775
+ 'change', d.change, 'rule', d.rule,
776
+ 'detail', d.detail);
777
+ end loop;
778
+
779
+ -- The version moves only when nothing is left waiting. A company that still
780
+ -- holds a difference it has not decided on has not finished upgrading, and
781
+ -- saying otherwise would hide the difference at the next run.
782
+ if jsonb_array_length(v_listed) = 0 then
783
+ update company_packs
784
+ set version = v_to, upgraded_at = now()
785
+ where company_id = p_company_id and country = v_country;
786
+ end if;
787
+
788
+ perform audit_record(
789
+ p_company_id, 'company_packs', null, v_country, 'update', 'pack_upgraded',
790
+ jsonb_build_object('version', v_from),
791
+ jsonb_build_object('version', case when jsonb_array_length(v_listed) = 0 then v_to else v_from end,
792
+ 'pack_version', v_to,
793
+ 'chart_code', v_chart,
794
+ 'applied', v_applied,
795
+ 'listed', v_listed,
796
+ 'never_applied', v_refused));
797
+
798
+ return jsonb_build_object(
799
+ 'company_id', p_company_id,
800
+ 'country', v_country,
801
+ 'chart_code', v_chart,
802
+ 'from_version', v_from,
803
+ 'to_version', v_to,
804
+ 'version_moved', jsonb_array_length(v_listed) = 0,
805
+ 'applied', v_applied,
806
+ 'listed', v_listed,
807
+ 'never_applied', v_refused);
808
+ end;
809
+ $$;
810
+
811
+
812
+ -- ---------------------------------------------------------------------------
813
+ -- post_document, which refuses a tax the document contradicts
814
+ -- ---------------------------------------------------------------------------
815
+
816
+ create or replace function post_document(p_document_id uuid)
817
+ returns entries
818
+ language plpgsql
819
+ as $$
820
+ declare
821
+ v_doc documents%rowtype;
822
+ v_entry entries%rowtype;
823
+ v_journal uuid;
824
+ v_date date;
825
+ v_is_sale boolean;
826
+ v_is_credit boolean;
827
+ v_kind tax_document_kind;
828
+ v_base_credit boolean;
829
+ v_seq integer := 0;
830
+ v_contact uuid;
831
+ v_maturity date;
832
+ v_terms smallint;
833
+ v_counterpart uuid;
834
+ -- Plain `numeric`, not `numeric(16, 2)`: a local that carries a scale is a
835
+ -- second rounding rule hiding in a declaration, and it is not the currency's.
836
+ -- The only thing that rounds here is `round_amount`.
837
+ v_diff numeric;
838
+ v_diff_cur numeric;
839
+ v_amount numeric;
840
+ v_book numeric;
841
+ v_box_amount numeric;
842
+ v_share numeric;
843
+ v_share_book numeric;
844
+ v_share_box numeric;
845
+ v_left numeric;
846
+ v_left_box numeric;
847
+ v_side_left numeric;
848
+ v_side_left_neg numeric;
849
+ v_side_credit boolean;
850
+ v_label text;
851
+ v_rate numeric(18, 8);
852
+ v_home char(3);
853
+ v_foreign boolean;
854
+ v_total_cur numeric;
855
+ -- Two currencies, one method: the document is stated in its own and the
856
+ -- ledger keeps the company's, and a yen invoice paid in euros rounds each
857
+ -- side at the decimals that side has.
858
+ v_round money_rounding;
859
+ v_book_round money_rounding;
860
+ v_cash boolean;
861
+ v_transition uuid;
862
+ v_postings integer;
863
+ -- Where the three parties of this document are, resolved once and only when
864
+ -- a tax on it asks. `document_territory()` is the ladder.
865
+ v_terr_seller text;
866
+ v_terr_buyer text;
867
+ v_terr_supply text;
868
+ r record;
869
+ p record;
870
+ g record;
871
+ begin
872
+ select * into v_doc from documents where id = p_document_id for update;
873
+ if not found then
874
+ raise exception 'unknown_document: document % does not exist', p_document_id;
875
+ end if;
876
+ if v_doc.state = 'posted' then
877
+ raise exception 'document_already_posted: document % is already posted', p_document_id;
878
+ end if;
879
+ if v_doc.state = 'cancelled' then
880
+ raise exception 'document_cancelled: document % cannot be posted', p_document_id;
881
+ end if;
882
+ if v_doc.entry_id is not null then
883
+ raise exception 'document_already_booked: document % already points at entry %',
884
+ p_document_id, v_doc.entry_id;
885
+ end if;
886
+
887
+ if v_doc.doc_type in ('sale_quote', 'purchase_order') then
888
+ raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
889
+ end if;
890
+
891
+ v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
892
+ v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
893
+ v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
894
+ -- Sale invoice and purchase credit note credit the base; the other two debit it.
895
+ v_base_credit := (v_is_sale <> v_is_credit);
896
+
897
+ v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
898
+
899
+ if not exists (
900
+ select 1 from document_lines
901
+ where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
902
+ ) then
903
+ raise exception 'document_empty: document % has no billable line', p_document_id;
904
+ end if;
905
+
906
+ -- A fixed-amount tax has no basis to spread over lines; refuse rather than
907
+ -- guess.
908
+ if exists (
909
+ select 1 from document_lines l join taxes t on t.id = l.tax_id
910
+ where l.document_id = p_document_id and t.amount_type <> 'percent'
911
+ ) then
912
+ raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
913
+ end if;
914
+
915
+ -- Every tax used must be in force on the accounting date.
916
+ for r in
917
+ select distinct t.id, t.code, t.valid_from, t.valid_to
918
+ from document_lines l join taxes t on t.id = l.tax_id
919
+ where l.document_id = p_document_id
920
+ loop
921
+ if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
922
+ raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
923
+ end if;
924
+ end loop;
925
+
926
+ -- Every tax that names a territory must find the parties where it says they
927
+ -- are. This is the one thing the core may do with a territory condition: it
928
+ -- refuses a tax that cannot apply, and it never chooses, substitutes or
929
+ -- suggests one — the core picks no tax for anybody, in any country.
930
+ --
931
+ -- The resolution is skipped entirely where nothing asks, which is every
932
+ -- document of every pack written before this migration.
933
+ if exists (
934
+ select 1
935
+ from document_lines l
936
+ join taxes t on t.id = l.tax_id
937
+ where l.document_id = p_document_id
938
+ and (t.applies_seller_territory is not null
939
+ or t.applies_buyer_territory is not null
940
+ or t.applies_supply_territory is not null)
941
+ ) then
942
+ v_terr_seller := document_territory(p_document_id, 'seller');
943
+ v_terr_buyer := document_territory(p_document_id, 'buyer');
944
+ v_terr_supply := document_territory(p_document_id, 'supply');
945
+
946
+ for r in
947
+ select distinct t.code,
948
+ t.applies_seller_territory as seller,
949
+ t.applies_buyer_territory as buyer,
950
+ t.applies_supply_territory as supply
951
+ from document_lines l
952
+ join taxes t on t.id = l.tax_id
953
+ where l.document_id = p_document_id
954
+ and (t.applies_seller_territory is not null
955
+ or t.applies_buyer_territory is not null
956
+ or t.applies_supply_territory is not null)
957
+ order by 1
958
+ loop
959
+ for p in
960
+ select v.party, v.wanted, v.actual, v.hint
961
+ from (values
962
+ ('seller', r.seller, v_terr_seller,
963
+ 'territory_code on the party that sells, or the country beside it'),
964
+ ('buyer', r.buyer, v_terr_buyer,
965
+ 'territory_code on the party that buys, or the country beside it'),
966
+ ('supply', r.supply, v_terr_supply,
967
+ 'supply_territory_code or delivery_country on the document')
968
+ ) as v (party, wanted, actual, hint)
969
+ where v.wanted is not null
970
+ order by v.party
971
+ loop
972
+ if p.actual is null then
973
+ raise exception 'no_party_territory: tax % applies where the % is in %, and nothing on this document says where the % is; set %',
974
+ r.code, p.party, p.wanted, p.party, p.hint;
975
+ end if;
976
+ if not territory_within(p.actual, p.wanted) then
977
+ raise exception 'tax_territory_mismatch: tax % applies where the % is in %; on this document the % is in %',
978
+ r.code, p.party, p.wanted, p.party, p.actual;
979
+ end if;
980
+ end loop;
981
+ end loop;
982
+ end if;
983
+
984
+ -- Totals are derived; make sure they reflect the lines as they stand now.
985
+ perform documents_refresh_totals(p_document_id);
986
+ select * into v_doc from documents where id = p_document_id;
987
+
988
+ select c.currency_code into v_home from companies c where c.id = v_doc.company_id;
989
+ v_rate := v_doc.exchange_rate;
990
+ v_foreign := v_doc.currency_code <> v_home;
991
+ v_round := rounding_of(v_doc.company_id, v_doc.currency_code);
992
+ v_book_round := rounding_of(v_doc.company_id);
993
+
994
+ v_journal := coalesce(
995
+ v_doc.journal_id,
996
+ case when v_is_sale
997
+ then (select sales_journal_id from companies where id = v_doc.company_id)
998
+ else (select purchase_journal_id from companies where id = v_doc.company_id)
999
+ end
1000
+ );
1001
+ if v_journal is null then
1002
+ raise exception 'no_journal: set journal_id on the document or a default journal on the company';
1003
+ end if;
1004
+
1005
+ perform assert_period_open(v_doc.company_id, v_date, true);
1006
+
1007
+ v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
1008
+
1009
+ insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
1010
+ description, state, document_id, currency_code)
1011
+ values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
1012
+ coalesce(v_doc.number, v_doc.supplier_reference),
1013
+ v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
1014
+ then ' / ' || v_doc.supplier_reference else '' end,
1015
+ 'draft', p_document_id, v_doc.currency_code)
1016
+ returning * into v_entry;
1017
+
1018
+ -- ------------------------------------------------------------------ bases
1019
+ for r in
1020
+ select l.account_id,
1021
+ l.tax_id,
1022
+ sum(l.amount_untaxed) as base_amount,
1023
+ min(l.sequence) as seq,
1024
+ string_agg(distinct l.name, ', ') as label
1025
+ from document_lines l
1026
+ where l.document_id = p_document_id
1027
+ and l.line_type = 'product'
1028
+ group by l.account_id, l.tax_id
1029
+ having sum(l.amount_untaxed) <> 0
1030
+ order by 4
1031
+ loop
1032
+ select tp.posting_type, tp.declaration_box, tp.factor_percent, tp.box_factor_percent,
1033
+ coalesce(t.cash_basis, false) as cash_basis
1034
+ into p
1035
+ from tax_postings tp
1036
+ join taxes t on t.id = tp.tax_id
1037
+ where tp.tax_id = r.tax_id
1038
+ and tp.document_kind = v_kind
1039
+ and tp.posting_type = 'base'
1040
+ limit 1;
1041
+
1042
+ v_amount := round_amount(r.base_amount * coalesce(p.factor_percent, 100) / 100, v_round);
1043
+ v_book := round_amount(v_amount / v_rate, v_book_round);
1044
+ v_seq := v_seq + 10;
1045
+
1046
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
1047
+ debit, credit, tax_id, tax_line, posting_type,
1048
+ declaration_box, box_amount, currency_code, amount_currency)
1049
+ values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
1050
+ case when v_base_credit then 0 else v_book end,
1051
+ case when v_base_credit then v_book else 0 end,
1052
+ r.tax_id, false, p.posting_type,
1053
+ case when coalesce(p.cash_basis, false) then null else p.declaration_box end,
1054
+ case when p.declaration_box is null then null
1055
+ else round_amount(r.base_amount * coalesce(p.box_factor_percent, 100) / 100 / v_rate,
1056
+ v_book_round) end,
1057
+ v_doc.currency_code,
1058
+ case when v_foreign then v_amount end);
1059
+ end loop;
1060
+
1061
+ -- ------------------------------------------------------------------ taxes
1062
+ for r in
1063
+ select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
1064
+ from document_tax_summary s
1065
+ where s.document_id = p_document_id
1066
+ and s.tax_id is not null
1067
+ and s.tax_amount <> 0
1068
+ order by s.tax_code
1069
+ loop
1070
+ select t.cash_basis, t.cash_basis_transition_account_id
1071
+ into v_cash, v_transition
1072
+ from taxes t where t.id = r.tax_id;
1073
+
1074
+ if v_cash then
1075
+ -- A tax that waits needs somewhere to wait. Refuse by name rather than
1076
+ -- book it on the account it is due on, which would make it due.
1077
+ if v_transition is null then
1078
+ raise exception 'no_cash_basis_account: tax % falls due on collection and names no transition account',
1079
+ r.tax_code;
1080
+ end if;
1081
+ -- One posting per side, or the transition lines of a document cannot be
1082
+ -- told apart when the matching sends each of them on. A tax whose
1083
+ -- postings net out has nothing waiting to collect anyway.
1084
+ select count(*) into v_postings
1085
+ from tax_postings tp
1086
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
1087
+ and tp.posting_type = 'tax';
1088
+ if v_postings > 1 then
1089
+ raise exception 'cash_basis_split_tax: tax % falls due on collection and has % tax postings; it takes one',
1090
+ r.tax_code, v_postings;
1091
+ end if;
1092
+ if exists (select 1 from tax_postings tp
1093
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
1094
+ and tp.posting_type = 'tax_on_base') then
1095
+ raise exception 'cash_basis_tax_on_base: tax % falls due on collection and carries a non-deductible share; a cost is not deferred',
1096
+ r.tax_code;
1097
+ end if;
1098
+ -- And it needs a box to fall due *into*. `settle_cash_basis_tax()` only
1099
+ -- ever looks at lines that carry a `box_amount`, and a posting with no
1100
+ -- `declaration_box` produces none — so the amount would sit on the
1101
+ -- transition account for ever, settled by nothing and reported by
1102
+ -- nothing, with no error anywhere. Refuse it here, where the pack can
1103
+ -- still be corrected, rather than discover it in a balance years later.
1104
+ if not exists (select 1 from tax_postings tp
1105
+ where tp.tax_id = r.tax_id and tp.document_kind = v_kind
1106
+ and tp.posting_type = 'tax'
1107
+ and tp.declaration_box is not null) then
1108
+ 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',
1109
+ r.tax_code;
1110
+ end if;
1111
+ end if;
1112
+
1113
+ -- The postings of one side share out the tax of the group; the last of
1114
+ -- each side takes what is left. Until `tax_on_base` there was never more
1115
+ -- than one posting per side, so this changes no existing tax by a cent —
1116
+ -- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
1117
+ -- where rounding each half on its own would book 0.64 against a document
1118
+ -- that totals 0.63.
1119
+ v_side_left := null;
1120
+ v_side_left_neg := null;
1121
+
1122
+ for p in
1123
+ select tp.posting_type, tp.factor_percent, tp.account_id,
1124
+ tp.declaration_box, tp.box_factor_percent,
1125
+ case when tp.factor_percent >= 0 then 1 else -1 end as side,
1126
+ sum(abs(tp.factor_percent))
1127
+ over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
1128
+ as side_factor,
1129
+ row_number() over (
1130
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end
1131
+ order by tp.sequence, tp.id)
1132
+ = count(*) over (
1133
+ partition by case when tp.factor_percent >= 0 then 1 else -1 end)
1134
+ as is_last_of_side
1135
+ from tax_postings tp
1136
+ where tp.tax_id = r.tax_id
1137
+ and tp.document_kind = v_kind
1138
+ and tp.posting_type in ('tax', 'tax_on_base')
1139
+ order by tp.sequence, tp.id
1140
+ loop
1141
+ -- The tax of the group was rounded once, in the view. Every posting is
1142
+ -- a share of that one figure, never of a re-derived one.
1143
+ if p.side >= 0 then
1144
+ if v_side_left is null then
1145
+ v_side_left := round_amount(r.tax_amount * p.side_factor / 100, v_round);
1146
+ end if;
1147
+ if p.is_last_of_side then
1148
+ v_amount := v_side_left;
1149
+ else
1150
+ v_amount := round_amount(r.tax_amount * abs(p.factor_percent) / 100, v_round);
1151
+ v_side_left := v_side_left - v_amount;
1152
+ end if;
1153
+ else
1154
+ if v_side_left_neg is null then
1155
+ v_side_left_neg := round_amount(r.tax_amount * p.side_factor / 100, v_round);
1156
+ end if;
1157
+ if p.is_last_of_side then
1158
+ v_amount := v_side_left_neg;
1159
+ else
1160
+ v_amount := round_amount(r.tax_amount * abs(p.factor_percent) / 100, v_round);
1161
+ v_side_left_neg := v_side_left_neg - v_amount;
1162
+ end if;
1163
+ end if;
1164
+
1165
+ if v_amount = 0 then
1166
+ continue;
1167
+ end if;
1168
+ -- A positive factor keeps the side of the base, a negative one flips it.
1169
+ v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
1170
+ -- The box keeps its own rounding: `box_factor_percent` was always
1171
+ -- independent from `factor_percent`, because a declaration figure is
1172
+ -- not a ledger figure and only the ledger has to balance.
1173
+ v_box_amount := case when p.declaration_box is null then null
1174
+ else round_amount(r.tax_amount * p.box_factor_percent / 100 / v_rate,
1175
+ v_book_round) end;
1176
+ v_book := round_amount(v_amount / v_rate, v_book_round);
1177
+
1178
+ if p.posting_type = 'tax' then
1179
+ v_seq := v_seq + 10;
1180
+
1181
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
1182
+ debit, credit, tax_id, tax_line, posting_type,
1183
+ declaration_box, box_amount, currency_code, amount_currency)
1184
+ values (v_entry.id, v_doc.company_id,
1185
+ case when v_cash then v_transition else p.account_id end,
1186
+ v_seq, r.tax_name,
1187
+ case when v_side_credit then 0 else v_book end,
1188
+ case when v_side_credit then v_book else 0 end,
1189
+ r.tax_id, true, p.posting_type,
1190
+ case when v_cash then null else p.declaration_box end,
1191
+ v_box_amount, v_doc.currency_code,
1192
+ case when v_foreign then v_amount end);
1193
+ continue;
1194
+ end if;
1195
+
1196
+ -- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
1197
+ -- lines it taxes, split in proportion to their base. The last share
1198
+ -- takes whatever is left, so the shares add up to the amount that was
1199
+ -- rounded once on the group and the entry still balances to the cent.
1200
+ v_left := v_amount;
1201
+ v_left_box := v_box_amount;
1202
+
1203
+ for g in
1204
+ select account_id,
1205
+ base_amount,
1206
+ seq,
1207
+ sum(base_amount) over () as total_base,
1208
+ row_number() over (order by seq) = count(*) over () as is_last
1209
+ from (
1210
+ select l.account_id,
1211
+ sum(l.amount_untaxed) as base_amount,
1212
+ min(l.sequence) as seq
1213
+ from document_lines l
1214
+ where l.document_id = p_document_id
1215
+ and l.line_type = 'product'
1216
+ and l.tax_id = r.tax_id
1217
+ group by l.account_id
1218
+ having sum(l.amount_untaxed) <> 0
1219
+ ) as groups
1220
+ order by seq
1221
+ loop
1222
+ if g.is_last then
1223
+ v_share := v_left;
1224
+ v_share_box := v_left_box;
1225
+ else
1226
+ v_share := round_amount(v_amount * g.base_amount / g.total_base, v_round);
1227
+ v_share_box := case when v_box_amount is null then null
1228
+ else round_amount(v_box_amount * g.base_amount / g.total_base,
1229
+ v_book_round) end;
1230
+ v_left := v_left - v_share;
1231
+ v_left_box := v_left_box - v_share_box;
1232
+ end if;
1233
+
1234
+ if v_share = 0 then
1235
+ continue;
1236
+ end if;
1237
+
1238
+ v_seq := v_seq + 10;
1239
+ v_share_book := round_amount(v_share / v_rate, v_book_round);
1240
+
1241
+ -- `tax_line` stays false: the amount is on a base account and belongs
1242
+ -- to the base side of the declaration, which is why the Belgian grids
1243
+ -- 82 and 83 report it together with the base.
1244
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
1245
+ debit, credit, tax_id, tax_line, posting_type,
1246
+ declaration_box, box_amount, currency_code, amount_currency)
1247
+ values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
1248
+ case when v_side_credit then 0 else v_share_book end,
1249
+ case when v_side_credit then v_share_book else 0 end,
1250
+ r.tax_id, false, p.posting_type,
1251
+ p.declaration_box, v_share_box, v_doc.currency_code,
1252
+ case when v_foreign then v_share end);
1253
+ end loop;
1254
+ end loop;
1255
+ end loop;
1256
+
1257
+ -- ------------------------------------------------------------ counterpart
1258
+ select total_debit - total_credit into v_diff from entries where id = v_entry.id;
1259
+ select coalesce(sum(case when l.debit > 0 then l.amount_currency else -l.amount_currency end), 0)
1260
+ into v_diff_cur
1261
+ from entry_lines l where l.entry_id = v_entry.id;
1262
+
1263
+ if v_diff = 0 then
1264
+ raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
1265
+ end if;
1266
+
1267
+ v_contact := commercial_entity(v_doc.contact_id);
1268
+ v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
1269
+
1270
+ select payment_terms_days into v_terms from contacts where id = v_contact;
1271
+ v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
1272
+
1273
+ v_amount := abs(v_diff);
1274
+ -- The counterpart balances the entry in both currencies. The total it is
1275
+ -- checked against is the document's own, which is the currency
1276
+ -- `amount_total` is stated in.
1277
+ v_total_cur := case when v_foreign then abs(v_diff_cur) else v_amount end;
1278
+ v_seq := v_seq + 10;
1279
+
1280
+ insert into entry_lines (entry_id, company_id, account_id, sequence, name,
1281
+ debit, credit, contact_id, date_maturity, currency_code,
1282
+ amount_currency)
1283
+ values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
1284
+ case when v_diff > 0 then 0 else v_amount end,
1285
+ case when v_diff > 0 then v_amount else 0 end,
1286
+ v_contact, v_maturity, v_doc.currency_code,
1287
+ case when v_foreign then v_total_cur end);
1288
+
1289
+ -- The ledger is right by construction. If the header disagrees, the header
1290
+ -- is what is wrong, and we say so instead of quietly patching a line.
1291
+ -- Half a unit of the document's own currency, which is what `0.005` used to
1292
+ -- mean when every currency was assumed to have cents.
1293
+ if abs(v_total_cur - abs(v_doc.amount_total)) > currency_unit(v_round) / 2 then
1294
+ raise exception 'document_total_mismatch: document % totals % but its lines book %',
1295
+ p_document_id, v_doc.amount_total, v_total_cur;
1296
+ end if;
1297
+
1298
+ -- --------------------------------------------------------------- posting
1299
+ v_entry := post_entry(v_entry.id);
1300
+
1301
+ update documents
1302
+ set state = 'posted',
1303
+ number = coalesce(number, v_entry.number),
1304
+ entry_id = v_entry.id,
1305
+ accounting_date = v_date
1306
+ where id = p_document_id;
1307
+
1308
+ return v_entry;
1309
+ end;
1310
+ $$;
1311
+
1312
+ comment on function post_document(uuid) is
1313
+ 'Books a document: one entry, the bases on the accounts of the lines, the tax of each group rounded once and shared over the postings of the tax, each ledger line naming the posting type that wrote it. Refuses a tax whose applies_*_territory the document contradicts, by name, before anything reaches the ledger — and chooses no tax for anybody.';
1314
+
1315
+ -- No new table, so no new policy: the six columns sit on tables whose row
1316
+ -- level security already decides who may read them, and `territories` keeps
1317
+ -- the select policy it was created with. The two new functions are granted
1318
+ -- above, beside their revokes.