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,138 @@
|
|
|
1
|
+
-- Ekwo OS — a euro written into the schema is a country written into the core.
|
|
2
|
+
--
|
|
3
|
+
-- Seven columns were declared `char(3) not null default 'EUR'` and one
|
|
4
|
+
-- `char(2) not null default 'fr'`. They date from before "a country is data"
|
|
5
|
+
-- was a rule, and they are the same mistake the country literals were: a
|
|
6
|
+
-- Canadian installation that forgets to name a currency does not get an
|
|
7
|
+
-- error, it gets euros — in its documents, its payments, its catalogue, its
|
|
8
|
+
-- bank accounts and its statements — and finds out at the first report.
|
|
9
|
+
--
|
|
10
|
+
-- The defaults go. What replaces them is not another default but the answer
|
|
11
|
+
-- that was always the right one, taken from the row above:
|
|
12
|
+
--
|
|
13
|
+
-- * a company's currency and language come from **the pack of its fiscal
|
|
14
|
+
-- country** or from whoever creates it. `create_company()` already
|
|
15
|
+
-- refuses both by name when the pack is silent (`no_currency`,
|
|
16
|
+
-- `no_language`) and `ekwo init` asks; for every other writer a trigger
|
|
17
|
+
-- reads `country_defaults`, so a silent pack leaves the column null and
|
|
18
|
+
-- the NOT NULL refuses the insert. Silence is an error again, where it
|
|
19
|
+
-- used to be euros;
|
|
20
|
+
-- * a document, a payment, a catalogue item and a bank account take the
|
|
21
|
+
-- currency of **their company**, and a statement line takes the currency
|
|
22
|
+
-- of **its bank account**, which is a stronger answer than the company's
|
|
23
|
+
-- and falls back to it. One trigger, one place, no literal;
|
|
24
|
+
-- * `country_defaults.currency_code` is pack data and the compiled seed of
|
|
25
|
+
-- every country fills it. A default there would have meant a pack that
|
|
26
|
+
-- says nothing about its own currency silently claims the euro.
|
|
27
|
+
--
|
|
28
|
+
-- The trigger fills only what the caller left empty, so a foreign-currency
|
|
29
|
+
-- document still says so and nothing that used to work stops working. What
|
|
30
|
+
-- changes is what happens when *nobody* answers: it used to be euros, and it
|
|
31
|
+
-- is now the company, which is an answer the installation actually chose.
|
|
32
|
+
|
|
33
|
+
alter table companies alter column currency_code drop default;
|
|
34
|
+
alter table companies alter column language drop default;
|
|
35
|
+
alter table country_defaults alter column currency_code drop default;
|
|
36
|
+
alter table documents alter column currency_code drop default;
|
|
37
|
+
alter table payments alter column currency_code drop default;
|
|
38
|
+
alter table products alter column currency_code drop default;
|
|
39
|
+
alter table bank_accounts alter column currency_code drop default;
|
|
40
|
+
alter table bank_transactions alter column currency_code drop default;
|
|
41
|
+
|
|
42
|
+
-- ---------------------------------------------------------------------------
|
|
43
|
+
-- Where a currency comes from when the caller names none
|
|
44
|
+
--
|
|
45
|
+
-- A BEFORE INSERT trigger rather than a column default, because a default can
|
|
46
|
+
-- only be a constant and the answer is a lookup. It runs before the NOT NULL
|
|
47
|
+
-- is checked, which is what lets the column stay NOT NULL with no default at
|
|
48
|
+
-- all: leave it out and the company answers; leave the company out and the
|
|
49
|
+
-- insert is refused by the foreign key, as it already was.
|
|
50
|
+
-- ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
create or replace function currency_of_company()
|
|
53
|
+
returns trigger
|
|
54
|
+
language plpgsql
|
|
55
|
+
as $$
|
|
56
|
+
begin
|
|
57
|
+
if new.currency_code is null then
|
|
58
|
+
select c.currency_code into new.currency_code
|
|
59
|
+
from companies c where c.id = new.company_id;
|
|
60
|
+
end if;
|
|
61
|
+
return new;
|
|
62
|
+
end;
|
|
63
|
+
$$;
|
|
64
|
+
|
|
65
|
+
comment on function currency_of_company() is
|
|
66
|
+
'Fills currency_code from the company when the caller named none. The one place the question is answered for a table that belongs to a company.';
|
|
67
|
+
|
|
68
|
+
-- A company has no company above it: its answer is its country's pack. A
|
|
69
|
+
-- pack that names neither leaves the column null, and NOT NULL refuses the
|
|
70
|
+
-- row — which is the whole point of dropping the default.
|
|
71
|
+
create or replace function locale_of_country_pack()
|
|
72
|
+
returns trigger
|
|
73
|
+
language plpgsql
|
|
74
|
+
as $$
|
|
75
|
+
declare
|
|
76
|
+
v_defaults country_defaults%rowtype;
|
|
77
|
+
begin
|
|
78
|
+
if new.currency_code is null or new.language is null then
|
|
79
|
+
select * into v_defaults
|
|
80
|
+
from country_defaults
|
|
81
|
+
where country = coalesce(new.fiscal_country, new.country);
|
|
82
|
+
new.currency_code := coalesce(new.currency_code, v_defaults.currency_code);
|
|
83
|
+
new.language := coalesce(new.language, v_defaults.language_default);
|
|
84
|
+
end if;
|
|
85
|
+
return new;
|
|
86
|
+
end;
|
|
87
|
+
$$;
|
|
88
|
+
|
|
89
|
+
comment on function locale_of_country_pack() is
|
|
90
|
+
'Fills a company''s currency and language from the pack of its fiscal country when the caller named neither. A pack that says nothing leaves them null, and NOT NULL refuses the row.';
|
|
91
|
+
|
|
92
|
+
create trigger companies_locale_of_country_pack
|
|
93
|
+
before insert on companies
|
|
94
|
+
for each row execute function locale_of_country_pack();
|
|
95
|
+
|
|
96
|
+
create trigger documents_currency_of_company
|
|
97
|
+
before insert on documents
|
|
98
|
+
for each row execute function currency_of_company();
|
|
99
|
+
|
|
100
|
+
create trigger payments_currency_of_company
|
|
101
|
+
before insert on payments
|
|
102
|
+
for each row execute function currency_of_company();
|
|
103
|
+
|
|
104
|
+
create trigger products_currency_of_company
|
|
105
|
+
before insert on products
|
|
106
|
+
for each row execute function currency_of_company();
|
|
107
|
+
|
|
108
|
+
create trigger bank_accounts_currency_of_company
|
|
109
|
+
before insert on bank_accounts
|
|
110
|
+
for each row execute function currency_of_company();
|
|
111
|
+
|
|
112
|
+
-- A statement line is in the currency of the account it came off, which is
|
|
113
|
+
-- not always the company's. The account answers first, the company after.
|
|
114
|
+
create or replace function currency_of_bank_account()
|
|
115
|
+
returns trigger
|
|
116
|
+
language plpgsql
|
|
117
|
+
as $$
|
|
118
|
+
begin
|
|
119
|
+
if new.currency_code is null then
|
|
120
|
+
select b.currency_code into new.currency_code
|
|
121
|
+
from bank_accounts b where b.id = new.bank_account_id;
|
|
122
|
+
end if;
|
|
123
|
+
if new.currency_code is null then
|
|
124
|
+
select c.currency_code into new.currency_code
|
|
125
|
+
from companies c where c.id = new.company_id;
|
|
126
|
+
end if;
|
|
127
|
+
return new;
|
|
128
|
+
end;
|
|
129
|
+
$$;
|
|
130
|
+
|
|
131
|
+
comment on function currency_of_bank_account() is
|
|
132
|
+
'Fills a statement line''s currency_code from its bank account, and from the company as a last resort.';
|
|
133
|
+
|
|
134
|
+
create trigger bank_transactions_currency_of_bank_account
|
|
135
|
+
before insert on bank_transactions
|
|
136
|
+
for each row execute function currency_of_bank_account();
|
|
137
|
+
|
|
138
|
+
revoke execute on all functions in schema public from public;
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
-- Ekwo OS — a tax that waits for a box that does not exist waits for ever.
|
|
2
|
+
--
|
|
3
|
+
-- `settle_cash_basis_tax()` is what moves a cash-basis tax off the transition
|
|
4
|
+
-- account when the invoice is paid. It finds the lines still waiting with
|
|
5
|
+
--
|
|
6
|
+
-- where t.cash_basis and q.declaration_box is null and q.box_amount is not null
|
|
7
|
+
--
|
|
8
|
+
-- and `post_document()` writes `box_amount` only when the posting names a
|
|
9
|
+
-- `declaration_box`:
|
|
10
|
+
--
|
|
11
|
+
-- v_box_amount := case when p.declaration_box is null then null else … end
|
|
12
|
+
--
|
|
13
|
+
-- So a cash-basis tax whose posting names no box books its amount onto the
|
|
14
|
+
-- transition account, is never seen as waiting, never settles, and never
|
|
15
|
+
-- reaches a declaration. Nothing raises. The balance simply grows, and it is
|
|
16
|
+
-- discovered — if it is discovered — years later by somebody reconciling a
|
|
17
|
+
-- transition account against nothing. The `no_cash_basis_box` exception that
|
|
18
|
+
-- was written for exactly this case sits inside the loop that already
|
|
19
|
+
-- excluded the line, so it could not fire.
|
|
20
|
+
--
|
|
21
|
+
-- The rule belongs upstream of the ledger, twice over, and both are cheap:
|
|
22
|
+
--
|
|
23
|
+
-- * `ekwo pack check` refuses a cash-basis tax whose tax posting names no
|
|
24
|
+
-- box, in the same place it already refuses one that names no transition
|
|
25
|
+
-- account, no more than one tax posting, or a non-deductible share;
|
|
26
|
+
-- * `post_document()` raises `no_cash_basis_box` before it writes the
|
|
27
|
+
-- entry, next to `no_cash_basis_account` and `cash_basis_split_tax` —
|
|
28
|
+
-- because a pack can be written by hand and installed without the CLI.
|
|
29
|
+
--
|
|
30
|
+
-- The unreachable check in `settle_cash_basis_tax()` stays where it is. It is
|
|
31
|
+
-- the last line of defence for a posting whose box was removed after a
|
|
32
|
+
-- document was booked, and with the two above in front of it that is the only
|
|
33
|
+
-- way to reach it.
|
|
34
|
+
|
|
35
|
+
create or replace function post_document(p_document_id uuid)
|
|
36
|
+
returns entries
|
|
37
|
+
language plpgsql
|
|
38
|
+
as $$
|
|
39
|
+
declare
|
|
40
|
+
v_doc documents%rowtype;
|
|
41
|
+
v_entry entries%rowtype;
|
|
42
|
+
v_journal uuid;
|
|
43
|
+
v_date date;
|
|
44
|
+
v_is_sale boolean;
|
|
45
|
+
v_is_credit boolean;
|
|
46
|
+
v_kind tax_document_kind;
|
|
47
|
+
v_base_credit boolean;
|
|
48
|
+
v_seq integer := 0;
|
|
49
|
+
v_contact uuid;
|
|
50
|
+
v_maturity date;
|
|
51
|
+
v_terms smallint;
|
|
52
|
+
v_counterpart uuid;
|
|
53
|
+
v_diff numeric(16, 2);
|
|
54
|
+
v_diff_cur numeric(16, 2);
|
|
55
|
+
v_amount numeric(16, 2);
|
|
56
|
+
v_book numeric(16, 2);
|
|
57
|
+
v_box_amount numeric(16, 2);
|
|
58
|
+
v_share numeric(16, 2);
|
|
59
|
+
v_share_book numeric(16, 2);
|
|
60
|
+
v_share_box numeric(16, 2);
|
|
61
|
+
v_left numeric(16, 2);
|
|
62
|
+
v_left_box numeric(16, 2);
|
|
63
|
+
v_side_left numeric(16, 2);
|
|
64
|
+
v_side_left_neg numeric(16, 2);
|
|
65
|
+
v_side_credit boolean;
|
|
66
|
+
v_label text;
|
|
67
|
+
v_rate numeric(18, 8);
|
|
68
|
+
v_home char(3);
|
|
69
|
+
v_foreign boolean;
|
|
70
|
+
v_total_cur numeric(16, 2);
|
|
71
|
+
v_cash boolean;
|
|
72
|
+
v_transition uuid;
|
|
73
|
+
v_postings integer;
|
|
74
|
+
r record;
|
|
75
|
+
p record;
|
|
76
|
+
g record;
|
|
77
|
+
begin
|
|
78
|
+
select * into v_doc from documents where id = p_document_id for update;
|
|
79
|
+
if not found then
|
|
80
|
+
raise exception 'unknown_document: document % does not exist', p_document_id;
|
|
81
|
+
end if;
|
|
82
|
+
if v_doc.state = 'posted' then
|
|
83
|
+
raise exception 'document_already_posted: document % is already posted', p_document_id;
|
|
84
|
+
end if;
|
|
85
|
+
if v_doc.state = 'cancelled' then
|
|
86
|
+
raise exception 'document_cancelled: document % cannot be posted', p_document_id;
|
|
87
|
+
end if;
|
|
88
|
+
if v_doc.entry_id is not null then
|
|
89
|
+
raise exception 'document_already_booked: document % already points at entry %',
|
|
90
|
+
p_document_id, v_doc.entry_id;
|
|
91
|
+
end if;
|
|
92
|
+
|
|
93
|
+
if v_doc.doc_type in ('sale_quote', 'purchase_order') then
|
|
94
|
+
raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
|
|
95
|
+
end if;
|
|
96
|
+
|
|
97
|
+
v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
|
|
98
|
+
v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
|
|
99
|
+
v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
|
|
100
|
+
-- Sale invoice and purchase credit note credit the base; the other two debit it.
|
|
101
|
+
v_base_credit := (v_is_sale <> v_is_credit);
|
|
102
|
+
|
|
103
|
+
v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
|
|
104
|
+
|
|
105
|
+
if not exists (
|
|
106
|
+
select 1 from document_lines
|
|
107
|
+
where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
|
|
108
|
+
) then
|
|
109
|
+
raise exception 'document_empty: document % has no billable line', p_document_id;
|
|
110
|
+
end if;
|
|
111
|
+
|
|
112
|
+
-- A fixed-amount tax has no basis to spread over lines; refuse rather than
|
|
113
|
+
-- guess.
|
|
114
|
+
if exists (
|
|
115
|
+
select 1 from document_lines l join taxes t on t.id = l.tax_id
|
|
116
|
+
where l.document_id = p_document_id and t.amount_type <> 'percent'
|
|
117
|
+
) then
|
|
118
|
+
raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
|
|
119
|
+
end if;
|
|
120
|
+
|
|
121
|
+
-- Every tax used must be in force on the accounting date.
|
|
122
|
+
for r in
|
|
123
|
+
select distinct t.id, t.code, t.valid_from, t.valid_to
|
|
124
|
+
from document_lines l join taxes t on t.id = l.tax_id
|
|
125
|
+
where l.document_id = p_document_id
|
|
126
|
+
loop
|
|
127
|
+
if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
|
|
128
|
+
raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
|
|
129
|
+
end if;
|
|
130
|
+
end loop;
|
|
131
|
+
|
|
132
|
+
-- Totals are derived; make sure they reflect the lines as they stand now.
|
|
133
|
+
perform documents_refresh_totals(p_document_id);
|
|
134
|
+
select * into v_doc from documents where id = p_document_id;
|
|
135
|
+
|
|
136
|
+
select c.currency_code into v_home from companies c where c.id = v_doc.company_id;
|
|
137
|
+
v_rate := v_doc.exchange_rate;
|
|
138
|
+
v_foreign := v_doc.currency_code <> v_home;
|
|
139
|
+
|
|
140
|
+
v_journal := coalesce(
|
|
141
|
+
v_doc.journal_id,
|
|
142
|
+
case when v_is_sale
|
|
143
|
+
then (select sales_journal_id from companies where id = v_doc.company_id)
|
|
144
|
+
else (select purchase_journal_id from companies where id = v_doc.company_id)
|
|
145
|
+
end
|
|
146
|
+
);
|
|
147
|
+
if v_journal is null then
|
|
148
|
+
raise exception 'no_journal: set journal_id on the document or a default journal on the company';
|
|
149
|
+
end if;
|
|
150
|
+
|
|
151
|
+
perform assert_period_open(v_doc.company_id, v_date, true);
|
|
152
|
+
|
|
153
|
+
v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
|
|
154
|
+
|
|
155
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
|
|
156
|
+
description, state, document_id, currency_code)
|
|
157
|
+
values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
|
|
158
|
+
coalesce(v_doc.number, v_doc.supplier_reference),
|
|
159
|
+
v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
|
|
160
|
+
then ' / ' || v_doc.supplier_reference else '' end,
|
|
161
|
+
'draft', p_document_id, v_doc.currency_code)
|
|
162
|
+
returning * into v_entry;
|
|
163
|
+
|
|
164
|
+
-- ------------------------------------------------------------------ bases
|
|
165
|
+
for r in
|
|
166
|
+
select l.account_id,
|
|
167
|
+
l.tax_id,
|
|
168
|
+
sum(l.amount_untaxed) as base_amount,
|
|
169
|
+
min(l.sequence) as seq,
|
|
170
|
+
string_agg(distinct l.name, ', ') as label
|
|
171
|
+
from document_lines l
|
|
172
|
+
where l.document_id = p_document_id
|
|
173
|
+
and l.line_type = 'product'
|
|
174
|
+
group by l.account_id, l.tax_id
|
|
175
|
+
having sum(l.amount_untaxed) <> 0
|
|
176
|
+
order by 4
|
|
177
|
+
loop
|
|
178
|
+
select tp.declaration_box, tp.factor_percent, tp.box_factor_percent,
|
|
179
|
+
coalesce(t.cash_basis, false) as cash_basis
|
|
180
|
+
into p
|
|
181
|
+
from tax_postings tp
|
|
182
|
+
join taxes t on t.id = tp.tax_id
|
|
183
|
+
where tp.tax_id = r.tax_id
|
|
184
|
+
and tp.document_kind = v_kind
|
|
185
|
+
and tp.posting_type = 'base'
|
|
186
|
+
limit 1;
|
|
187
|
+
|
|
188
|
+
v_amount := round(r.base_amount * coalesce(p.factor_percent, 100) / 100, 2);
|
|
189
|
+
v_book := round(v_amount / v_rate, 2);
|
|
190
|
+
v_seq := v_seq + 10;
|
|
191
|
+
|
|
192
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
193
|
+
debit, credit, tax_id, tax_line,
|
|
194
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
195
|
+
values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
|
|
196
|
+
case when v_base_credit then 0 else v_book end,
|
|
197
|
+
case when v_base_credit then v_book else 0 end,
|
|
198
|
+
r.tax_id, false,
|
|
199
|
+
case when coalesce(p.cash_basis, false) then null else p.declaration_box end,
|
|
200
|
+
case when p.declaration_box is null then null
|
|
201
|
+
else round(r.base_amount * coalesce(p.box_factor_percent, 100) / 100 / v_rate, 2) end,
|
|
202
|
+
v_doc.currency_code,
|
|
203
|
+
case when v_foreign then v_amount end);
|
|
204
|
+
end loop;
|
|
205
|
+
|
|
206
|
+
-- ------------------------------------------------------------------ taxes
|
|
207
|
+
for r in
|
|
208
|
+
select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
|
|
209
|
+
from document_tax_summary s
|
|
210
|
+
where s.document_id = p_document_id
|
|
211
|
+
and s.tax_id is not null
|
|
212
|
+
and s.tax_amount <> 0
|
|
213
|
+
order by s.tax_code
|
|
214
|
+
loop
|
|
215
|
+
select t.cash_basis, t.cash_basis_transition_account_id
|
|
216
|
+
into v_cash, v_transition
|
|
217
|
+
from taxes t where t.id = r.tax_id;
|
|
218
|
+
|
|
219
|
+
if v_cash then
|
|
220
|
+
-- A tax that waits needs somewhere to wait. Refuse by name rather than
|
|
221
|
+
-- book it on the account it is due on, which would make it due.
|
|
222
|
+
if v_transition is null then
|
|
223
|
+
raise exception 'no_cash_basis_account: tax % falls due on collection and names no transition account',
|
|
224
|
+
r.tax_code;
|
|
225
|
+
end if;
|
|
226
|
+
-- One posting per side, or the transition lines of a document cannot be
|
|
227
|
+
-- told apart when the matching sends each of them on. A tax whose
|
|
228
|
+
-- postings net out has nothing waiting to collect anyway.
|
|
229
|
+
select count(*) into v_postings
|
|
230
|
+
from tax_postings tp
|
|
231
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
232
|
+
and tp.posting_type = 'tax';
|
|
233
|
+
if v_postings > 1 then
|
|
234
|
+
raise exception 'cash_basis_split_tax: tax % falls due on collection and has % tax postings; it takes one',
|
|
235
|
+
r.tax_code, v_postings;
|
|
236
|
+
end if;
|
|
237
|
+
if exists (select 1 from tax_postings tp
|
|
238
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
239
|
+
and tp.posting_type = 'tax_on_base') then
|
|
240
|
+
raise exception 'cash_basis_tax_on_base: tax % falls due on collection and carries a non-deductible share; a cost is not deferred',
|
|
241
|
+
r.tax_code;
|
|
242
|
+
end if;
|
|
243
|
+
-- And it needs a box to fall due *into*. `settle_cash_basis_tax()` only
|
|
244
|
+
-- ever looks at lines that carry a `box_amount`, and a posting with no
|
|
245
|
+
-- `declaration_box` produces none — so the amount would sit on the
|
|
246
|
+
-- transition account for ever, settled by nothing and reported by
|
|
247
|
+
-- nothing, with no error anywhere. Refuse it here, where the pack can
|
|
248
|
+
-- still be corrected, rather than discover it in a balance years later.
|
|
249
|
+
if not exists (select 1 from tax_postings tp
|
|
250
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
251
|
+
and tp.posting_type = 'tax'
|
|
252
|
+
and tp.declaration_box is not null) then
|
|
253
|
+
raise exception 'no_cash_basis_box: tax % falls due on collection and its posting names no declaration box; the amount would wait on the transition account and never settle',
|
|
254
|
+
r.tax_code;
|
|
255
|
+
end if;
|
|
256
|
+
end if;
|
|
257
|
+
|
|
258
|
+
-- The postings of one side share out the tax of the group; the last of
|
|
259
|
+
-- each side takes what is left. Until `tax_on_base` there was never more
|
|
260
|
+
-- than one posting per side, so this changes no existing tax by a cent —
|
|
261
|
+
-- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
|
|
262
|
+
-- where rounding each half on its own would book 0.64 against a document
|
|
263
|
+
-- that totals 0.63.
|
|
264
|
+
v_side_left := null;
|
|
265
|
+
v_side_left_neg := null;
|
|
266
|
+
|
|
267
|
+
for p in
|
|
268
|
+
select tp.posting_type, tp.factor_percent, tp.account_id,
|
|
269
|
+
tp.declaration_box, tp.box_factor_percent,
|
|
270
|
+
case when tp.factor_percent >= 0 then 1 else -1 end as side,
|
|
271
|
+
sum(abs(tp.factor_percent))
|
|
272
|
+
over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
|
|
273
|
+
as side_factor,
|
|
274
|
+
row_number() over (
|
|
275
|
+
partition by case when tp.factor_percent >= 0 then 1 else -1 end
|
|
276
|
+
order by tp.sequence, tp.id)
|
|
277
|
+
= count(*) over (
|
|
278
|
+
partition by case when tp.factor_percent >= 0 then 1 else -1 end)
|
|
279
|
+
as is_last_of_side
|
|
280
|
+
from tax_postings tp
|
|
281
|
+
where tp.tax_id = r.tax_id
|
|
282
|
+
and tp.document_kind = v_kind
|
|
283
|
+
and tp.posting_type in ('tax', 'tax_on_base')
|
|
284
|
+
order by tp.sequence, tp.id
|
|
285
|
+
loop
|
|
286
|
+
-- The tax of the group was rounded once, in the view. Every posting is
|
|
287
|
+
-- a share of that one figure, never of a re-derived one.
|
|
288
|
+
if p.side >= 0 then
|
|
289
|
+
if v_side_left is null then
|
|
290
|
+
v_side_left := round(r.tax_amount * p.side_factor / 100, 2);
|
|
291
|
+
end if;
|
|
292
|
+
if p.is_last_of_side then
|
|
293
|
+
v_amount := v_side_left;
|
|
294
|
+
else
|
|
295
|
+
v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
|
|
296
|
+
v_side_left := v_side_left - v_amount;
|
|
297
|
+
end if;
|
|
298
|
+
else
|
|
299
|
+
if v_side_left_neg is null then
|
|
300
|
+
v_side_left_neg := round(r.tax_amount * p.side_factor / 100, 2);
|
|
301
|
+
end if;
|
|
302
|
+
if p.is_last_of_side then
|
|
303
|
+
v_amount := v_side_left_neg;
|
|
304
|
+
else
|
|
305
|
+
v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
|
|
306
|
+
v_side_left_neg := v_side_left_neg - v_amount;
|
|
307
|
+
end if;
|
|
308
|
+
end if;
|
|
309
|
+
|
|
310
|
+
if v_amount = 0 then
|
|
311
|
+
continue;
|
|
312
|
+
end if;
|
|
313
|
+
-- A positive factor keeps the side of the base, a negative one flips it.
|
|
314
|
+
v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
|
|
315
|
+
-- The box keeps its own rounding: `box_factor_percent` was always
|
|
316
|
+
-- independent from `factor_percent`, because a declaration figure is
|
|
317
|
+
-- not a ledger figure and only the ledger has to balance.
|
|
318
|
+
v_box_amount := case when p.declaration_box is null then null
|
|
319
|
+
else round(r.tax_amount * p.box_factor_percent / 100 / v_rate, 2) end;
|
|
320
|
+
v_book := round(v_amount / v_rate, 2);
|
|
321
|
+
|
|
322
|
+
if p.posting_type = 'tax' then
|
|
323
|
+
v_seq := v_seq + 10;
|
|
324
|
+
|
|
325
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
326
|
+
debit, credit, tax_id, tax_line,
|
|
327
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
328
|
+
values (v_entry.id, v_doc.company_id,
|
|
329
|
+
case when v_cash then v_transition else p.account_id end,
|
|
330
|
+
v_seq, r.tax_name,
|
|
331
|
+
case when v_side_credit then 0 else v_book end,
|
|
332
|
+
case when v_side_credit then v_book else 0 end,
|
|
333
|
+
r.tax_id, true,
|
|
334
|
+
case when v_cash then null else p.declaration_box end,
|
|
335
|
+
v_box_amount, v_doc.currency_code,
|
|
336
|
+
case when v_foreign then v_amount end);
|
|
337
|
+
continue;
|
|
338
|
+
end if;
|
|
339
|
+
|
|
340
|
+
-- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
|
|
341
|
+
-- lines it taxes, split in proportion to their base. The last share
|
|
342
|
+
-- takes whatever is left, so the shares add up to the amount that was
|
|
343
|
+
-- rounded once on the group and the entry still balances to the cent.
|
|
344
|
+
v_left := v_amount;
|
|
345
|
+
v_left_box := v_box_amount;
|
|
346
|
+
|
|
347
|
+
for g in
|
|
348
|
+
select account_id,
|
|
349
|
+
base_amount,
|
|
350
|
+
seq,
|
|
351
|
+
sum(base_amount) over () as total_base,
|
|
352
|
+
row_number() over (order by seq) = count(*) over () as is_last
|
|
353
|
+
from (
|
|
354
|
+
select l.account_id,
|
|
355
|
+
sum(l.amount_untaxed) as base_amount,
|
|
356
|
+
min(l.sequence) as seq
|
|
357
|
+
from document_lines l
|
|
358
|
+
where l.document_id = p_document_id
|
|
359
|
+
and l.line_type = 'product'
|
|
360
|
+
and l.tax_id = r.tax_id
|
|
361
|
+
group by l.account_id
|
|
362
|
+
having sum(l.amount_untaxed) <> 0
|
|
363
|
+
) as groups
|
|
364
|
+
order by seq
|
|
365
|
+
loop
|
|
366
|
+
if g.is_last then
|
|
367
|
+
v_share := v_left;
|
|
368
|
+
v_share_box := v_left_box;
|
|
369
|
+
else
|
|
370
|
+
v_share := round(v_amount * g.base_amount / g.total_base, 2);
|
|
371
|
+
v_share_box := case when v_box_amount is null then null
|
|
372
|
+
else round(v_box_amount * g.base_amount / g.total_base, 2) end;
|
|
373
|
+
v_left := v_left - v_share;
|
|
374
|
+
v_left_box := v_left_box - v_share_box;
|
|
375
|
+
end if;
|
|
376
|
+
|
|
377
|
+
if v_share = 0 then
|
|
378
|
+
continue;
|
|
379
|
+
end if;
|
|
380
|
+
|
|
381
|
+
v_seq := v_seq + 10;
|
|
382
|
+
v_share_book := round(v_share / v_rate, 2);
|
|
383
|
+
|
|
384
|
+
-- `tax_line` stays false: the amount is on a base account and belongs
|
|
385
|
+
-- to the base side of the declaration, which is why the Belgian grids
|
|
386
|
+
-- 82 and 83 report it together with the base.
|
|
387
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
388
|
+
debit, credit, tax_id, tax_line,
|
|
389
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
390
|
+
values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
|
|
391
|
+
case when v_side_credit then 0 else v_share_book end,
|
|
392
|
+
case when v_side_credit then v_share_book else 0 end,
|
|
393
|
+
r.tax_id, false,
|
|
394
|
+
p.declaration_box, v_share_box, v_doc.currency_code,
|
|
395
|
+
case when v_foreign then v_share end);
|
|
396
|
+
end loop;
|
|
397
|
+
end loop;
|
|
398
|
+
end loop;
|
|
399
|
+
|
|
400
|
+
-- ------------------------------------------------------------ counterpart
|
|
401
|
+
select total_debit - total_credit into v_diff from entries where id = v_entry.id;
|
|
402
|
+
select coalesce(sum(case when l.debit > 0 then l.amount_currency else -l.amount_currency end), 0)
|
|
403
|
+
into v_diff_cur
|
|
404
|
+
from entry_lines l where l.entry_id = v_entry.id;
|
|
405
|
+
|
|
406
|
+
if v_diff = 0 then
|
|
407
|
+
raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
|
|
408
|
+
end if;
|
|
409
|
+
|
|
410
|
+
v_contact := commercial_entity(v_doc.contact_id);
|
|
411
|
+
v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
|
|
412
|
+
|
|
413
|
+
select payment_terms_days into v_terms from contacts where id = v_contact;
|
|
414
|
+
v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
|
|
415
|
+
|
|
416
|
+
v_amount := abs(v_diff);
|
|
417
|
+
-- The counterpart balances the entry in both currencies. The total it is
|
|
418
|
+
-- checked against is the document's own, which is the currency
|
|
419
|
+
-- `amount_total` is stated in.
|
|
420
|
+
v_total_cur := case when v_foreign then abs(v_diff_cur) else v_amount end;
|
|
421
|
+
v_seq := v_seq + 10;
|
|
422
|
+
|
|
423
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
424
|
+
debit, credit, contact_id, date_maturity, currency_code,
|
|
425
|
+
amount_currency)
|
|
426
|
+
values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
|
|
427
|
+
case when v_diff > 0 then 0 else v_amount end,
|
|
428
|
+
case when v_diff > 0 then v_amount else 0 end,
|
|
429
|
+
v_contact, v_maturity, v_doc.currency_code,
|
|
430
|
+
case when v_foreign then v_total_cur end);
|
|
431
|
+
|
|
432
|
+
-- The ledger is right by construction. If the header disagrees, the header
|
|
433
|
+
-- is what is wrong, and we say so instead of quietly patching a line.
|
|
434
|
+
if abs(v_total_cur - abs(v_doc.amount_total)) > 0.005 then
|
|
435
|
+
raise exception 'document_total_mismatch: document % totals % but its lines book %',
|
|
436
|
+
p_document_id, v_doc.amount_total, v_total_cur;
|
|
437
|
+
end if;
|
|
438
|
+
|
|
439
|
+
-- --------------------------------------------------------------- posting
|
|
440
|
+
v_entry := post_entry(v_entry.id);
|
|
441
|
+
|
|
442
|
+
update documents
|
|
443
|
+
set state = 'posted',
|
|
444
|
+
number = coalesce(number, v_entry.number),
|
|
445
|
+
entry_id = v_entry.id,
|
|
446
|
+
accounting_date = v_date
|
|
447
|
+
where id = p_document_id;
|
|
448
|
+
|
|
449
|
+
return v_entry;
|
|
450
|
+
end;
|
|
451
|
+
$$;
|
|
452
|
+
|
|
453
|
+
revoke execute on all functions in schema public from public;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
-- Ekwo OS — "not payable" is not the same as "receivable".
|
|
2
|
+
--
|
|
3
|
+
-- `aged_balance(company, at, group)` tested the group twice, both times as
|
|
4
|
+
-- `case when p_group = 'payable' then … else …`. So every value that is not
|
|
5
|
+
-- exactly `'payable'` was silently the receivable ageing: `'supplier'`,
|
|
6
|
+
-- `'creditors'`, `'Payable'`, a typo, a column name passed by mistake. The
|
|
7
|
+
-- caller gets a full, plausible, wrong report — the one failure mode a report
|
|
8
|
+
-- must never have, because nothing about it looks like an error.
|
|
9
|
+
--
|
|
10
|
+
-- Two named groups, and anything else is refused by name. The body is
|
|
11
|
+
-- unchanged; it moves to plpgsql only so that it can raise.
|
|
12
|
+
|
|
13
|
+
create or replace function aged_balance(
|
|
14
|
+
p_company_id uuid,
|
|
15
|
+
p_at date default current_date,
|
|
16
|
+
p_group text default 'receivable'
|
|
17
|
+
)
|
|
18
|
+
returns table (
|
|
19
|
+
contact_id uuid,
|
|
20
|
+
contact_name text,
|
|
21
|
+
account_id uuid,
|
|
22
|
+
account_code text,
|
|
23
|
+
not_due numeric,
|
|
24
|
+
days_1_30 numeric,
|
|
25
|
+
days_31_60 numeric,
|
|
26
|
+
days_61_90 numeric,
|
|
27
|
+
days_over_90 numeric,
|
|
28
|
+
total numeric
|
|
29
|
+
)
|
|
30
|
+
language plpgsql
|
|
31
|
+
stable
|
|
32
|
+
as $$
|
|
33
|
+
begin
|
|
34
|
+
if p_group is null or p_group not in ('receivable', 'payable') then
|
|
35
|
+
raise exception 'invalid_group: % is not an ageing group; it is receivable or payable', coalesce(p_group, 'null');
|
|
36
|
+
end if;
|
|
37
|
+
|
|
38
|
+
return query
|
|
39
|
+
with open_lines as (
|
|
40
|
+
select l.contact_id,
|
|
41
|
+
l.account_id,
|
|
42
|
+
case when p_group = 'payable' then -l.balance else l.balance end
|
|
43
|
+
* (case when abs(l.balance) = 0 then 0
|
|
44
|
+
else (abs(l.balance) - l.matched_amount) / abs(l.balance) end) as residual,
|
|
45
|
+
coalesce(l.date_maturity, e.entry_date) as due_date
|
|
46
|
+
from entry_lines l
|
|
47
|
+
join entries e on e.id = l.entry_id
|
|
48
|
+
join accounts a on a.id = l.account_id
|
|
49
|
+
where l.company_id = p_company_id
|
|
50
|
+
and e.state = 'posted'
|
|
51
|
+
and e.entry_date <= p_at
|
|
52
|
+
and a.reconcilable
|
|
53
|
+
and a.account_type = case when p_group = 'payable'
|
|
54
|
+
then 'liability_payable'::account_type
|
|
55
|
+
else 'asset_receivable'::account_type end
|
|
56
|
+
and abs(l.balance) - l.matched_amount > 0.005
|
|
57
|
+
)
|
|
58
|
+
select o.contact_id,
|
|
59
|
+
c.name,
|
|
60
|
+
o.account_id,
|
|
61
|
+
a.code,
|
|
62
|
+
round(sum(o.residual) filter (where o.due_date >= p_at), 2),
|
|
63
|
+
round(sum(o.residual) filter (where p_at - o.due_date between 1 and 30), 2),
|
|
64
|
+
round(sum(o.residual) filter (where p_at - o.due_date between 31 and 60), 2),
|
|
65
|
+
round(sum(o.residual) filter (where p_at - o.due_date between 61 and 90), 2),
|
|
66
|
+
round(sum(o.residual) filter (where p_at - o.due_date > 90), 2),
|
|
67
|
+
round(sum(o.residual), 2)
|
|
68
|
+
from open_lines o
|
|
69
|
+
left join contacts c on c.id = o.contact_id
|
|
70
|
+
join accounts a on a.id = o.account_id
|
|
71
|
+
group by o.contact_id, c.name, o.account_id, a.code
|
|
72
|
+
order by c.name nulls last;
|
|
73
|
+
end;
|
|
74
|
+
$$;
|
|
75
|
+
|
|
76
|
+
comment on function aged_balance(uuid, date, text) is
|
|
77
|
+
'Ageing of what is still open, read from the ledger and from the matching. Two groups, receivable and payable; anything else is refused by name rather than reported as receivable.';
|
|
78
|
+
|
|
79
|
+
revoke execute on all functions in schema public from public;
|