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,1190 @@
1
+ -- Ekwo — the `assets` module: fixed assets, their depreciation and their disposal.
2
+ --
3
+ -- One schema, its own row level security, and not a line of the socle changed.
4
+ -- It depends on `public` by foreign key — companies, accounts, contacts,
5
+ -- products, document lines — and it reaches the ledger only through
6
+ -- `public.post_module_entry()`, which builds the draft and calls
7
+ -- `post_entry()`. The words `entries` and `entry_lines` do not appear in a
8
+ -- statement of this file, and a test over `modules/**` proves it.
9
+ --
10
+ -- **The engine knows no country.** What a country decides is data, in
11
+ -- `packs/<cc>/assets.json`, compiled into the two reference tables below:
12
+ -- `assets.country_rules` says how that country takes a prorata, whether its
13
+ -- declining balance is capped and how it derecognises an asset;
14
+ -- `assets.category_templates` says the usual duration and coefficient of a
15
+ -- kind of asset. Neither is copied into a company — a usual duration admitted
16
+ -- by an administration is not something an operator redefines, any more than a
17
+ -- box of a VAT return is — and an asset that departs from one says so in its
18
+ -- own columns, which is what makes the templates a suggestion rather than a
19
+ -- rule.
20
+ --
21
+ -- **The accounts a disposal lands on are roles of the chart**, named by
22
+ -- `defaults.roles` in the pack and read from `public.country_defaults`. A pack
23
+ -- that has not named them makes a disposal refuse, by name.
24
+ --
25
+ -- **Rounding.** Every amount is rounded to the cent as it is computed and the
26
+ -- last line of a schedule takes the remainder, so the schedule sums to exactly
27
+ -- `cost - residual_value` whatever happened on the way. That makes the
28
+ -- country's `rounding_method` — which is about sharing out a tax — beside the
29
+ -- point here, and there is deliberately no second rounding parameter for
30
+ -- depreciation: one calculation, one rule, and the proof is a test that sums
31
+ -- every line of every golden schedule.
32
+
33
+ create schema assets;
34
+
35
+ comment on schema assets is
36
+ 'Ekwo module `assets`: fixed assets, depreciation schedules and disposals. Posts to the ledger only through public.post_module_entry().';
37
+
38
+ -- ---------------------------------------------------------------------------
39
+ -- Vocabulary
40
+ -- ---------------------------------------------------------------------------
41
+
42
+ create type assets.depreciation_method as enum (
43
+ 'straight_line',
44
+ 'declining_balance',
45
+ 'units_of_production'
46
+ );
47
+
48
+ comment on type assets.depreciation_method is
49
+ 'How an annuity is worked out. units_of_production is in the enum and refused by generate_schedule(): a schedule by output needs the units of each period, which this module does not record — refusing is what post_document does with a fixed-amount tax rather than guessing.';
50
+
51
+ create type assets.asset_state as enum ('draft', 'active', 'disposed', 'fully_depreciated');
52
+
53
+ create type assets.prorata_rule as enum ('none', 'days', 'months');
54
+
55
+ comment on type assets.prorata_rule is
56
+ 'How much of the first period an asset is depreciated over. none: the whole annuity whatever the date. days: from the day it entered service. months: from the first day of the month it entered service, which is what a French declining balance does.';
57
+
58
+ create type assets.day_count as enum ('actual', 'thirty_360');
59
+
60
+ comment on type assets.day_count is
61
+ 'Which calendar a prorata in days counts on: the real one, or the commercial year of twelve thirty-day months.';
62
+
63
+ create type assets.disposal_style as enum ('net_result', 'gross');
64
+
65
+ comment on type assets.disposal_style is
66
+ 'How a country derecognises an asset. net_result: the difference between the proceeds and the net book value lands on one account, a gain or a loss (Belgium 763/663). gross: the net book value is a charge and the proceeds an income, both in full, and the income statement prints the two (France 675/775). Named after the mechanism, never after a country.';
67
+
68
+ -- ---------------------------------------------------------------------------
69
+ -- What a country says
70
+ -- ---------------------------------------------------------------------------
71
+
72
+ create table assets.country_rules (
73
+ country char(2) primary key,
74
+ prorata_straight_line assets.prorata_rule not null,
75
+ prorata_declining assets.prorata_rule not null,
76
+ day_count assets.day_count not null default 'actual',
77
+ declining_cap_percent numeric(7, 3),
78
+ declining_switch_to_linear boolean not null default true,
79
+ disposal_style assets.disposal_style,
80
+ legal_reference text,
81
+ constraint assets_country_rules_country_format check (country ~ '^[A-Z]{2}$'),
82
+ constraint assets_country_rules_cap check (
83
+ declining_cap_percent is null or (declining_cap_percent > 0 and declining_cap_percent <= 100)
84
+ )
85
+ );
86
+
87
+ comment on table assets.country_rules is
88
+ 'How one country depreciates and derecognises. Filled by `ekwo pack build` from packs/<cc>/assets.json, read where it stands, never copied into a company.';
89
+ comment on column assets.country_rules.declining_cap_percent is
90
+ 'Largest annuity a declining balance may take in one period, as a percentage of the acquisition value. Null where the country caps nothing.';
91
+ comment on column assets.country_rules.declining_switch_to_linear is
92
+ 'Whether the declining balance switches to the straight line over the remaining periods once that gives the larger annuity. True everywhere the declining balance is a tax incentive rather than a valuation method.';
93
+
94
+ create table assets.category_templates (
95
+ country char(2) not null,
96
+ code text not null,
97
+ name text not null,
98
+ name_i18n jsonb not null default '{}'::jsonb,
99
+ method assets.depreciation_method not null,
100
+ duration_months integer not null,
101
+ coefficient numeric(7, 3),
102
+ prorata assets.prorata_rule,
103
+ account_type account_type,
104
+ sequence integer not null default 10,
105
+ legal_reference text,
106
+ primary key (country, code),
107
+ constraint assets_category_duration check (duration_months > 0),
108
+ constraint assets_category_coefficient check (
109
+ method <> 'declining_balance' or coefficient is not null
110
+ )
111
+ );
112
+
113
+ comment on table assets.category_templates is
114
+ 'The usual duration and method of a kind of asset in one country, with the source it comes from. A suggestion an asset may depart from, which is why it is never copied into a company.';
115
+ comment on column assets.category_templates.prorata is
116
+ 'Overrides the country rule for this category. Null is the ordinary case: the rule of the country, for the method this category uses.';
117
+ comment on column assets.category_templates.account_type is
118
+ 'Which of the eighteen account types the asset account of this category is, so a client can propose the accounts of a chart it has never seen. Advisory: nothing resolves an account from it.';
119
+
120
+ -- ---------------------------------------------------------------------------
121
+ -- The asset
122
+ -- ---------------------------------------------------------------------------
123
+
124
+ create table assets.assets (
125
+ id uuid primary key default gen_random_uuid(),
126
+ company_id uuid not null references public.companies(id) on delete cascade,
127
+ code text not null,
128
+ name text not null,
129
+ description text,
130
+ category_code text,
131
+ -- Where it came from. All three are optional: an asset taken over from a
132
+ -- previous system has no invoice line in this database.
133
+ document_line_id uuid references public.document_lines(id) on delete set null,
134
+ contact_id uuid references public.contacts(id) on delete set null,
135
+ product_id uuid references public.products(id) on delete set null,
136
+ acquisition_date date not null,
137
+ -- Depreciation starts when the asset is used, which is not always the day it
138
+ -- was bought. Null means the two are the same day.
139
+ in_service_date date,
140
+ cost numeric(16, 2) not null,
141
+ residual_value numeric(16, 2) not null default 0,
142
+ method assets.depreciation_method not null default 'straight_line',
143
+ duration_months integer not null,
144
+ coefficient numeric(7, 3),
145
+ prorata assets.prorata_rule,
146
+ asset_account_id uuid not null references public.accounts(id) on delete restrict,
147
+ depreciation_account_id uuid not null references public.accounts(id) on delete restrict,
148
+ expense_account_id uuid not null references public.accounts(id) on delete restrict,
149
+ state assets.asset_state not null default 'draft',
150
+ notes text,
151
+ created_at timestamptz not null default now(),
152
+ updated_at timestamptz not null default now(),
153
+ unique (company_id, code),
154
+ constraint assets_cost_positive check (cost > 0),
155
+ constraint assets_residual_within check (residual_value >= 0 and residual_value < cost),
156
+ constraint assets_duration_positive check (duration_months > 0),
157
+ constraint assets_in_service_after check (in_service_date is null or in_service_date >= acquisition_date),
158
+ constraint assets_declining_has_coefficient check (
159
+ method <> 'declining_balance' or coefficient is not null
160
+ ),
161
+ -- The three accounts belong to the same company as the asset. The composite
162
+ -- key is the socle's own pattern for this: `entry_lines(account_id,
163
+ -- company_id)` does it for the same reason.
164
+ foreign key (asset_account_id, company_id) references public.accounts(id, company_id),
165
+ foreign key (depreciation_account_id, company_id) references public.accounts(id, company_id),
166
+ foreign key (expense_account_id, company_id) references public.accounts(id, company_id),
167
+ foreign key (contact_id, company_id) references public.contacts(id, company_id)
168
+ );
169
+
170
+ comment on table assets.assets is
171
+ 'One fixed asset: what it cost, how it is depreciated, and the three accounts that carry it. The schedule is assets.depreciation_lines.';
172
+ comment on column assets.assets.prorata is
173
+ 'How much of the first period this asset takes. Null reads the country rule for its method, and an asset in a country whose pack says nothing is refused by name rather than given another country''s convention.';
174
+ comment on column assets.assets.coefficient is
175
+ 'Multiplier of the straight-line rate under a declining balance. France 1,25 / 1,75 / 2,25 by duration; Belgium doubles the rate. Required by a check constraint for that method, because a declining balance with no coefficient is a straight line nobody asked for.';
176
+
177
+ create unique index assets_id_company_idx on assets.assets (id, company_id);
178
+ create index assets_company_state_idx on assets.assets (company_id, state);
179
+ create index assets_document_line_idx on assets.assets (document_line_id) where document_line_id is not null;
180
+
181
+ create trigger assets_set_updated_at
182
+ before update on assets.assets
183
+ for each row execute function public.set_updated_at();
184
+
185
+ -- ---------------------------------------------------------------------------
186
+ -- The schedule
187
+ -- ---------------------------------------------------------------------------
188
+
189
+ create table assets.depreciation_lines (
190
+ id uuid primary key default gen_random_uuid(),
191
+ asset_id uuid not null references assets.assets(id) on delete cascade,
192
+ company_id uuid not null references public.companies(id) on delete cascade,
193
+ sequence integer not null,
194
+ period_start date not null,
195
+ period_end date not null,
196
+ amount numeric(16, 2) not null,
197
+ -- After this line. Stored because a schedule is read far more often than it
198
+ -- is written, and because an accountant reads a table of four columns.
199
+ accumulated numeric(16, 2) not null,
200
+ net_book_value numeric(16, 2) not null,
201
+ entry_id uuid references public.entries(id) on delete set null,
202
+ posted_at timestamptz,
203
+ created_at timestamptz not null default now(),
204
+ unique (asset_id, sequence),
205
+ unique (asset_id, period_end),
206
+ constraint assets_line_period check (period_end >= period_start),
207
+ constraint assets_line_amount check (amount >= 0),
208
+ constraint assets_line_posted_has_entry check (posted_at is null or entry_id is not null),
209
+ foreign key (asset_id, company_id) references assets.assets(id, company_id) on delete cascade
210
+ );
211
+
212
+ comment on table assets.depreciation_lines is
213
+ 'One planned period of depreciation. `entry_id` is the entry that booked it, and is what makes running the depreciation of a period twice a no-op.';
214
+
215
+ create index assets_lines_company_period_idx
216
+ on assets.depreciation_lines (company_id, period_end)
217
+ where posted_at is null;
218
+ create index assets_lines_entry_idx on assets.depreciation_lines (entry_id) where entry_id is not null;
219
+
220
+ -- ---------------------------------------------------------------------------
221
+ -- The disposal
222
+ -- ---------------------------------------------------------------------------
223
+
224
+ create table assets.disposals (
225
+ id uuid primary key default gen_random_uuid(),
226
+ asset_id uuid not null unique references assets.assets(id) on delete cascade,
227
+ company_id uuid not null references public.companies(id) on delete cascade,
228
+ disposal_date date not null,
229
+ proceeds numeric(16, 2) not null default 0,
230
+ counterpart_account_id uuid references public.accounts(id) on delete restrict,
231
+ contact_id uuid references public.contacts(id) on delete set null,
232
+ cost numeric(16, 2) not null,
233
+ accumulated numeric(16, 2) not null,
234
+ net_book_value numeric(16, 2) not null,
235
+ -- Positive is a gain. Recorded because it is what the disposal decided, and
236
+ -- recomputing it a year later from a chart that has moved on is how a
237
+ -- register stops tying to the ledger.
238
+ result numeric(16, 2) not null,
239
+ entry_id uuid references public.entries(id) on delete set null,
240
+ created_at timestamptz not null default now(),
241
+ constraint assets_disposal_proceeds check (proceeds >= 0),
242
+ foreign key (asset_id, company_id) references assets.assets(id, company_id) on delete cascade
243
+ );
244
+
245
+ comment on table assets.disposals is
246
+ 'What leaving the books cost or earned: one row per asset, written by assets.dispose_asset(). There is no undo, for the reason there is no unpost.';
247
+
248
+ -- ---------------------------------------------------------------------------
249
+ -- Counting days
250
+ --
251
+ -- Two conventions and no third. `actual` is the calendar. `thirty_360` is the
252
+ -- commercial year — twelve months of thirty days — which is what a French
253
+ -- straight-line prorata is worked out on. Both are written here so that a pack
254
+ -- names one rather than a reader assuming one.
255
+ -- ---------------------------------------------------------------------------
256
+
257
+ create or replace function assets.days360(p_from date, p_to date)
258
+ returns integer
259
+ language sql
260
+ immutable
261
+ as $$
262
+ select ((extract(year from p_to)::int - extract(year from p_from)::int) * 360)
263
+ + ((extract(month from p_to)::int - extract(month from p_from)::int) * 30)
264
+ + (least(extract(day from p_to)::int, 30) - least(extract(day from p_from)::int, 30));
265
+ $$;
266
+
267
+ comment on function assets.days360(date, date) is
268
+ 'Days between two dates on a year of 360 days and months of 30, the day capped at the 30th. Half-open: days360(1 January, 1 January of the next year) is 360.';
269
+
270
+ /**
271
+ * Whole months from the month of `p_from` to the month of `p_to`, inclusive.
272
+ * January to January is one month, January to December is twelve.
273
+ */
274
+ create or replace function assets.months_inclusive(p_from date, p_to date)
275
+ returns integer
276
+ language sql
277
+ immutable
278
+ as $$
279
+ select greatest(
280
+ 0,
281
+ (extract(year from p_to)::int - extract(year from p_from)::int) * 12
282
+ + (extract(month from p_to)::int - extract(month from p_from)::int) + 1
283
+ );
284
+ $$;
285
+
286
+ /**
287
+ * The share of a period an asset is depreciated over.
288
+ *
289
+ * One for every period after the first, and for an asset whose country takes
290
+ * no prorata at all. The date it counts from is the later of the day the asset
291
+ * entered service and the first day of the period, so a full period is a full
292
+ * annuity without the caller having to know which period it is on.
293
+ */
294
+ create or replace function assets.prorata_fraction(
295
+ p_rule assets.prorata_rule,
296
+ p_day_count assets.day_count,
297
+ p_start date,
298
+ p_period_start date,
299
+ p_period_end date
300
+ )
301
+ returns numeric
302
+ language plpgsql
303
+ immutable
304
+ as $$
305
+ declare
306
+ v_from date := greatest(p_start, p_period_start);
307
+ begin
308
+ if p_rule = 'none' or v_from <= p_period_start then
309
+ return 1;
310
+ end if;
311
+ if v_from > p_period_end then
312
+ return 0;
313
+ end if;
314
+
315
+ if p_rule = 'months' then
316
+ return assets.months_inclusive(v_from, p_period_end)::numeric
317
+ / nullif(assets.months_inclusive(p_period_start, p_period_end), 0);
318
+ end if;
319
+
320
+ if p_day_count = 'thirty_360' then
321
+ return assets.days360(v_from, p_period_end + 1)::numeric
322
+ / nullif(assets.days360(p_period_start, p_period_end + 1), 0);
323
+ end if;
324
+
325
+ return ((p_period_end - v_from) + 1)::numeric / nullif((p_period_end - p_period_start) + 1, 0);
326
+ end;
327
+ $$;
328
+
329
+ comment on function assets.prorata_fraction(assets.prorata_rule, assets.day_count, date, date, date) is
330
+ 'The share of a period that runs from the day an asset entered service. A prorata in days counts the day of entry into service itself, which is the convention that makes a full year come to exactly one.';
331
+
332
+ -- ---------------------------------------------------------------------------
333
+ -- What a company follows
334
+ -- ---------------------------------------------------------------------------
335
+
336
+ create or replace function assets.rules(p_company_id uuid)
337
+ returns assets.country_rules
338
+ language plpgsql
339
+ stable
340
+ as $$
341
+ declare
342
+ v_rules assets.country_rules%rowtype;
343
+ begin
344
+ select r.* into v_rules
345
+ from public.companies c
346
+ join assets.country_rules r on r.country = c.country
347
+ where c.id = p_company_id;
348
+ return v_rules;
349
+ end;
350
+ $$;
351
+
352
+ comment on function assets.rules(uuid) is
353
+ 'The depreciation rules of this company''s country, or an empty row when its pack says nothing. The callers name what is missing rather than borrowing another country''s answer.';
354
+
355
+ /**
356
+ * Monthly or yearly, from the settings the company keeps for this module.
357
+ *
358
+ * The default is yearly, and that is a mechanism and not a country: every rule
359
+ * a country writes about depreciation is expressed as an annuity, and a
360
+ * monthly schedule is that annuity split. A company that wants the split asks
361
+ * for it with `{"period": "monthly"}`.
362
+ */
363
+ create or replace function assets.period_is_monthly(p_company_id uuid)
364
+ returns boolean
365
+ language sql
366
+ stable
367
+ as $$
368
+ select coalesce(public.module_settings(p_company_id, 'assets') ->> 'period', 'yearly') = 'monthly';
369
+ $$;
370
+
371
+ /**
372
+ * The twelve-month period an asset's schedule is cut into, from the financial
373
+ * year that covers the day it entered service.
374
+ *
375
+ * Where the company has declared the financial year a period falls in, that
376
+ * year's own end date is used, so a schedule follows the books. Beyond the
377
+ * declared years it rolls twelve months at a time from the same anchor, which
378
+ * is the only thing it can do: a building bought this year is depreciated over
379
+ * twenty, and nineteen of those years have not been declared yet.
380
+ */
381
+ create or replace function assets.period_end_for(p_company_id uuid, p_period_start date)
382
+ returns date
383
+ language sql
384
+ stable
385
+ as $$
386
+ select coalesce(
387
+ (select f.end_date
388
+ from public.fiscal_years f
389
+ where f.company_id = p_company_id
390
+ and f.start_date = p_period_start
391
+ limit 1),
392
+ (p_period_start + interval '1 year' - interval '1 day')::date
393
+ );
394
+ $$;
395
+
396
+ -- ---------------------------------------------------------------------------
397
+ -- create_asset
398
+ -- ---------------------------------------------------------------------------
399
+
400
+ create or replace function assets.create_asset(
401
+ p_company_id uuid,
402
+ p_code text,
403
+ p_name text,
404
+ p_acquisition_date date,
405
+ p_cost numeric,
406
+ p_asset_account text,
407
+ p_depreciation_account text,
408
+ p_expense_account text,
409
+ p_category_code text default null,
410
+ p_duration_months integer default null,
411
+ p_method assets.depreciation_method default null,
412
+ p_coefficient numeric default null,
413
+ p_residual_value numeric default 0,
414
+ p_in_service_date date default null,
415
+ p_document_line_id uuid default null,
416
+ p_contact_id uuid default null,
417
+ p_description text default null
418
+ )
419
+ returns uuid
420
+ language plpgsql
421
+ as $$
422
+ declare
423
+ v_country char(2);
424
+ v_category assets.category_templates%rowtype;
425
+ v_method assets.depreciation_method;
426
+ v_duration integer;
427
+ v_coef numeric(7, 3);
428
+ v_id uuid;
429
+ v_asset uuid;
430
+ v_deprec uuid;
431
+ v_expense uuid;
432
+ begin
433
+ if not public.module_is_enabled(p_company_id, 'assets') then
434
+ raise exception 'module_not_enabled: assets is not enabled on this company'
435
+ using errcode = '55006';
436
+ end if;
437
+
438
+ select country into v_country from public.companies where id = p_company_id;
439
+ if v_country is null then
440
+ raise exception 'unknown_company: %', p_company_id;
441
+ end if;
442
+
443
+ if p_category_code is not null then
444
+ select * into v_category
445
+ from assets.category_templates
446
+ where country = v_country and code = p_category_code;
447
+ if not found then
448
+ raise exception 'unknown_asset_category: % is not a category of the % pack', p_category_code, v_country;
449
+ end if;
450
+ end if;
451
+
452
+ -- The category is a suggestion, so anything the caller gave wins over it.
453
+ -- Same rule as a product pre-filling a document line.
454
+ v_method := coalesce(p_method, v_category.method, 'straight_line');
455
+ v_duration := coalesce(p_duration_months, v_category.duration_months);
456
+ v_coef := coalesce(p_coefficient, v_category.coefficient);
457
+
458
+ if v_duration is null then
459
+ raise exception 'no_duration: name a duration in months, or a category of the % pack that carries one', v_country;
460
+ end if;
461
+
462
+ v_asset := public.account_id_by_code(p_company_id, p_asset_account);
463
+ v_deprec := public.account_id_by_code(p_company_id, p_depreciation_account);
464
+ v_expense := public.account_id_by_code(p_company_id, p_expense_account);
465
+ if v_asset is null then
466
+ raise exception 'unknown_account: % is not an account of this company', p_asset_account;
467
+ end if;
468
+ if v_deprec is null then
469
+ raise exception 'unknown_account: % is not an account of this company', p_depreciation_account;
470
+ end if;
471
+ if v_expense is null then
472
+ raise exception 'unknown_account: % is not an account of this company', p_expense_account;
473
+ end if;
474
+
475
+ insert into assets.assets (
476
+ company_id, code, name, description, category_code, document_line_id, contact_id,
477
+ acquisition_date, in_service_date, cost, residual_value, method, duration_months,
478
+ coefficient, prorata, asset_account_id, depreciation_account_id, expense_account_id, state
479
+ )
480
+ values (
481
+ p_company_id, p_code, p_name, p_description, p_category_code, p_document_line_id, p_contact_id,
482
+ p_acquisition_date, p_in_service_date, round(p_cost, 2), round(coalesce(p_residual_value, 0), 2),
483
+ v_method, v_duration, v_coef, v_category.prorata, v_asset, v_deprec, v_expense, 'active'
484
+ )
485
+ returning id into v_id;
486
+
487
+ perform assets.generate_schedule(v_id);
488
+ return v_id;
489
+ end;
490
+ $$;
491
+
492
+ comment on function assets.create_asset(uuid, text, text, date, numeric, text, text, text, text, integer, assets.depreciation_method, numeric, numeric, date, uuid, uuid, text) is
493
+ 'Creates an asset and its schedule in one call. A category of the country pack fills in the method, the duration and the coefficient; anything the caller passes wins over it.';
494
+
495
+ -- ---------------------------------------------------------------------------
496
+ -- generate_schedule
497
+ --
498
+ -- The one place an annuity is worked out, for both methods. Everything is
499
+ -- rounded to the cent as it goes and the last line takes what is left, so the
500
+ -- schedule sums to exactly `cost - residual_value` and a test asserts it on
501
+ -- every golden asset of both packs.
502
+ -- ---------------------------------------------------------------------------
503
+
504
+ create or replace function assets.generate_schedule(p_asset_id uuid)
505
+ returns integer
506
+ language plpgsql
507
+ as $$
508
+ declare
509
+ v_asset assets.assets%rowtype;
510
+ v_rules assets.country_rules%rowtype;
511
+ v_prorata assets.prorata_rule;
512
+ v_start date;
513
+ v_year public.fiscal_years%rowtype;
514
+ v_base numeric(16, 2);
515
+ v_rate numeric;
516
+ v_period_start date;
517
+ v_period_end date;
518
+ v_fraction numeric;
519
+ v_amount numeric(16, 2);
520
+ v_linear numeric(16, 2);
521
+ v_cap numeric(16, 2);
522
+ v_accum numeric(16, 2) := 0;
523
+ v_sequence integer := 0;
524
+ v_periods integer;
525
+ v_left integer;
526
+ v_guard integer := 0;
527
+ begin
528
+ select * into v_asset from assets.assets where id = p_asset_id;
529
+ if not found then
530
+ raise exception 'unknown_asset: %', p_asset_id;
531
+ end if;
532
+ if v_asset.state = 'disposed' then
533
+ raise exception 'asset_disposed: % has left the books', v_asset.code;
534
+ end if;
535
+ if v_asset.method = 'units_of_production' then
536
+ raise exception 'units_of_production_unsupported: a schedule by output needs the units of each period, which this module does not record. Name a duration and a straight line, or keep the schedule outside Ekwo.';
537
+ end if;
538
+
539
+ -- A schedule is generated once. Once a line has been booked, the schedule is
540
+ -- what the ledger says happened, and regenerating it would leave the two
541
+ -- disagreeing without anything looking wrong — which is the failure the
542
+ -- column `posted_at` exists to make visible.
543
+ if exists (select 1 from assets.depreciation_lines l
544
+ where l.asset_id = p_asset_id and l.posted_at is not null) then
545
+ raise exception 'schedule_already_posted: % has depreciation already booked; a schedule is not rewritten under the ledger', v_asset.code
546
+ using errcode = '55006';
547
+ end if;
548
+ delete from assets.depreciation_lines where asset_id = p_asset_id;
549
+
550
+ v_start := coalesce(v_asset.in_service_date, v_asset.acquisition_date);
551
+ v_base := v_asset.cost - v_asset.residual_value;
552
+
553
+ v_rules := assets.rules(v_asset.company_id);
554
+ v_prorata := coalesce(
555
+ v_asset.prorata,
556
+ case when v_asset.method = 'declining_balance' then v_rules.prorata_declining
557
+ else v_rules.prorata_straight_line end);
558
+ if v_prorata is null then
559
+ raise exception 'no_assets_country_rules: the pack of this company''s country says nothing about how a first period is prorated. Add an assets section to the pack, or set assets.prorata on this asset.'
560
+ using errcode = '55006';
561
+ end if;
562
+
563
+ select * into v_year
564
+ from public.fiscal_years f
565
+ where f.company_id = v_asset.company_id
566
+ and v_start between f.start_date and f.end_date
567
+ limit 1;
568
+ if not found then
569
+ raise exception 'no_fiscal_year: % entered service on %, which falls in no financial year of this company', v_asset.code, v_start;
570
+ end if;
571
+
572
+ v_period_start := v_year.start_date;
573
+ v_rate := 12::numeric / v_asset.duration_months;
574
+ -- How many annuities the duration is worth. A declining balance measures
575
+ -- what is left to run in periods and not in days, because that is what the
576
+ -- rule it comes from says: at the start of the second year of a five-year
577
+ -- asset, four annuities remain, whatever day of the first year it was
578
+ -- bought on.
579
+ v_periods := ceil(v_asset.duration_months / 12.0)::integer;
580
+ if v_rules.declining_cap_percent is not null then
581
+ v_cap := round(v_asset.cost * v_rules.declining_cap_percent / 100, 2);
582
+ end if;
583
+
584
+ while v_accum < v_base loop
585
+ v_guard := v_guard + 1;
586
+ if v_guard > 1200 then
587
+ raise exception 'schedule_runaway: % produced more than 1200 periods; check its duration and its coefficient', v_asset.code;
588
+ end if;
589
+
590
+ v_period_end := assets.period_end_for(v_asset.company_id, v_period_start);
591
+ v_fraction := assets.prorata_fraction(v_prorata, v_rules.day_count, v_start, v_period_start, v_period_end);
592
+
593
+ if v_asset.method = 'declining_balance' then
594
+ v_amount := round((v_base - v_accum) * v_rate * v_asset.coefficient * v_fraction, 2);
595
+ if v_cap is not null then
596
+ v_amount := least(v_amount, round(v_cap * v_fraction, 2));
597
+ end if;
598
+ -- The straight line over what is left to run. A declining balance is a
599
+ -- front-loaded schedule that never reaches zero on its own, and the
600
+ -- switch is what makes it end on the last period rather than never.
601
+ -- The last period is `v_left = 1`, where the straight line is the whole
602
+ -- remaining value.
603
+ v_left := greatest(1, v_periods - v_sequence);
604
+ if coalesce(v_rules.declining_switch_to_linear, true) then
605
+ v_linear := round((v_base - v_accum) / v_left * v_fraction, 2);
606
+ v_amount := greatest(v_amount, v_linear);
607
+ end if;
608
+ else
609
+ v_amount := round(v_base * v_rate * v_fraction, 2);
610
+ end if;
611
+
612
+ if v_amount <= 0 then
613
+ v_amount := v_base - v_accum;
614
+ end if;
615
+ -- The last period takes the remainder, whatever the rounding did on the
616
+ -- way. This is the line that makes the schedule sum to the cent.
617
+ if v_accum + v_amount > v_base then
618
+ v_amount := v_base - v_accum;
619
+ end if;
620
+
621
+ v_accum := v_accum + v_amount;
622
+ v_sequence := v_sequence + 1;
623
+
624
+ insert into assets.depreciation_lines
625
+ (asset_id, company_id, sequence, period_start, period_end, amount, accumulated, net_book_value)
626
+ values (p_asset_id, v_asset.company_id, v_sequence, v_period_start, v_period_end,
627
+ v_amount, v_accum, v_asset.cost - v_accum);
628
+
629
+ v_period_start := v_period_end + 1;
630
+ end loop;
631
+
632
+ if assets.period_is_monthly(v_asset.company_id) then
633
+ perform assets.split_into_months(p_asset_id);
634
+ end if;
635
+
636
+ update assets.assets
637
+ set state = case when state = 'draft' then 'active' else state end
638
+ where id = p_asset_id;
639
+
640
+ return (select count(*)::integer from assets.depreciation_lines where asset_id = p_asset_id);
641
+ end;
642
+ $$;
643
+
644
+ comment on function assets.generate_schedule(uuid) is
645
+ 'Writes the depreciation schedule of an asset, period by period, rounded to the cent with the last line taking the remainder. Refuses to rewrite a schedule whose lines are already booked.';
646
+
647
+ /**
648
+ * A yearly schedule, split.
649
+ *
650
+ * The annuity is what a country's rule is written in, so it is computed first
651
+ * and cut afterwards — one calculation and not two. Each period becomes as
652
+ * many months as it holds, each month takes a twelfth rounded to the cent, and
653
+ * the last month of each period takes the remainder, exactly as the last
654
+ * period of a schedule takes the remainder of the whole.
655
+ */
656
+ create or replace function assets.split_into_months(p_asset_id uuid)
657
+ returns integer
658
+ language plpgsql
659
+ as $$
660
+ declare
661
+ v_line assets.depreciation_lines%rowtype;
662
+ v_asset assets.assets%rowtype;
663
+ v_months integer;
664
+ v_start date;
665
+ v_end date;
666
+ v_share numeric(16, 2);
667
+ v_done numeric(16, 2);
668
+ v_accum numeric(16, 2) := 0;
669
+ v_sequence integer := 0;
670
+ v_index integer;
671
+ v_new jsonb := '[]'::jsonb;
672
+ begin
673
+ select * into v_asset from assets.assets where id = p_asset_id;
674
+
675
+ for v_line in
676
+ select * from assets.depreciation_lines where asset_id = p_asset_id order by sequence
677
+ loop
678
+ v_months := assets.months_inclusive(v_line.period_start, v_line.period_end);
679
+ v_done := 0;
680
+ for v_index in 1 .. v_months loop
681
+ v_start := (date_trunc('month', v_line.period_start) + make_interval(months => v_index - 1))::date;
682
+ v_end := (date_trunc('month', v_start) + interval '1 month' - interval '1 day')::date;
683
+ if v_index = 1 then v_start := v_line.period_start; end if;
684
+ if v_index = v_months then v_end := v_line.period_end; end if;
685
+
686
+ v_share := case when v_index = v_months
687
+ then v_line.amount - v_done
688
+ else round(v_line.amount / v_months, 2) end;
689
+ v_done := v_done + v_share;
690
+ if v_share = 0 then continue; end if;
691
+
692
+ v_accum := v_accum + v_share;
693
+ v_sequence := v_sequence + 1;
694
+ v_new := v_new || jsonb_build_object(
695
+ 'sequence', v_sequence, 'period_start', v_start, 'period_end', v_end,
696
+ 'amount', v_share, 'accumulated', v_accum,
697
+ 'net_book_value', v_asset.cost - v_accum);
698
+ end loop;
699
+ end loop;
700
+
701
+ delete from assets.depreciation_lines where asset_id = p_asset_id;
702
+ insert into assets.depreciation_lines
703
+ (asset_id, company_id, sequence, period_start, period_end, amount, accumulated, net_book_value)
704
+ select p_asset_id, v_asset.company_id,
705
+ (row ->> 'sequence')::integer, (row ->> 'period_start')::date, (row ->> 'period_end')::date,
706
+ (row ->> 'amount')::numeric, (row ->> 'accumulated')::numeric, (row ->> 'net_book_value')::numeric
707
+ from jsonb_array_elements(v_new) as row;
708
+
709
+ return v_sequence;
710
+ end;
711
+ $$;
712
+
713
+ -- ---------------------------------------------------------------------------
714
+ -- run_depreciation
715
+ --
716
+ -- One entry per period, through `post_module_entry()`, tagged
717
+ -- `depreciation:<period end>`. The tag is unique per company and module, so
718
+ -- running the same period twice is refused by the database rather than by a
719
+ -- flag this function remembered to check — and the flag is checked too, which
720
+ -- is why the ordinary second run is a quiet no-op and not an error.
721
+ -- ---------------------------------------------------------------------------
722
+
723
+ create or replace function assets.run_depreciation(p_company_id uuid, p_period_end date)
724
+ returns jsonb
725
+ language plpgsql
726
+ as $$
727
+ declare
728
+ v_period date;
729
+ v_lines jsonb;
730
+ v_entry uuid;
731
+ v_total numeric(16, 2);
732
+ v_posted jsonb := '[]'::jsonb;
733
+ begin
734
+ if not public.module_is_enabled(p_company_id, 'assets') then
735
+ raise exception 'module_not_enabled: assets is not enabled on this company'
736
+ using errcode = '55006';
737
+ end if;
738
+
739
+ for v_period in
740
+ select distinct l.period_end
741
+ from assets.depreciation_lines l
742
+ join assets.assets a on a.id = l.asset_id
743
+ where l.company_id = p_company_id
744
+ and l.posted_at is null
745
+ and l.period_end <= p_period_end
746
+ and a.state <> 'disposed'
747
+ and l.amount > 0
748
+ order by l.period_end
749
+ loop
750
+ -- One debit and one credit per asset, named by the asset. An entry that
751
+ -- aggregates by account is shorter and tells whoever reads the ledger in
752
+ -- three years nothing about which asset moved.
753
+ select jsonb_agg(line order by line_order), sum(amount)
754
+ into v_lines, v_total
755
+ from (
756
+ select jsonb_build_object(
757
+ 'account_id', a.expense_account_id,
758
+ 'label', a.code || ' — ' || a.name,
759
+ 'debit', l.amount) as line,
760
+ (a.code || ':1') as line_order, l.amount
761
+ from assets.depreciation_lines l
762
+ join assets.assets a on a.id = l.asset_id
763
+ where l.company_id = p_company_id and l.period_end = v_period
764
+ and l.posted_at is null and a.state <> 'disposed' and l.amount > 0
765
+ union all
766
+ select jsonb_build_object(
767
+ 'account_id', a.depreciation_account_id,
768
+ 'label', a.code || ' — ' || a.name,
769
+ 'credit', l.amount),
770
+ (a.code || ':2'), 0
771
+ from assets.depreciation_lines l
772
+ join assets.assets a on a.id = l.asset_id
773
+ where l.company_id = p_company_id and l.period_end = v_period
774
+ and l.posted_at is null and a.state <> 'disposed' and l.amount > 0
775
+ ) parts;
776
+
777
+ if v_lines is null then
778
+ continue;
779
+ end if;
780
+
781
+ v_entry := public.post_module_entry(
782
+ p_company_id, 'assets', 'depreciation:' || v_period::text, v_period,
783
+ 'Depreciation ' || v_period::text, v_lines);
784
+
785
+ update assets.depreciation_lines l
786
+ set entry_id = v_entry, posted_at = now()
787
+ from assets.assets a
788
+ where a.id = l.asset_id
789
+ and l.company_id = p_company_id
790
+ and l.period_end = v_period
791
+ and l.posted_at is null
792
+ and a.state <> 'disposed'
793
+ and l.amount > 0;
794
+
795
+ v_posted := v_posted || jsonb_build_object(
796
+ 'period_end', v_period,
797
+ 'entry_id', v_entry,
798
+ 'amount', to_char(v_total, 'FM9999999999999990.00'));
799
+ end loop;
800
+
801
+ -- An asset whose whole schedule is booked says so, so a register does not
802
+ -- have to work it out from the lines every time it is read.
803
+ update assets.assets a
804
+ set state = 'fully_depreciated'
805
+ where a.company_id = p_company_id
806
+ and a.state = 'active'
807
+ and not exists (select 1 from assets.depreciation_lines l
808
+ where l.asset_id = a.id and l.posted_at is null);
809
+
810
+ return jsonb_build_object('company_id', p_company_id, 'entries', v_posted);
811
+ end;
812
+ $$;
813
+
814
+ comment on function assets.run_depreciation(uuid, date) is
815
+ 'Books every planned period that ends on or before a date, one entry per period, through post_module_entry(). Idempotent: a period already booked is skipped, and the unique tag on the entry refuses a second one anyway. A closed financial year refuses the posting, because post_entry() asserts the period is open.';
816
+
817
+ -- ---------------------------------------------------------------------------
818
+ -- dispose_asset
819
+ -- ---------------------------------------------------------------------------
820
+
821
+ create or replace function assets.dispose_asset(
822
+ p_asset_id uuid,
823
+ p_date date,
824
+ p_proceeds numeric default 0,
825
+ p_counterpart_account text default null,
826
+ p_contact_id uuid default null
827
+ )
828
+ returns uuid
829
+ language plpgsql
830
+ as $$
831
+ declare
832
+ v_asset assets.assets%rowtype;
833
+ v_rules assets.country_rules%rowtype;
834
+ v_defaults public.country_defaults%rowtype;
835
+ v_country char(2);
836
+ v_accum numeric(16, 2);
837
+ v_nbv numeric(16, 2);
838
+ v_result numeric(16, 2);
839
+ v_proceeds numeric(16, 2) := round(coalesce(p_proceeds, 0), 2);
840
+ v_counter uuid;
841
+ v_account uuid;
842
+ v_lines jsonb := '[]'::jsonb;
843
+ v_entry uuid;
844
+ begin
845
+ select * into v_asset from assets.assets where id = p_asset_id for update;
846
+ if not found then
847
+ raise exception 'unknown_asset: %', p_asset_id;
848
+ end if;
849
+ if v_asset.state = 'disposed' then
850
+ raise exception 'asset_disposed: % has already left the books', v_asset.code;
851
+ end if;
852
+ if p_date < coalesce(v_asset.in_service_date, v_asset.acquisition_date) then
853
+ raise exception 'disposal_before_service: % cannot leave the books before it entered them', v_asset.code;
854
+ end if;
855
+
856
+ -- Everything the schedule planned for a period that has already ended has to
857
+ -- be booked before the asset goes: a disposal reads the accumulated
858
+ -- depreciation off the ledger, and a period left unbooked would make it read
859
+ -- a figure the accounts do not carry.
860
+ if exists (select 1 from assets.depreciation_lines l
861
+ where l.asset_id = p_asset_id and l.posted_at is null and l.period_end < p_date) then
862
+ raise exception 'depreciation_pending: run assets.run_depreciation(company, %) before disposing of %',
863
+ p_date, v_asset.code
864
+ using errcode = '55006';
865
+ end if;
866
+
867
+ select country into v_country from public.companies where id = v_asset.company_id;
868
+ v_rules := assets.rules(v_asset.company_id);
869
+ if v_rules.disposal_style is null then
870
+ raise exception 'no_disposal_style: the pack of % says nothing about how an asset leaves the books. Set assets.disposal, and the roles it needs, in the pack.', v_country
871
+ using errcode = '55006';
872
+ end if;
873
+ select * into v_defaults from public.country_defaults where country = v_country;
874
+
875
+ select coalesce(sum(l.amount), 0) into v_accum
876
+ from assets.depreciation_lines l
877
+ where l.asset_id = p_asset_id and l.posted_at is not null;
878
+ v_nbv := v_asset.cost - v_accum;
879
+ v_result := v_proceeds - v_nbv;
880
+
881
+ -- What the schedule still planned never happened. Deleting only the unposted
882
+ -- lines keeps everything the ledger knows about.
883
+ delete from assets.depreciation_lines
884
+ where asset_id = p_asset_id and posted_at is null;
885
+
886
+ -- 1. The asset leaves, and its accumulated depreciation with it.
887
+ if v_accum > 0 then
888
+ v_lines := v_lines || jsonb_build_object(
889
+ 'account_id', v_asset.depreciation_account_id,
890
+ 'label', v_asset.code || ' — accumulated depreciation', 'debit', v_accum);
891
+ end if;
892
+ v_lines := v_lines || jsonb_build_object(
893
+ 'account_id', v_asset.asset_account_id,
894
+ 'label', v_asset.code || ' — ' || v_asset.name, 'credit', v_asset.cost);
895
+
896
+ -- 2. What the buyer owes, if anything.
897
+ if v_proceeds > 0 then
898
+ v_counter := case
899
+ when p_counterpart_account is not null
900
+ then public.account_id_by_code(v_asset.company_id, p_counterpart_account)
901
+ else (select receivable_account_id from public.companies where id = v_asset.company_id)
902
+ end;
903
+ if v_counter is null then
904
+ raise exception 'no_counterpart_account: name the account the proceeds of % land on', v_asset.code;
905
+ end if;
906
+ v_lines := v_lines || jsonb_build_object(
907
+ 'account_id', v_counter, 'label', v_asset.code || ' — proceeds',
908
+ 'debit', v_proceeds, 'contact_id', p_contact_id);
909
+ end if;
910
+
911
+ -- 3. The result, the way the country presents it.
912
+ if v_rules.disposal_style = 'gross' then
913
+ -- The net book value is a charge and the proceeds an income, both in full.
914
+ -- The two lines above already cleared the asset and its depreciation, so
915
+ -- what is left to write is the charge; the proceeds line of the income
916
+ -- account replaces nothing, it is the other half of the same presentation.
917
+ v_account := public.account_id_by_code(v_asset.company_id, v_defaults.asset_disposal_value_code);
918
+ if v_account is null then
919
+ raise exception 'no_asset_disposal_value_account: % files a gross disposal and its pack names no account for the value of what was sold', v_country;
920
+ end if;
921
+ if v_nbv > 0 then
922
+ v_lines := v_lines || jsonb_build_object(
923
+ 'account_id', v_account, 'label', v_asset.code || ' — net book value sold',
924
+ 'debit', v_nbv);
925
+ end if;
926
+ if v_proceeds > 0 then
927
+ v_account := public.account_id_by_code(v_asset.company_id, v_defaults.asset_disposal_proceeds_code);
928
+ if v_account is null then
929
+ raise exception 'no_asset_disposal_proceeds_account: % files a gross disposal and its pack names no account for the proceeds', v_country;
930
+ end if;
931
+ v_lines := v_lines || jsonb_build_object(
932
+ 'account_id', v_account, 'label', v_asset.code || ' — proceeds of disposal',
933
+ 'credit', v_proceeds);
934
+ end if;
935
+ -- The two proceeds lines are the two halves of one movement and not a
936
+ -- double count: the buyer is debited above, the income account is credited
937
+ -- here. What the gross style adds to the net-result one is the charge, and
938
+ -- an income statement that prints both is what France asks for.
939
+ elsif v_result <> 0 then
940
+ v_account := public.account_id_by_code(
941
+ v_asset.company_id,
942
+ case when v_result > 0 then v_defaults.asset_disposal_gain_code
943
+ else coalesce(v_defaults.asset_disposal_loss_code, v_defaults.asset_disposal_gain_code) end);
944
+ if v_account is null then
945
+ raise exception 'no_asset_disposal_account: the pack of % names no account for a % on the disposal of a fixed asset',
946
+ v_country, case when v_result > 0 then 'gain' else 'loss' end;
947
+ end if;
948
+ v_lines := v_lines || jsonb_build_object(
949
+ 'account_id', v_account,
950
+ 'label', v_asset.code || ' — ' || case when v_result > 0 then 'gain' else 'loss' end || ' on disposal',
951
+ 'debit', case when v_result < 0 then -v_result else 0 end,
952
+ 'credit', case when v_result > 0 then v_result else 0 end);
953
+ end if;
954
+
955
+ v_entry := public.post_module_entry(
956
+ v_asset.company_id, 'assets', 'disposal:' || v_asset.code, p_date,
957
+ 'Disposal of ' || v_asset.code || ' — ' || v_asset.name, v_lines);
958
+
959
+ insert into assets.disposals
960
+ (asset_id, company_id, disposal_date, proceeds, counterpart_account_id, contact_id,
961
+ cost, accumulated, net_book_value, result, entry_id)
962
+ values (p_asset_id, v_asset.company_id, p_date, v_proceeds, v_counter, p_contact_id,
963
+ v_asset.cost, v_accum, v_nbv, v_result, v_entry);
964
+
965
+ update assets.assets set state = 'disposed' where id = p_asset_id;
966
+ return v_entry;
967
+ end;
968
+ $$;
969
+
970
+ comment on function assets.dispose_asset(uuid, date, numeric, text, uuid) is
971
+ 'Takes an asset off the books on a date: clears its cost and its accumulated depreciation, books the proceeds, and presents the result the way the country''s pack says — one gain or loss line, or the value and the proceeds in full.';
972
+
973
+ -- ---------------------------------------------------------------------------
974
+ -- can_disable
975
+ --
976
+ -- The convention `disable_module()` looks for: null when there is nothing in
977
+ -- the way, a sentence when there is. A module that holds no posted data writes
978
+ -- none of this at all.
979
+ -- ---------------------------------------------------------------------------
980
+
981
+ create or replace function assets.can_disable(p_company_id uuid)
982
+ returns text
983
+ language sql
984
+ stable
985
+ as $$
986
+ select case
987
+ when exists (
988
+ select 1 from assets.depreciation_lines l
989
+ where l.company_id = p_company_id and l.posted_at is not null
990
+ ) then 'depreciation has been booked from it; the entries stay whatever happens to the module, but the register that explains them would be gone'
991
+ when exists (select 1 from assets.disposals d where d.company_id = p_company_id)
992
+ then 'an asset has been disposed of through it'
993
+ when exists (select 1 from assets.assets a where a.company_id = p_company_id)
994
+ then 'it still holds assets; delete them first if they were a mistake'
995
+ end;
996
+ $$;
997
+
998
+ comment on function assets.can_disable(uuid) is
999
+ 'Why this company cannot disable the assets module, or null when it can. The convention disable_module() reads.';
1000
+
1001
+ -- ---------------------------------------------------------------------------
1002
+ -- The two readings
1003
+ -- ---------------------------------------------------------------------------
1004
+
1005
+ create or replace function assets.register(p_company_id uuid, p_at date)
1006
+ returns table (
1007
+ code text,
1008
+ name text,
1009
+ category_code text,
1010
+ acquisition_date date,
1011
+ cost numeric,
1012
+ accumulated numeric,
1013
+ net_book_value numeric,
1014
+ state text
1015
+ )
1016
+ language sql
1017
+ stable
1018
+ as $$
1019
+ select a.code,
1020
+ a.name,
1021
+ a.category_code,
1022
+ a.acquisition_date,
1023
+ a.cost,
1024
+ coalesce(d.accumulated, 0)::numeric(16, 2),
1025
+ (a.cost - coalesce(d.accumulated, 0))::numeric(16, 2),
1026
+ a.state::text
1027
+ from assets.assets a
1028
+ left join lateral (
1029
+ select coalesce(sum(l.amount), 0) as accumulated
1030
+ from assets.depreciation_lines l
1031
+ where l.asset_id = a.id and l.posted_at is not null and l.period_end <= p_at
1032
+ ) d on true
1033
+ where a.company_id = p_company_id
1034
+ and a.acquisition_date <= p_at
1035
+ and not exists (
1036
+ select 1 from assets.disposals x
1037
+ where x.asset_id = a.id and x.disposal_date <= p_at
1038
+ )
1039
+ order by a.code;
1040
+ $$;
1041
+
1042
+ comment on function assets.register(uuid, date) is
1043
+ 'The table of fixed assets at a date: what each one cost, what has been written off it, and what is left. Reads what has been booked, so it ties to the ledger.';
1044
+
1045
+ create or replace function assets.movements(p_company_id uuid, p_from date, p_to date)
1046
+ returns table (
1047
+ code text,
1048
+ name text,
1049
+ opening_cost numeric,
1050
+ additions numeric,
1051
+ disposals_cost numeric,
1052
+ closing_cost numeric,
1053
+ opening_accumulated numeric,
1054
+ depreciation numeric,
1055
+ disposals_accumulated numeric,
1056
+ closing_accumulated numeric,
1057
+ net_book_value numeric
1058
+ )
1059
+ language sql
1060
+ stable
1061
+ as $$
1062
+ with movement as (
1063
+ select a.id, a.code, a.name, a.cost,
1064
+ a.acquisition_date < p_from as held_before,
1065
+ a.acquisition_date between p_from and p_to as added,
1066
+ x.disposal_date, x.accumulated as disposed_accumulated,
1067
+ coalesce((select sum(l.amount) from assets.depreciation_lines l
1068
+ where l.asset_id = a.id and l.posted_at is not null
1069
+ and l.period_end < p_from), 0) as accum_before,
1070
+ coalesce((select sum(l.amount) from assets.depreciation_lines l
1071
+ where l.asset_id = a.id and l.posted_at is not null
1072
+ and l.period_end between p_from and p_to), 0) as accum_period
1073
+ from assets.assets a
1074
+ left join assets.disposals x on x.asset_id = a.id
1075
+ where a.company_id = p_company_id
1076
+ and a.acquisition_date <= p_to
1077
+ )
1078
+ select m.code, m.name,
1079
+ (case when m.held_before then m.cost else 0 end)::numeric(16, 2),
1080
+ (case when m.added then m.cost else 0 end)::numeric(16, 2),
1081
+ (case when m.disposal_date between p_from and p_to then m.cost else 0 end)::numeric(16, 2),
1082
+ (case when m.disposal_date is not null and m.disposal_date <= p_to then 0 else m.cost end)::numeric(16, 2),
1083
+ m.accum_before::numeric(16, 2),
1084
+ m.accum_period::numeric(16, 2),
1085
+ (case when m.disposal_date between p_from and p_to
1086
+ then coalesce(m.disposed_accumulated, 0) else 0 end)::numeric(16, 2),
1087
+ (case when m.disposal_date is not null and m.disposal_date <= p_to
1088
+ then 0 else m.accum_before + m.accum_period end)::numeric(16, 2),
1089
+ (case when m.disposal_date is not null and m.disposal_date <= p_to
1090
+ then 0 else m.cost - m.accum_before - m.accum_period end)::numeric(16, 2)
1091
+ from movement m
1092
+ order by m.code;
1093
+ $$;
1094
+
1095
+ comment on function assets.movements(uuid, date, date) is
1096
+ 'What came in, what was written off and what went out between two dates, per asset — the movement table an annual account asks for beside the register.';
1097
+
1098
+ -- ---------------------------------------------------------------------------
1099
+ -- Row level security
1100
+ --
1101
+ -- Every table of a module goes through `module_enabled()`, which is the
1102
+ -- membership **and** the module being on for that company. A company that
1103
+ -- disables the module stops seeing its own rows, which is the point: enabled
1104
+ -- is what the tables mean.
1105
+ -- ---------------------------------------------------------------------------
1106
+
1107
+ alter table assets.country_rules enable row level security;
1108
+ alter table assets.category_templates enable row level security;
1109
+ alter table assets.assets enable row level security;
1110
+ alter table assets.depreciation_lines enable row level security;
1111
+ alter table assets.disposals enable row level security;
1112
+
1113
+ -- Reference data of the installation, like `country_defaults`: readable by
1114
+ -- anyone signed in, written by a seed.
1115
+ create policy assets_country_rules_select on assets.country_rules
1116
+ for select using (auth.uid() is not null);
1117
+ create policy assets_category_templates_select on assets.category_templates
1118
+ for select using (auth.uid() is not null);
1119
+
1120
+ create policy assets_select on assets.assets
1121
+ for select using (public.module_enabled(company_id, 'assets'));
1122
+ create policy assets_write on assets.assets
1123
+ for all using (public.module_enabled(company_id, 'assets') and public.can_write_company(company_id))
1124
+ with check (public.module_enabled(company_id, 'assets') and public.can_write_company(company_id));
1125
+
1126
+ create policy assets_lines_select on assets.depreciation_lines
1127
+ for select using (public.module_enabled(company_id, 'assets'));
1128
+ create policy assets_lines_write on assets.depreciation_lines
1129
+ for all using (public.module_enabled(company_id, 'assets') and public.can_write_company(company_id))
1130
+ with check (public.module_enabled(company_id, 'assets') and public.can_write_company(company_id));
1131
+
1132
+ create policy assets_disposals_select on assets.disposals
1133
+ for select using (public.module_enabled(company_id, 'assets'));
1134
+ create policy assets_disposals_write on assets.disposals
1135
+ for all using (public.module_enabled(company_id, 'assets') and public.can_write_company(company_id))
1136
+ with check (public.module_enabled(company_id, 'assets') and public.can_write_company(company_id));
1137
+
1138
+ -- ---------------------------------------------------------------------------
1139
+ -- Privileges
1140
+ --
1141
+ -- A module schema does its own grants: `public` gets them from Supabase's
1142
+ -- default privileges, and a schema created by a migration gets nothing. The
1143
+ -- shape is the socle's — `authenticated` reads and writes under row level
1144
+ -- security, `anon` may select and sees nothing, and the functions are closed
1145
+ -- to PUBLIC.
1146
+ --
1147
+ -- None of this puts the schema on the API. PostgREST serves what the project
1148
+ -- lists under its exposed schemas, which no migration can set; `ekwo module
1149
+ -- enable` prints the line to add.
1150
+ -- ---------------------------------------------------------------------------
1151
+
1152
+ grant usage on schema assets to anon, authenticated, service_role;
1153
+
1154
+ grant select on all tables in schema assets to anon;
1155
+ grant select, insert, update, delete on all tables in schema assets to authenticated, service_role;
1156
+ grant execute on all functions in schema assets to authenticated, service_role;
1157
+
1158
+ alter default privileges in schema assets grant select on tables to anon;
1159
+ alter default privileges in schema assets
1160
+ grant select, insert, update, delete on tables to authenticated, service_role;
1161
+ alter default privileges in schema assets revoke execute on functions from public, anon;
1162
+ alter default privileges in schema assets grant execute on functions to authenticated, service_role;
1163
+
1164
+ revoke execute on all functions in schema assets from public;
1165
+
1166
+ -- ---------------------------------------------------------------------------
1167
+ -- The registry row
1168
+ --
1169
+ -- A module is installed when its schema exists and `public.modules` says so.
1170
+ -- This is the row, and it is the last thing the first migration of a module
1171
+ -- does — so a half-applied migration leaves nothing claiming to be here.
1172
+ -- ---------------------------------------------------------------------------
1173
+
1174
+ insert into public.modules (code, name, description, schema_name, version, status, requires_socle_min)
1175
+ values (
1176
+ 'assets',
1177
+ 'Fixed assets',
1178
+ 'Fixed assets, their depreciation schedule and their disposal. Durations, declining coefficients and the prorata convention are country pack data.',
1179
+ 'assets',
1180
+ '1.0.0',
1181
+ 'available',
1182
+ '20260913075903'
1183
+ )
1184
+ on conflict (code) do update set
1185
+ name = excluded.name,
1186
+ description = excluded.description,
1187
+ schema_name = excluded.schema_name,
1188
+ version = excluded.version,
1189
+ status = excluded.status,
1190
+ requires_socle_min = excluded.requires_socle_min;