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.
- package/README.md +873 -0
- package/dist/args.d.ts +57 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +134 -0
- package/dist/args.js.map +1 -0
- package/dist/assets/expected-objects.json +9432 -0
- package/dist/assets/migrations/20260911120000_core_companies.sql +260 -0
- package/dist/assets/migrations/20260911120100_accounts_journals.sql +211 -0
- package/dist/assets/migrations/20260911120200_contacts.sql +90 -0
- package/dist/assets/migrations/20260911120300_taxes.sql +146 -0
- package/dist/assets/migrations/20260911120400_entries.sql +338 -0
- package/dist/assets/migrations/20260911120500_documents.sql +279 -0
- package/dist/assets/migrations/20260911120600_payments_reconciliation.sql +251 -0
- package/dist/assets/migrations/20260911120700_bank.sql +178 -0
- package/dist/assets/migrations/20260911120800_analytics_attachments.sql +128 -0
- package/dist/assets/migrations/20260911120900_post_document.sql +279 -0
- package/dist/assets/migrations/20260911121000_reporting.sql +235 -0
- package/dist/assets/migrations/20260911121100_country_templates.sql +226 -0
- package/dist/assets/migrations/20260911121200_fec.sql +71 -0
- package/dist/assets/migrations/20260911130000_instance.sql +77 -0
- package/dist/assets/migrations/20260911130100_instance_members.sql +224 -0
- package/dist/assets/migrations/20260911140000_instance_admins.sql +112 -0
- package/dist/assets/migrations/20260911160000_tax_posting_templates_unique.sql +17 -0
- package/dist/assets/migrations/20260911170000_document_amount_paid.sql +82 -0
- package/dist/assets/migrations/20260911173000_post_payment.sql +122 -0
- package/dist/assets/migrations/20260911173100_sequence_counters_under_rls.sql +84 -0
- package/dist/assets/migrations/20260911183000_country_journal_defaults.sql +143 -0
- package/dist/assets/migrations/20260911193853_line_account_defaults.sql +274 -0
- package/dist/assets/migrations/20260911195054_products.sql +221 -0
- package/dist/assets/migrations/20260911210131_anon_surface.sql +85 -0
- package/dist/assets/migrations/20260912074712_country_packs.sql +318 -0
- package/dist/assets/migrations/20260912080311_report_code_and_region.sql +198 -0
- package/dist/assets/migrations/20260912081014_pack_certification_maintained.sql +31 -0
- package/dist/assets/migrations/20260912081015_pack_certification_backfill.sql +16 -0
- package/dist/assets/migrations/20260912090407_tax_report_boxes.sql +329 -0
- package/dist/assets/migrations/20260912091917_tax_on_base_value.sql +25 -0
- package/dist/assets/migrations/20260912091918_tax_engine_columns.sql +776 -0
- package/dist/assets/migrations/20260912094412_opening_and_closing.sql +759 -0
- package/dist/assets/migrations/20260912095825_charts_of_accounts.sql +326 -0
- package/dist/assets/migrations/20260912100412_financial_statements.sql +632 -0
- package/dist/assets/migrations/20260912104719_one_formula_evaluator.sql +209 -0
- package/dist/assets/migrations/20260912105720_entry_kind_appropriation.sql +26 -0
- package/dist/assets/migrations/20260912105721_appropriation_entry_kind.sql +444 -0
- package/dist/assets/migrations/20260912111751_document_rules.sql +274 -0
- package/dist/assets/migrations/20260912112132_cash_basis_vat_and_fx.sql +1189 -0
- package/dist/assets/migrations/20260913074512_modules.sql +493 -0
- package/dist/assets/migrations/20260913075903_asset_disposal_roles.sql +47 -0
- package/dist/assets/migrations/20260913083216_capabilities.sql +637 -0
- package/dist/assets/migrations/20260913083901_company_invitations.sql +262 -0
- package/dist/assets/migrations/20260913084402_user_preferences.sql +376 -0
- package/dist/assets/migrations/20260913084847_company_profile.sql +261 -0
- package/dist/assets/migrations/20260913085436_numbering_reads_the_pack.sql +235 -0
- package/dist/assets/migrations/20260913085932_api_keys.sql +303 -0
- package/dist/assets/migrations/20260913090216_fiscal_year_bounds.sql +159 -0
- package/dist/assets/migrations/20260913092527_entries_import.sql +219 -0
- package/dist/assets/migrations/20260913101536_null_safe_guards.sql +188 -0
- package/dist/assets/migrations/20260913102115_installer_is_named.sql +462 -0
- package/dist/assets/migrations/20260913102758_no_currency_default.sql +138 -0
- package/dist/assets/migrations/20260913103355_cash_basis_needs_a_box.sql +453 -0
- package/dist/assets/migrations/20260913104014_aged_balance_named_group.sql +79 -0
- package/dist/assets/migrations/20260913104232_foreign_key_indexes.sql +144 -0
- package/dist/assets/migrations/20260913105120_declared_no_reader.sql +41 -0
- package/dist/assets/migrations/20260913111407_pack_languages.sql +223 -0
- package/dist/assets/migrations/20260913112233_schema_comments_for_a_reader.sql +27 -0
- package/dist/assets/migrations/20260913114535_fec_opening_balances.sql +307 -0
- package/dist/assets/migrations/20260914103412_audit_log.sql +509 -0
- package/dist/assets/migrations/20260914111907_pack_upgrade.sql +428 -0
- package/dist/assets/migrations/20260914120500_rounding_reads_the_currency.sql +206 -0
- package/dist/assets/migrations/20260914121200_amounts_round_at_the_currency.sql +2403 -0
- package/dist/assets/migrations/20260914134325_schema_version_0_2_0.sql +36 -0
- package/dist/assets/migrations/20260914143915_accounts_in_use.sql +428 -0
- package/dist/assets/migrations/20260914144731_account_code_frozen.sql +99 -0
- package/dist/assets/migrations/20260914151207_schema_grants_its_own_rights.sql +284 -0
- package/dist/assets/migrations/20260914152840_pack_upgrade_records_its_own_line.sql +237 -0
- package/dist/assets/migrations/20260914163943_declaration_periodicity.sql +349 -0
- package/dist/assets/migrations/20260915094000_foreign_services_received.sql +37 -0
- package/dist/assets/migrations/20260915094500_mentions_read_a_foreign_service.sql +72 -0
- package/dist/assets/migrations/20260915153000_document_shares.sql +568 -0
- package/dist/assets/migrations/20260915160000_ec_sales_list.sql +181 -0
- package/dist/assets/migrations/20260915161842_pack_sources.sql +56 -0
- package/dist/assets/migrations/20260915170500_schema_version_0_3_0.sql +45 -0
- package/dist/assets/migrations/20260915174500_a_vat_category_is_a_code.sql +177 -0
- package/dist/assets/migrations/20260915180000_document_rule_references.sql +85 -0
- package/dist/assets/migrations/20260915181000_territories.sql +342 -0
- package/dist/assets/migrations/20260915181500_ec_sales_list_reads_the_territories.sql +222 -0
- package/dist/assets/migrations/20260915182000_intracom_triangular.sql +53 -0
- package/dist/assets/migrations/20260915182500_a_triangular_supply_says_reverse_charge.sql +99 -0
- package/dist/assets/migrations/20260915191200_a_document_knows_its_language.sql +633 -0
- package/dist/assets/migrations/20260915195000_a_price_that_holds_its_tax.sql +649 -0
- package/dist/assets/migrations/20260915200000_a_posting_names_its_boxes.sql +1004 -0
- package/dist/assets/migrations/20260916094500_a_company_files_more_than_one_declaration.sql +795 -0
- package/dist/assets/migrations/20260916103000_a_ledger_line_names_its_posting.sql +902 -0
- package/dist/assets/migrations/20260916123000_a_vocabulary_for_a_tax_that_is_not_a_vat.sql +144 -0
- package/dist/assets/migrations/20260916124000_a_box_can_be_a_rate_of_a_box.sql +525 -0
- package/dist/assets/migrations/20260916125000_a_tax_follows_the_territory.sql +1318 -0
- package/dist/assets/migrations/20260916126000_a_tax_point_and_its_exception.sql +1211 -0
- package/dist/assets/migrations/20260917090000_a_counterparty_that_learns.sql +561 -0
- package/dist/assets/migrations/20260917120000_what_the_money_pays.sql +545 -0
- package/dist/assets/migrations/20260917150000_a_filing_is_a_row.sql +488 -0
- package/dist/assets/migrations/20260917170000_when_it_is_due.sql +194 -0
- package/dist/assets/migrations/20260917180000_a_box_is_a_number_and_a_kind.sql +213 -0
- package/dist/assets/migrations/20260917190000_what_a_declaration_owes.sql +486 -0
- package/dist/assets/migrations/20260917200000_what_moved_after_it_went.sql +227 -0
- package/dist/assets/migrations/20260918090000_a_form_names_its_file.sql +27 -0
- package/dist/assets/migrations/20260918110000_a_deposit_and_what_came_back.sql +286 -0
- package/dist/assets/migrations/20260918113741_a_fourth_preset_is_named.sql +9 -0
- package/dist/assets/migrations/20260918113807_a_client_reads_and_hands_over.sql +96 -0
- package/dist/assets/migrations/20260918114322_two_definer_functions_check_their_caller.sql +75 -0
- package/dist/assets/migrations/20260918130000_every_company_somebody_keeps.sql +177 -0
- package/dist/assets/migrations/20260918140000_a_session_nobody_prepared.sql +40 -0
- package/dist/assets/migrations/20260918141107_a_line_keeps_the_tax_it_was_posted_with.sql +256 -0
- package/dist/assets/migrations/20260918141342_a_company_has_an_electronic_address.sql +66 -0
- package/dist/assets/migrations/20260918141605_an_invoice_reads_whole_from_the_views.sql +313 -0
- package/dist/assets/migrations/20260918141627_a_policy_asks_once.sql +132 -0
- package/dist/assets/migrations/20260918143352_a_suggestion_reads_the_words_once.sql +204 -0
- package/dist/assets/migrations/20260918143417_a_reference_of_the_caller_and_a_rehearsal.sql +115 -0
- package/dist/assets/migrations/20260918150712_a_company_leaves_with_its_books.sql +1119 -0
- package/dist/assets/migrations/20260918150931_a_statement_is_imported_once.sql +654 -0
- package/dist/assets/migrations/20260918161204_a_posted_document_does_not_move.sql +437 -0
- package/dist/assets/migrations/20260918161538_a_posted_entry_does_not_move.sql +252 -0
- package/dist/assets/migrations/20260918171946_an_entry_is_posted_by_post_entry.sql +289 -0
- package/dist/assets/migrations/20260918174312_schema_version_0_4_0.sql +53 -0
- package/dist/assets/modules/assets/module.json +13 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913081447_assets.sql +1190 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104234_foreign_key_indexes.sql +47 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104500_assets_capabilities.sql +145 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914122100_assets_round_at_the_currency.sql +564 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914145012_assets_accounts_in_use.sql +37 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914151530_assets_grants_its_own_rights.sql +63 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918113912_assets_client_reads.sql +20 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918150801_assets_leave_with_the_company.sql +41 -0
- package/dist/assets/modules/budgets/module.json +12 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913083012_budgets.sql +210 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104233_foreign_key_indexes.sql +37 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104501_budgets_capabilities.sql +72 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914122400_budgets_round_at_the_currency.sql +68 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914145048_budgets_accounts_in_use.sql +30 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914151812_budgets_grants_its_own_rights.sql +31 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918113954_budgets_client_reads.sql +20 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918150823_budgets_leave_with_the_company.sql +37 -0
- package/dist/assets/modules/schema/module.1.json +69 -0
- package/dist/assets/seed/00_currencies.sql +15 -0
- package/dist/assets/seed/00_territories.sql +280 -0
- package/dist/assets/seed/05_framework_generic.sql +110 -0
- package/dist/assets/seed/10_pack_be.sql +1284 -0
- package/dist/assets/seed/11_pack_fr.sql +1112 -0
- package/dist/assets/seed/12_pack_lu.sql +2484 -0
- package/dist/assets/seed/13_pack_ee.sql +733 -0
- package/dist/assets/seed/14_pack_gb.sql +796 -0
- package/dist/assets/seed/15_pack_us.sql +751 -0
- package/dist/assets/seed/90_demo_company.sql +368 -0
- package/dist/assets/seed/modules/assets/10_pack_be.sql +58 -0
- package/dist/assets/seed/modules/assets/11_pack_fr.sql +57 -0
- package/dist/assets/seed/modules/assets/14_pack_gb.sql +58 -0
- package/dist/assets/seed/modules/assets/15_pack_us.sql +60 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +134 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +8 -0
- package/dist/bin.js.map +1 -0
- package/dist/books.d.ts +60 -0
- package/dist/books.d.ts.map +1 -0
- package/dist/books.js +168 -0
- package/dist/books.js.map +1 -0
- package/dist/bootstrap.d.ts +236 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +459 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bundle.d.ts +27 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +47 -0
- package/dist/bundle.js.map +1 -0
- package/dist/checklist.d.ts +35 -0
- package/dist/checklist.d.ts.map +1 -0
- package/dist/checklist.js +64 -0
- package/dist/checklist.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +318 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/company.d.ts +59 -0
- package/dist/commands/company.d.ts.map +1 -0
- package/dist/commands/company.js +287 -0
- package/dist/commands/company.js.map +1 -0
- package/dist/commands/contact.d.ts +9 -0
- package/dist/commands/contact.d.ts.map +1 -0
- package/dist/commands/contact.js +82 -0
- package/dist/commands/contact.js.map +1 -0
- package/dist/commands/demo.d.ts +19 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +72 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +68 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/document.d.ts +17 -0
- package/dist/commands/document.d.ts.map +1 -0
- package/dist/commands/document.js +217 -0
- package/dist/commands/document.js.map +1 -0
- package/dist/commands/init.d.ts +31 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +535 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +24 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +128 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +115 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/module.d.ts +32 -0
- package/dist/commands/module.d.ts.map +1 -0
- package/dist/commands/module.js +285 -0
- package/dist/commands/module.js.map +1 -0
- package/dist/commands/pack.d.ts +22 -0
- package/dist/commands/pack.d.ts.map +1 -0
- package/dist/commands/pack.js +481 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/payment.d.ts +15 -0
- package/dist/commands/payment.d.ts.map +1 -0
- package/dist/commands/payment.js +97 -0
- package/dist/commands/payment.js.map +1 -0
- package/dist/commands/register.d.ts +20 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +134 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +134 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/whoami.d.ts +21 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +133 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/company.d.ts +16 -0
- package/dist/company.d.ts.map +1 -0
- package/dist/company.js +26 -0
- package/dist/company.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/connection.d.ts +79 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +116 -0
- package/dist/connection.js.map +1 -0
- package/dist/context.d.ts +47 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +93 -0
- package/dist/context.js.map +1 -0
- package/dist/doctor.d.ts +45 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +448 -0
- package/dist/doctor.js.map +1 -0
- package/dist/grants.d.ts +88 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +174 -0
- package/dist/grants.js.map +1 -0
- package/dist/identity.d.ts +54 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +132 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +155 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +303 -0
- package/dist/inventory.js.map +1 -0
- package/dist/migrations.d.ts +76 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +226 -0
- package/dist/migrations.js.map +1 -0
- package/dist/module/read.d.ts +91 -0
- package/dist/module/read.d.ts.map +1 -0
- package/dist/module/read.js +172 -0
- package/dist/module/read.js.map +1 -0
- package/dist/output.d.ts +110 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +173 -0
- package/dist/output.js.map +1 -0
- package/dist/pack/certification.d.ts +27 -0
- package/dist/pack/certification.d.ts.map +1 -0
- package/dist/pack/certification.js +38 -0
- package/dist/pack/certification.js.map +1 -0
- package/dist/pack/compile.d.ts +75 -0
- package/dist/pack/compile.d.ts.map +1 -0
- package/dist/pack/compile.js +801 -0
- package/dist/pack/compile.js.map +1 -0
- package/dist/pack/filing.d.ts +48 -0
- package/dist/pack/filing.d.ts.map +1 -0
- package/dist/pack/filing.js +56 -0
- package/dist/pack/filing.js.map +1 -0
- package/dist/pack/read.d.ts +766 -0
- package/dist/pack/read.d.ts.map +1 -0
- package/dist/pack/read.js +2336 -0
- package/dist/pack/read.js.map +1 -0
- package/dist/pack/schema.d.ts +26 -0
- package/dist/pack/schema.d.ts.map +1 -0
- package/dist/pack/schema.js +181 -0
- package/dist/pack/schema.js.map +1 -0
- package/dist/pack/territories.d.ts +98 -0
- package/dist/pack/territories.d.ts.map +1 -0
- package/dist/pack/territories.js +0 -0
- package/dist/pack/territories.js.map +1 -0
- package/dist/pack/upgrade.d.ts +86 -0
- package/dist/pack/upgrade.d.ts.map +1 -0
- package/dist/pack/upgrade.js +76 -0
- package/dist/pack/upgrade.js.map +1 -0
- package/dist/pack/vat-codes.d.ts +273 -0
- package/dist/pack/vat-codes.d.ts.map +1 -0
- package/dist/pack/vat-codes.js +513 -0
- package/dist/pack/vat-codes.js.map +1 -0
- package/dist/profiles.d.ts +67 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +187 -0
- package/dist/profiles.js.map +1 -0
- package/dist/prompt.d.ts +40 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +120 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +84 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +104 -0
- package/dist/registry.js.map +1 -0
- package/dist/rest.d.ts +81 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +245 -0
- package/dist/rest.js.map +1 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +15 -0
- package/dist/schema.js.map +1 -0
- package/dist/seeds.d.ts +27 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +47 -0
- package/dist/seeds.js.map +1 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +92 -0
- package/dist/session.js.map +1 -0
- package/dist/sql.d.ts +48 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +112 -0
- package/dist/sql.js.map +1 -0
- package/dist/status.d.ts +94 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +120 -0
- package/dist/status.js.map +1 -0
- package/dist/ui.d.ts +55 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +114 -0
- package/dist/ui.js.map +1 -0
- package/package.json +29 -0
- 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;
|