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,545 @@
|
|
|
1
|
+
-- Ekwo OS — what the money pays.
|
|
2
|
+
--
|
|
3
|
+
-- The other half of recognising a statement line. `confirm_contact()` says who
|
|
4
|
+
-- the money came from; this says what it settles, which is a different
|
|
5
|
+
-- question and a stricter one: getting the counterparty wrong misfiles a line,
|
|
6
|
+
-- getting the settlement wrong marks an invoice paid that nobody paid.
|
|
7
|
+
--
|
|
8
|
+
-- Three rules hold the whole file together.
|
|
9
|
+
--
|
|
10
|
+
-- **A statement line does not become an entry.** It becomes a *payment* —
|
|
11
|
+
-- the object the ledger already knows, with its journal, its direction, its
|
|
12
|
+
-- counterpart account — and `post_payment()` books it. There is no second way
|
|
13
|
+
-- of writing a ledger here, and nothing in this file inserts an `entries` or
|
|
14
|
+
-- an `entry_lines` row.
|
|
15
|
+
--
|
|
16
|
+
-- **Only identification is applied; resemblance is proposed.** A reference
|
|
17
|
+
-- that matches, or an exact amount with exactly one candidate, is an
|
|
18
|
+
-- identification. A sum of invoices that happens to reach the right total is a
|
|
19
|
+
-- resemblance: it is offered and never applied on its own. The production this
|
|
20
|
+
-- comes from does apply combinations, at two cents of tolerance, and it is
|
|
21
|
+
-- right often enough to be tempting — but a combination that lands on the
|
|
22
|
+
-- right total from the wrong invoices leaves no trace that anything went
|
|
23
|
+
-- wrong, and that is the one mistake a ledger cannot show you afterwards.
|
|
24
|
+
--
|
|
25
|
+
-- **Partial is never automatic.** A payment smaller than the invoice may be a
|
|
26
|
+
-- deposit, a discount, a short payment or an error — four different treatments
|
|
27
|
+
-- and the ledger cannot tell them apart. `settle_from_statement()` will settle
|
|
28
|
+
-- partially when a person names the lines; `auto_settle()` never will.
|
|
29
|
+
|
|
30
|
+
-- ---------------------------------------------------------------------------
|
|
31
|
+
-- open_items
|
|
32
|
+
--
|
|
33
|
+
-- What is still owed, line by line, in the shape matching needs: the side, the
|
|
34
|
+
-- amount still open, the document behind it and the references it carries.
|
|
35
|
+
-- `aged_balance()` answers a neighbouring question for a human to read; this
|
|
36
|
+
-- one is keyed for a machine to search.
|
|
37
|
+
-- ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
create or replace function open_items(
|
|
40
|
+
p_company_id uuid,
|
|
41
|
+
p_contact_id uuid default null,
|
|
42
|
+
p_as_of date default null
|
|
43
|
+
)
|
|
44
|
+
returns table (
|
|
45
|
+
line_id uuid,
|
|
46
|
+
contact_id uuid,
|
|
47
|
+
account_id uuid,
|
|
48
|
+
side text,
|
|
49
|
+
amount_open numeric,
|
|
50
|
+
entry_date date,
|
|
51
|
+
date_maturity date,
|
|
52
|
+
document_id uuid,
|
|
53
|
+
document_number text,
|
|
54
|
+
reference text
|
|
55
|
+
)
|
|
56
|
+
language sql
|
|
57
|
+
stable
|
|
58
|
+
security invoker
|
|
59
|
+
as $$
|
|
60
|
+
select l.id,
|
|
61
|
+
l.contact_id,
|
|
62
|
+
l.account_id,
|
|
63
|
+
case when l.debit > 0 then 'debit' else 'credit' end,
|
|
64
|
+
abs(l.balance) - l.matched_amount,
|
|
65
|
+
e.entry_date,
|
|
66
|
+
l.date_maturity,
|
|
67
|
+
d.id,
|
|
68
|
+
d.number,
|
|
69
|
+
coalesce(d.payment_reference, d.number)
|
|
70
|
+
from entry_lines l
|
|
71
|
+
join entries e on e.id = l.entry_id
|
|
72
|
+
join accounts a on a.id = l.account_id
|
|
73
|
+
left join documents d on d.entry_id = e.id
|
|
74
|
+
where l.company_id = p_company_id
|
|
75
|
+
and e.state = 'posted'
|
|
76
|
+
and a.reconcilable
|
|
77
|
+
and abs(l.balance) - l.matched_amount > 0
|
|
78
|
+
and (p_contact_id is null or l.contact_id = p_contact_id)
|
|
79
|
+
and (p_as_of is null or e.entry_date <= p_as_of);
|
|
80
|
+
$$;
|
|
81
|
+
|
|
82
|
+
comment on function open_items(uuid, uuid, date) is
|
|
83
|
+
'Third-party lines with something still open, with the document behind each and the references it carries. Read by the matching, and by anybody asking what is still owed.';
|
|
84
|
+
|
|
85
|
+
revoke execute on function open_items(uuid, uuid, date) from public, anon;
|
|
86
|
+
grant execute on function open_items(uuid, uuid, date) to authenticated, service_role;
|
|
87
|
+
|
|
88
|
+
-- ---------------------------------------------------------------------------
|
|
89
|
+
-- suggest_matches
|
|
90
|
+
--
|
|
91
|
+
-- Read-only, and — like `suggest_contacts()` — it decides nothing. Every
|
|
92
|
+
-- candidate says what kind of evidence it rests on and how many other
|
|
93
|
+
-- candidates the same evidence produced.
|
|
94
|
+
-- ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
create or replace function suggest_matches(p_transaction_id uuid)
|
|
97
|
+
returns table (
|
|
98
|
+
kind text,
|
|
99
|
+
line_ids uuid[],
|
|
100
|
+
amount numeric,
|
|
101
|
+
method text,
|
|
102
|
+
score numeric,
|
|
103
|
+
because text,
|
|
104
|
+
alternatives integer
|
|
105
|
+
)
|
|
106
|
+
language plpgsql
|
|
107
|
+
stable
|
|
108
|
+
security invoker
|
|
109
|
+
as $$
|
|
110
|
+
declare
|
|
111
|
+
v_tx bank_transactions;
|
|
112
|
+
v_policy matching_policy;
|
|
113
|
+
v_round money_rounding;
|
|
114
|
+
v_tolerance numeric;
|
|
115
|
+
v_side text;
|
|
116
|
+
v_target numeric;
|
|
117
|
+
v_home char(3);
|
|
118
|
+
v_internal boolean;
|
|
119
|
+
begin
|
|
120
|
+
select * into v_tx from bank_transactions t where t.id = p_transaction_id;
|
|
121
|
+
if not found then
|
|
122
|
+
raise exception 'not_found: bank transaction %', p_transaction_id
|
|
123
|
+
using errcode = 'no_data_found';
|
|
124
|
+
end if;
|
|
125
|
+
|
|
126
|
+
v_policy := matching_policy_of(v_tx.company_id);
|
|
127
|
+
select c.currency_code into v_home from companies c where c.id = v_tx.company_id;
|
|
128
|
+
|
|
129
|
+
-- One smallest unit of the currency, whatever that currency calls a unit.
|
|
130
|
+
-- A yen has none, a dinar has three, and `round_amount` has known that since
|
|
131
|
+
-- the rounding read the currency.
|
|
132
|
+
v_round := rounding_of(v_tx.company_id, v_tx.currency_code);
|
|
133
|
+
v_tolerance := v_policy.amount_tolerance_units * power(10::numeric, -v_round.decimals);
|
|
134
|
+
|
|
135
|
+
-- Money in settles what a customer owes, which is a debit still open. Money
|
|
136
|
+
-- out settles what we owe, which is a credit.
|
|
137
|
+
v_side := case when v_tx.amount > 0 then 'debit' else 'credit' end;
|
|
138
|
+
v_target := abs(v_tx.amount);
|
|
139
|
+
|
|
140
|
+
-- A transfer between two accounts of this company is not a settlement of
|
|
141
|
+
-- anything, and saying so is the point: the production this comes from
|
|
142
|
+
-- learnt that an internal wire left alone gets matched to whatever invoice
|
|
143
|
+
-- happens to carry the same amount.
|
|
144
|
+
select exists (
|
|
145
|
+
select 1 from bank_accounts b
|
|
146
|
+
where b.company_id = v_tx.company_id
|
|
147
|
+
and b.iban is not null
|
|
148
|
+
and v_tx.counterpart_iban is not null
|
|
149
|
+
and upper(replace(b.iban, ' ', '')) = upper(replace(v_tx.counterpart_iban, ' ', ''))
|
|
150
|
+
) into v_internal;
|
|
151
|
+
|
|
152
|
+
if v_internal then
|
|
153
|
+
return query
|
|
154
|
+
select 'internal_transfer'::text, '{}'::uuid[], v_target, 'own_account'::text, 1::numeric,
|
|
155
|
+
'the counterparty account is an account of this company'::text, 1;
|
|
156
|
+
return;
|
|
157
|
+
end if;
|
|
158
|
+
|
|
159
|
+
-- Matching across currencies is not done here. The ledger amount and the
|
|
160
|
+
-- statement amount are then two numbers in two currencies, and the rate that
|
|
161
|
+
-- reconciles them is a question `reconcile()` answers at the moment of
|
|
162
|
+
-- matching, not one a search may assume. Recorded in docs/international.md.
|
|
163
|
+
if v_tx.currency_code <> v_home then
|
|
164
|
+
return;
|
|
165
|
+
end if;
|
|
166
|
+
|
|
167
|
+
return query
|
|
168
|
+
with items as (
|
|
169
|
+
select * from open_items(v_tx.company_id, v_tx.contact_id)
|
|
170
|
+
where side = v_side
|
|
171
|
+
),
|
|
172
|
+
-- 1. A reference. The strongest evidence there is, because it was put there
|
|
173
|
+
-- for this purpose — and the weakest link in the chain too, which is why
|
|
174
|
+
-- its check digits are a gap written down rather than a guess made here.
|
|
175
|
+
by_reference as (
|
|
176
|
+
select 'document'::text as kind, array[i.line_id] as line_ids,
|
|
177
|
+
least(i.amount_open, v_target) as amount,
|
|
178
|
+
'reference'::text as method, 1::numeric as score,
|
|
179
|
+
format('the statement carries the reference of %s', coalesce(i.document_number, i.reference)) as because
|
|
180
|
+
from items i
|
|
181
|
+
where i.reference is not null
|
|
182
|
+
and (
|
|
183
|
+
(v_tx.structured_reference is not null
|
|
184
|
+
and upper(regexp_replace(v_tx.structured_reference, '[^0-9A-Za-z]', '', 'g'))
|
|
185
|
+
= upper(regexp_replace(i.reference, '[^0-9A-Za-z]', '', 'g')))
|
|
186
|
+
or (v_tx.reference is not null
|
|
187
|
+
and upper(regexp_replace(v_tx.reference, '[^0-9A-Za-z]', '', 'g'))
|
|
188
|
+
= upper(regexp_replace(i.reference, '[^0-9A-Za-z]', '', 'g')))
|
|
189
|
+
or (v_tx.description is not null
|
|
190
|
+
and position(upper(i.reference) in upper(v_tx.description)) > 0)
|
|
191
|
+
)
|
|
192
|
+
),
|
|
193
|
+
-- 2. The amount, to the tolerance the company set, inside the window it set.
|
|
194
|
+
by_amount as (
|
|
195
|
+
select 'document'::text, array[i.line_id], i.amount_open,
|
|
196
|
+
'exact_amount'::text, 0.900::numeric,
|
|
197
|
+
format('%s is open on %s and the statement is for the same amount',
|
|
198
|
+
i.amount_open, coalesce(i.document_number, 'an entry')) as because
|
|
199
|
+
from items i
|
|
200
|
+
where abs(i.amount_open - v_target) <= v_tolerance
|
|
201
|
+
and abs(i.entry_date - v_tx.transaction_date) <= v_policy.date_window_days
|
|
202
|
+
),
|
|
203
|
+
candidates as (
|
|
204
|
+
select * from by_reference
|
|
205
|
+
union all select * from by_amount
|
|
206
|
+
)
|
|
207
|
+
select c.kind, c.line_ids, c.amount, c.method, c.score, c.because,
|
|
208
|
+
count(*) over (partition by c.method)::integer
|
|
209
|
+
from candidates c
|
|
210
|
+
order by c.score desc, c.amount desc;
|
|
211
|
+
end;
|
|
212
|
+
$$;
|
|
213
|
+
|
|
214
|
+
comment on function suggest_matches(uuid) is
|
|
215
|
+
'What a statement line could settle: the open items it matches, the evidence, and how many candidates that same evidence produced. Writes nothing. A combination of several documents is offered by suggest_combination(), separately, because it is a resemblance and not an identification.';
|
|
216
|
+
|
|
217
|
+
revoke execute on function suggest_matches(uuid) from public, anon;
|
|
218
|
+
grant execute on function suggest_matches(uuid) to authenticated, service_role;
|
|
219
|
+
|
|
220
|
+
-- ---------------------------------------------------------------------------
|
|
221
|
+
-- suggest_combination
|
|
222
|
+
--
|
|
223
|
+
-- One transaction paying several invoices at once — the ordinary shape of a
|
|
224
|
+
-- customer who settles a month of them in one wire. Deliberately its own
|
|
225
|
+
-- function: the answer is a *proposal*, never applied by `auto_settle()`, and
|
|
226
|
+
-- keeping it apart makes that visible rather than buried in a score.
|
|
227
|
+
--
|
|
228
|
+
-- The walk is oldest first and greedy, which is what the production this comes
|
|
229
|
+
-- from does, and it is deterministic — the same statement and the same ledger
|
|
230
|
+
-- give the same answer every time. It does not prove the subset is the only
|
|
231
|
+
-- one that works, and that is exactly why it is not applied.
|
|
232
|
+
-- ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
create or replace function suggest_combination(p_transaction_id uuid)
|
|
235
|
+
returns table (
|
|
236
|
+
line_ids uuid[],
|
|
237
|
+
amount numeric,
|
|
238
|
+
because text
|
|
239
|
+
)
|
|
240
|
+
language plpgsql
|
|
241
|
+
stable
|
|
242
|
+
security invoker
|
|
243
|
+
as $$
|
|
244
|
+
declare
|
|
245
|
+
v_tx bank_transactions;
|
|
246
|
+
v_policy matching_policy;
|
|
247
|
+
v_round money_rounding;
|
|
248
|
+
v_tolerance numeric;
|
|
249
|
+
v_side text;
|
|
250
|
+
v_remaining numeric;
|
|
251
|
+
v_picked uuid[] := '{}';
|
|
252
|
+
v_total numeric := 0;
|
|
253
|
+
v_home char(3);
|
|
254
|
+
r record;
|
|
255
|
+
begin
|
|
256
|
+
select * into v_tx from bank_transactions t where t.id = p_transaction_id;
|
|
257
|
+
if not found then
|
|
258
|
+
raise exception 'not_found: bank transaction %', p_transaction_id
|
|
259
|
+
using errcode = 'no_data_found';
|
|
260
|
+
end if;
|
|
261
|
+
|
|
262
|
+
-- Without a counterparty the search space is every open item of the company,
|
|
263
|
+
-- and a sum that reaches the right total across unrelated customers is
|
|
264
|
+
-- arithmetic, not evidence.
|
|
265
|
+
if v_tx.contact_id is null then return; end if;
|
|
266
|
+
|
|
267
|
+
select c.currency_code into v_home from companies c where c.id = v_tx.company_id;
|
|
268
|
+
if v_tx.currency_code <> v_home then return; end if;
|
|
269
|
+
|
|
270
|
+
v_policy := matching_policy_of(v_tx.company_id);
|
|
271
|
+
v_round := rounding_of(v_tx.company_id, v_tx.currency_code);
|
|
272
|
+
-- Each document was rounded on its own, so a sum of them may miss by more
|
|
273
|
+
-- than one unit. Two, by default, and the company may say otherwise.
|
|
274
|
+
v_tolerance := v_policy.sum_tolerance_units * power(10::numeric, -v_round.decimals);
|
|
275
|
+
v_side := case when v_tx.amount > 0 then 'debit' else 'credit' end;
|
|
276
|
+
v_remaining := abs(v_tx.amount);
|
|
277
|
+
|
|
278
|
+
for r in
|
|
279
|
+
select * from open_items(v_tx.company_id, v_tx.contact_id)
|
|
280
|
+
where side = v_side
|
|
281
|
+
and abs(entry_date - v_tx.transaction_date) <= v_policy.date_window_days
|
|
282
|
+
order by entry_date, line_id
|
|
283
|
+
loop
|
|
284
|
+
if r.amount_open <= v_remaining + v_tolerance then
|
|
285
|
+
v_picked := v_picked || r.line_id;
|
|
286
|
+
v_total := v_total + r.amount_open;
|
|
287
|
+
v_remaining := v_remaining - r.amount_open;
|
|
288
|
+
end if;
|
|
289
|
+
exit when abs(v_remaining) <= v_tolerance;
|
|
290
|
+
end loop;
|
|
291
|
+
|
|
292
|
+
if cardinality(v_picked) > 1 and abs(v_remaining) <= v_tolerance then
|
|
293
|
+
return query select v_picked, v_total,
|
|
294
|
+
format('%s open items of this counterparty add up to %s', cardinality(v_picked), v_total);
|
|
295
|
+
end if;
|
|
296
|
+
end;
|
|
297
|
+
$$;
|
|
298
|
+
|
|
299
|
+
comment on function suggest_combination(uuid) is
|
|
300
|
+
'A subset of one counterparty''s open items that adds up to the statement line, oldest first. A proposal only: it is never applied automatically, because a sum that reaches the right total from the wrong documents leaves nothing behind to notice.';
|
|
301
|
+
|
|
302
|
+
revoke execute on function suggest_combination(uuid) from public, anon;
|
|
303
|
+
grant execute on function suggest_combination(uuid) to authenticated, service_role;
|
|
304
|
+
|
|
305
|
+
-- ---------------------------------------------------------------------------
|
|
306
|
+
-- settle_from_statement
|
|
307
|
+
--
|
|
308
|
+
-- The act. A person — or an agent acting as one — names the lines this money
|
|
309
|
+
-- settles, and this books the payment and matches it.
|
|
310
|
+
-- ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
create or replace function settle_from_statement(p_transaction_id uuid, p_line_ids uuid[])
|
|
313
|
+
returns bank_transactions
|
|
314
|
+
language plpgsql
|
|
315
|
+
volatile
|
|
316
|
+
security invoker
|
|
317
|
+
as $$
|
|
318
|
+
declare
|
|
319
|
+
v_tx bank_transactions;
|
|
320
|
+
v_policy matching_policy;
|
|
321
|
+
v_round money_rounding;
|
|
322
|
+
v_tolerance numeric;
|
|
323
|
+
v_side text;
|
|
324
|
+
v_contact uuid;
|
|
325
|
+
v_contacts integer;
|
|
326
|
+
v_open numeric;
|
|
327
|
+
v_journal uuid;
|
|
328
|
+
v_payment uuid;
|
|
329
|
+
v_entry entries;
|
|
330
|
+
v_pay_line uuid;
|
|
331
|
+
v_remaining numeric;
|
|
332
|
+
v_amount numeric;
|
|
333
|
+
r record;
|
|
334
|
+
begin
|
|
335
|
+
select * into v_tx from bank_transactions t where t.id = p_transaction_id;
|
|
336
|
+
if not found then
|
|
337
|
+
raise exception 'not_found: bank transaction %', p_transaction_id
|
|
338
|
+
using errcode = 'no_data_found';
|
|
339
|
+
end if;
|
|
340
|
+
if v_tx.entry_id is not null then
|
|
341
|
+
raise exception 'statement_line_already_settled: % already points at entry %',
|
|
342
|
+
p_transaction_id, v_tx.entry_id;
|
|
343
|
+
end if;
|
|
344
|
+
if p_line_ids is null or cardinality(p_line_ids) = 0 then
|
|
345
|
+
raise exception 'no_lines_named: settling needs the open items this money pays';
|
|
346
|
+
end if;
|
|
347
|
+
|
|
348
|
+
v_side := case when v_tx.amount > 0 then 'debit' else 'credit' end;
|
|
349
|
+
|
|
350
|
+
-- Every line has to be open, of this company, and on the side this money can
|
|
351
|
+
-- settle. Anything else is a caller that has not looked.
|
|
352
|
+
select count(distinct i.contact_id), count(*), sum(i.amount_open)
|
|
353
|
+
into v_contacts, v_amount, v_open
|
|
354
|
+
from open_items(v_tx.company_id) i
|
|
355
|
+
where i.line_id = any(p_line_ids) and i.side = v_side;
|
|
356
|
+
|
|
357
|
+
if v_amount is distinct from cardinality(p_line_ids) then
|
|
358
|
+
raise exception 'line_not_open: one of the lines named is not an open item of this company on the % side', v_side;
|
|
359
|
+
end if;
|
|
360
|
+
if v_contacts > 1 then
|
|
361
|
+
raise exception 'mixed_contacts: one payment settles the items of one counterparty';
|
|
362
|
+
end if;
|
|
363
|
+
|
|
364
|
+
select i.contact_id into v_contact
|
|
365
|
+
from open_items(v_tx.company_id) i where i.line_id = p_line_ids[1];
|
|
366
|
+
if v_contact is null then
|
|
367
|
+
raise exception 'no_contact_on_open_item: a payment is made to or by somebody, and this item names nobody';
|
|
368
|
+
end if;
|
|
369
|
+
|
|
370
|
+
v_policy := matching_policy_of(v_tx.company_id);
|
|
371
|
+
v_round := rounding_of(v_tx.company_id, v_tx.currency_code);
|
|
372
|
+
v_tolerance := v_policy.amount_tolerance_units * power(10::numeric, -v_round.decimals);
|
|
373
|
+
if abs(v_tx.amount) > v_open + v_tolerance then
|
|
374
|
+
raise exception 'more_money_than_open: the statement line is % and the items named leave % open',
|
|
375
|
+
abs(v_tx.amount), v_open;
|
|
376
|
+
end if;
|
|
377
|
+
|
|
378
|
+
select coalesce(b.journal_id, (select j.id from journals j
|
|
379
|
+
where j.company_id = v_tx.company_id and j.bank_account_id = b.id
|
|
380
|
+
limit 1))
|
|
381
|
+
into v_journal
|
|
382
|
+
from bank_accounts b where b.id = v_tx.bank_account_id;
|
|
383
|
+
if v_journal is null then
|
|
384
|
+
raise exception 'no_journal_for_bank_account: account % books through no journal', v_tx.bank_account_id;
|
|
385
|
+
end if;
|
|
386
|
+
|
|
387
|
+
insert into payments (company_id, direction, payment_date, amount, currency_code,
|
|
388
|
+
contact_id, journal_id, bank_account_id, reference, memo)
|
|
389
|
+
values (v_tx.company_id,
|
|
390
|
+
(case when v_tx.amount > 0 then 'inbound' else 'outbound' end)::payment_direction,
|
|
391
|
+
v_tx.transaction_date, abs(v_tx.amount), v_tx.currency_code,
|
|
392
|
+
v_contact, v_journal, v_tx.bank_account_id,
|
|
393
|
+
coalesce(v_tx.structured_reference, v_tx.reference),
|
|
394
|
+
v_tx.description)
|
|
395
|
+
returning id into v_payment;
|
|
396
|
+
|
|
397
|
+
v_entry := post_payment(v_payment);
|
|
398
|
+
|
|
399
|
+
-- The third-party side of the payment: the line the open items are matched
|
|
400
|
+
-- against. The other side is the bank account, which settles nothing.
|
|
401
|
+
select l.id into v_pay_line
|
|
402
|
+
from entry_lines l
|
|
403
|
+
join accounts a on a.id = l.account_id
|
|
404
|
+
where l.entry_id = v_entry.id and a.reconcilable
|
|
405
|
+
limit 1;
|
|
406
|
+
if v_pay_line is null then
|
|
407
|
+
raise exception 'payment_has_no_third_party_line: entry % books against no reconcilable account',
|
|
408
|
+
v_entry.id;
|
|
409
|
+
end if;
|
|
410
|
+
|
|
411
|
+
-- Oldest first, each item settled up to what is left of the money. A partial
|
|
412
|
+
-- settlement is possible here because a person asked for it by naming the
|
|
413
|
+
-- lines; `auto_settle()` never gets here with less money than is open.
|
|
414
|
+
v_remaining := abs(v_tx.amount);
|
|
415
|
+
for r in
|
|
416
|
+
select * from open_items(v_tx.company_id) i
|
|
417
|
+
where i.line_id = any(p_line_ids)
|
|
418
|
+
order by i.entry_date, i.line_id
|
|
419
|
+
loop
|
|
420
|
+
exit when v_remaining <= 0;
|
|
421
|
+
perform reconcile(v_pay_line, r.line_id, least(r.amount_open, v_remaining));
|
|
422
|
+
v_remaining := v_remaining - least(r.amount_open, v_remaining);
|
|
423
|
+
end loop;
|
|
424
|
+
|
|
425
|
+
update bank_transactions t
|
|
426
|
+
set entry_id = v_entry.id,
|
|
427
|
+
contact_id = coalesce(t.contact_id, v_contact),
|
|
428
|
+
state = 'reconciled'
|
|
429
|
+
where t.id = p_transaction_id
|
|
430
|
+
returning * into v_tx;
|
|
431
|
+
|
|
432
|
+
return v_tx;
|
|
433
|
+
end;
|
|
434
|
+
$$;
|
|
435
|
+
|
|
436
|
+
comment on function settle_from_statement(uuid, uuid[]) is
|
|
437
|
+
'Books the payment a statement line is, and matches it against the open items named. One counterparty, one payment, `post_payment()` and `reconcile()` doing the accounting — nothing here writes a ledger of its own.';
|
|
438
|
+
|
|
439
|
+
revoke execute on function settle_from_statement(uuid, uuid[]) from public, anon;
|
|
440
|
+
grant execute on function settle_from_statement(uuid, uuid[]) to authenticated, service_role;
|
|
441
|
+
|
|
442
|
+
-- ---------------------------------------------------------------------------
|
|
443
|
+
-- auto_settle
|
|
444
|
+
--
|
|
445
|
+
-- The pass over a period. It applies what is an identification and leaves
|
|
446
|
+
-- everything else with a reason, which is the report a bookkeeper actually
|
|
447
|
+
-- reads: what went through, what is waiting for them, and what nobody can
|
|
448
|
+
-- decide from the ledger alone.
|
|
449
|
+
-- ---------------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
create or replace function auto_settle(
|
|
452
|
+
p_company_id uuid,
|
|
453
|
+
p_from date,
|
|
454
|
+
p_to date,
|
|
455
|
+
p_apply boolean default false
|
|
456
|
+
)
|
|
457
|
+
returns table (
|
|
458
|
+
transaction_id uuid,
|
|
459
|
+
action text,
|
|
460
|
+
method text,
|
|
461
|
+
line_ids uuid[],
|
|
462
|
+
because text
|
|
463
|
+
)
|
|
464
|
+
language plpgsql
|
|
465
|
+
volatile
|
|
466
|
+
security invoker
|
|
467
|
+
as $$
|
|
468
|
+
declare
|
|
469
|
+
v_policy matching_policy;
|
|
470
|
+
v_round money_rounding;
|
|
471
|
+
v_tolerance numeric;
|
|
472
|
+
t record;
|
|
473
|
+
best record;
|
|
474
|
+
v_count integer;
|
|
475
|
+
v_open numeric;
|
|
476
|
+
begin
|
|
477
|
+
v_policy := matching_policy_of(p_company_id);
|
|
478
|
+
|
|
479
|
+
for t in
|
|
480
|
+
select * from bank_transactions b
|
|
481
|
+
where b.company_id = p_company_id
|
|
482
|
+
and b.state = 'pending'
|
|
483
|
+
and b.entry_id is null
|
|
484
|
+
and b.transaction_date between p_from and p_to
|
|
485
|
+
order by b.transaction_date, b.sequence
|
|
486
|
+
loop
|
|
487
|
+
v_round := rounding_of(p_company_id, t.currency_code);
|
|
488
|
+
v_tolerance := v_policy.amount_tolerance_units * power(10::numeric, -v_round.decimals);
|
|
489
|
+
|
|
490
|
+
select count(*) into v_count
|
|
491
|
+
from suggest_matches(t.id) s
|
|
492
|
+
where s.kind = 'internal_transfer';
|
|
493
|
+
if v_count > 0 then
|
|
494
|
+
transaction_id := t.id; action := 'left'; method := 'internal_transfer';
|
|
495
|
+
line_ids := '{}'::uuid[];
|
|
496
|
+
because := 'a transfer between two accounts of this company settles nothing, and its other side is its own statement line';
|
|
497
|
+
return next; continue;
|
|
498
|
+
end if;
|
|
499
|
+
|
|
500
|
+
-- The one candidate whose evidence identified exactly one thing.
|
|
501
|
+
select * into best
|
|
502
|
+
from suggest_matches(t.id) s
|
|
503
|
+
where s.alternatives = 1 and s.method in ('reference', 'exact_amount')
|
|
504
|
+
order by s.score desc
|
|
505
|
+
limit 1;
|
|
506
|
+
|
|
507
|
+
if not found then
|
|
508
|
+
select count(*) into v_count from suggest_matches(t.id);
|
|
509
|
+
transaction_id := t.id; action := 'proposed'; method := 'none';
|
|
510
|
+
line_ids := '{}'::uuid[];
|
|
511
|
+
because := case when v_count = 0
|
|
512
|
+
then 'nothing open matches this line'
|
|
513
|
+
else format('%s candidates, none of them an identification', v_count) end;
|
|
514
|
+
return next; continue;
|
|
515
|
+
end if;
|
|
516
|
+
|
|
517
|
+
-- Partial settlement is four different accounting treatments wearing the
|
|
518
|
+
-- same face. The machine proposes; a person decides which one it is.
|
|
519
|
+
select sum(i.amount_open) into v_open
|
|
520
|
+
from open_items(p_company_id) i where i.line_id = any(best.line_ids);
|
|
521
|
+
if abs(t.amount) + v_tolerance < v_open then
|
|
522
|
+
transaction_id := t.id; action := 'proposed'; method := best.method;
|
|
523
|
+
line_ids := best.line_ids;
|
|
524
|
+
because := format('%s of %s — a deposit, a discount, a short payment or an error, and the ledger cannot tell which',
|
|
525
|
+
abs(t.amount), v_open);
|
|
526
|
+
return next; continue;
|
|
527
|
+
end if;
|
|
528
|
+
|
|
529
|
+
if p_apply then
|
|
530
|
+
perform settle_from_statement(t.id, best.line_ids);
|
|
531
|
+
transaction_id := t.id; action := 'settled';
|
|
532
|
+
else
|
|
533
|
+
transaction_id := t.id; action := 'would_settle';
|
|
534
|
+
end if;
|
|
535
|
+
method := best.method; line_ids := best.line_ids; because := best.because;
|
|
536
|
+
return next;
|
|
537
|
+
end loop;
|
|
538
|
+
end;
|
|
539
|
+
$$;
|
|
540
|
+
|
|
541
|
+
comment on function auto_settle(uuid, date, date, boolean) is
|
|
542
|
+
'Walks the pending statement lines of a period and settles the ones a single piece of evidence identifies — a reference, or an exact amount with one candidate. Everything else comes back with the reason it was left: a combination, a partial payment, an internal transfer, or nothing open that fits. With p_apply false it changes nothing and says what it would do.';
|
|
543
|
+
|
|
544
|
+
revoke execute on function auto_settle(uuid, date, date, boolean) from public, anon;
|
|
545
|
+
grant execute on function auto_settle(uuid, date, date, boolean) to authenticated, service_role;
|