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,1189 @@
|
|
|
1
|
+
-- Ekwo OS — VAT when the cash moves, and the exchange difference when it settles.
|
|
2
|
+
--
|
|
3
|
+
-- Two things a country decides that the core could not say yet, and they meet
|
|
4
|
+
-- in the same place: the moment a document is settled.
|
|
5
|
+
--
|
|
6
|
+
-- 1. **A tax that falls due on collection.** `taxes.cash_basis` and
|
|
7
|
+
-- `cash_basis_transition_account_id` landed with the tax engine and nothing
|
|
8
|
+
-- read them. From here `post_document` books such a tax on the transition
|
|
9
|
+
-- account **and on no declaration box**, because nothing is due yet, and
|
|
10
|
+
-- the matching moves the settled share to the final account with the box.
|
|
11
|
+
-- The share is pro rata of what has been settled, cumulative, so a partial
|
|
12
|
+
-- payment carries its part and the last one carries the remainder to the
|
|
13
|
+
-- cent. The whole operation waits, base and tax together: a return whose
|
|
14
|
+
-- base is declared in one period and whose tax is declared in the next is
|
|
15
|
+
-- a return that does not tie out, and the base column of a line is the
|
|
16
|
+
-- base *collected*.
|
|
17
|
+
--
|
|
18
|
+
-- This is not cash accounting as a ledger. Revenue and expense are booked
|
|
19
|
+
-- when the document is, which is what every chart in scope expects; only
|
|
20
|
+
-- the declaration waits. A cash-basis *report* is derived from matched
|
|
21
|
+
-- payments, and stays derived.
|
|
22
|
+
--
|
|
23
|
+
-- 2. **The exchange difference realised at matching.** A receivable booked at
|
|
24
|
+
-- one rate and collected at another leaves a residual in the company's
|
|
25
|
+
-- currency that no payment will ever clear. It is a gain or a loss, and it
|
|
26
|
+
-- is booked as such on the accounts the pack names, so the third-party
|
|
27
|
+
-- account goes to nil. Revaluation of open items — the difference that is
|
|
28
|
+
-- not realised, at a closing date — stays out of scope.
|
|
29
|
+
--
|
|
30
|
+
-- What had to be fixed on the way: **the ledger did not convert**. A document
|
|
31
|
+
-- in a foreign currency booked its foreign figures as if they were the
|
|
32
|
+
-- company's own, and `entry_lines.amount_currency` — which the FEC exports and
|
|
33
|
+
-- which this change needs — was never written by anything. So `post_document`
|
|
34
|
+
-- and `post_payment` now book the company's currency in `debit`/`credit` and
|
|
35
|
+
-- the document's in `amount_currency`, at the rate the document or the payment
|
|
36
|
+
-- carries. There is deliberately no rate feed: the rate is an input, as
|
|
37
|
+
-- `documents.exchange_rate` has been since the first migration.
|
|
38
|
+
|
|
39
|
+
-- ---------------------------------------------------------------------------
|
|
40
|
+
-- Where an exchange difference lands
|
|
41
|
+
--
|
|
42
|
+
-- Two columns, nullable, with no default. A country that has not named them
|
|
43
|
+
-- gets a refusal the day a difference actually arises, and never somebody
|
|
44
|
+
-- else's account: an exchange gain sits in the financial income of the chart
|
|
45
|
+
-- the company keeps, and that number is a fact about a chart, not a constant.
|
|
46
|
+
-- ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
alter table country_defaults
|
|
49
|
+
add column if not exists fx_gain_code text,
|
|
50
|
+
add column if not exists fx_loss_code text;
|
|
51
|
+
|
|
52
|
+
comment on column country_defaults.fx_gain_code is
|
|
53
|
+
'Account a realised exchange gain is booked on, from the pack. Null until the pack names one, and then a matching that realises a gain is refused rather than booked somewhere plausible.';
|
|
54
|
+
comment on column country_defaults.fx_loss_code is
|
|
55
|
+
'The same for a realised loss. A pair, because every chart in scope keeps the gain and the loss apart.';
|
|
56
|
+
|
|
57
|
+
-- ---------------------------------------------------------------------------
|
|
58
|
+
-- The rate a payment was made at
|
|
59
|
+
--
|
|
60
|
+
-- `documents.exchange_rate` has existed since the first migration and nothing
|
|
61
|
+
-- read it. A payment had no equivalent at all, and without one a payment in a
|
|
62
|
+
-- foreign currency cannot differ from the invoice — which is the entire
|
|
63
|
+
-- subject. Same convention on both, stated here once.
|
|
64
|
+
-- ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
alter table payments
|
|
67
|
+
add column if not exists exchange_rate numeric(18, 8) not null default 1;
|
|
68
|
+
|
|
69
|
+
alter table payments
|
|
70
|
+
drop constraint if exists payments_rate_positive;
|
|
71
|
+
alter table payments
|
|
72
|
+
add constraint payments_rate_positive check (exchange_rate > 0);
|
|
73
|
+
|
|
74
|
+
comment on column payments.exchange_rate is
|
|
75
|
+
'Units of the payment currency for one unit of the company currency, as currency_rates states it. The ledger amount is the payment amount divided by it. 1 when the payment is in the company currency.';
|
|
76
|
+
comment on column documents.exchange_rate is
|
|
77
|
+
'Units of the document currency for one unit of the company currency, as currency_rates states it. The ledger amount is the document amount divided by it. 1 when the document is in the company currency.';
|
|
78
|
+
comment on column entry_lines.amount_currency is
|
|
79
|
+
'The amount of this line in its own currency, written whenever that currency is not the company''s. Positive like debit and credit; the side carries the sign.';
|
|
80
|
+
comment on column entry_lines.box_amount is
|
|
81
|
+
'Amount to report in that box, in the sign the form expects. A line of a cash-basis tax carries the amount with no box: it is computed when the document is posted and waits for the matching that names the box it is finally reported in.';
|
|
82
|
+
|
|
83
|
+
-- ---------------------------------------------------------------------------
|
|
84
|
+
-- What a matching had to book
|
|
85
|
+
--
|
|
86
|
+
-- A matching is not supposed to touch the accounts — that is the sentence the
|
|
87
|
+
-- MCP server prints when it undoes one. It stays true of the matching itself
|
|
88
|
+
-- and stops being true of what a matching *reveals*: a tax that falls due, a
|
|
89
|
+
-- difference that is realised. Both are entries of their own, and the row that
|
|
90
|
+
-- caused them says so, so a client can show them and `unreconcile` can find
|
|
91
|
+
-- them.
|
|
92
|
+
-- ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
alter table reconciliations
|
|
95
|
+
add column if not exists fx_entry_id uuid references entries(id) on delete set null,
|
|
96
|
+
add column if not exists tax_transfer_entry_id uuid references entries(id) on delete set null;
|
|
97
|
+
|
|
98
|
+
comment on column reconciliations.fx_entry_id is
|
|
99
|
+
'Entry that booked the exchange difference this matching realised, when there was one.';
|
|
100
|
+
comment on column reconciliations.tax_transfer_entry_id is
|
|
101
|
+
'Entry that moved the cash-basis tax this matching made due, when there was one.';
|
|
102
|
+
|
|
103
|
+
create index if not exists reconciliations_fx_entry_idx on reconciliations (fx_entry_id);
|
|
104
|
+
create index if not exists reconciliations_tax_entry_idx on reconciliations (tax_transfer_entry_id);
|
|
105
|
+
|
|
106
|
+
-- ---------------------------------------------------------------------------
|
|
107
|
+
-- What a document has been settled by, when it is not in the company currency
|
|
108
|
+
--
|
|
109
|
+
-- `amount_paid` is compared with `amount_total`, which is the document's own
|
|
110
|
+
-- currency, and it was summing `matched_amount`, which is the ledger's. The
|
|
111
|
+
-- two were the same number as long as nothing converted. Now that something
|
|
112
|
+
-- does, the settled share is read back through the currency amount of the line
|
|
113
|
+
-- it was matched on. A line with no currency amount contributes what it always
|
|
114
|
+
-- did, so a document in the company's currency answers exactly as before.
|
|
115
|
+
-- ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
create or replace function documents_refresh_amount_paid(p_document_id uuid)
|
|
118
|
+
returns void
|
|
119
|
+
language plpgsql
|
|
120
|
+
as $$
|
|
121
|
+
begin
|
|
122
|
+
-- The statement names amount_paid so that documents_refresh_payment_state,
|
|
123
|
+
-- which watches that column, fires in turn.
|
|
124
|
+
update documents d
|
|
125
|
+
set amount_paid = coalesce((
|
|
126
|
+
select sum(case
|
|
127
|
+
when l.amount_currency is null then l.matched_amount
|
|
128
|
+
else round(l.matched_amount * l.amount_currency
|
|
129
|
+
/ nullif(abs(l.debit - l.credit), 0), 2)
|
|
130
|
+
end)
|
|
131
|
+
from entry_lines l
|
|
132
|
+
join accounts a on a.id = l.account_id
|
|
133
|
+
where l.entry_id = d.entry_id
|
|
134
|
+
and a.reconcilable
|
|
135
|
+
and a.account_type in ('asset_receivable', 'liability_payable')
|
|
136
|
+
), 0)
|
|
137
|
+
where d.id = p_document_id
|
|
138
|
+
and d.entry_id is not null;
|
|
139
|
+
end;
|
|
140
|
+
$$;
|
|
141
|
+
|
|
142
|
+
-- ---------------------------------------------------------------------------
|
|
143
|
+
-- post_document — the same function, converting, and deferring what is not due
|
|
144
|
+
--
|
|
145
|
+
-- Three changes, and nothing else moves:
|
|
146
|
+
--
|
|
147
|
+
-- * every amount is worked out in the document's currency exactly as
|
|
148
|
+
-- before, then divided by the rate to give the ledger amount. At a rate
|
|
149
|
+
-- of 1 the division is the identity, which is why `posting.test.ts` is
|
|
150
|
+
-- untouched and green.
|
|
151
|
+
-- * the counterpart still balances by construction, in both currencies; the
|
|
152
|
+
-- total it is checked against is the one in the document's currency,
|
|
153
|
+
-- which is the currency `documents.amount_total` is stated in.
|
|
154
|
+
-- * a tax on a cash basis lands on its transition account and names no box,
|
|
155
|
+
-- and so does the base it is computed on. The box amount is worked out
|
|
156
|
+
-- here all the same and waits on the line: it is what the matching will
|
|
157
|
+
-- report, pro rata, and working it out twice is how two answers appear.
|
|
158
|
+
-- ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
create or replace function post_document(p_document_id uuid)
|
|
161
|
+
returns entries
|
|
162
|
+
language plpgsql
|
|
163
|
+
as $$
|
|
164
|
+
declare
|
|
165
|
+
v_doc documents%rowtype;
|
|
166
|
+
v_entry entries%rowtype;
|
|
167
|
+
v_journal uuid;
|
|
168
|
+
v_date date;
|
|
169
|
+
v_is_sale boolean;
|
|
170
|
+
v_is_credit boolean;
|
|
171
|
+
v_kind tax_document_kind;
|
|
172
|
+
v_base_credit boolean;
|
|
173
|
+
v_seq integer := 0;
|
|
174
|
+
v_contact uuid;
|
|
175
|
+
v_maturity date;
|
|
176
|
+
v_terms smallint;
|
|
177
|
+
v_counterpart uuid;
|
|
178
|
+
v_diff numeric(16, 2);
|
|
179
|
+
v_diff_cur numeric(16, 2);
|
|
180
|
+
v_amount numeric(16, 2);
|
|
181
|
+
v_book numeric(16, 2);
|
|
182
|
+
v_box_amount numeric(16, 2);
|
|
183
|
+
v_share numeric(16, 2);
|
|
184
|
+
v_share_book numeric(16, 2);
|
|
185
|
+
v_share_box numeric(16, 2);
|
|
186
|
+
v_left numeric(16, 2);
|
|
187
|
+
v_left_box numeric(16, 2);
|
|
188
|
+
v_side_left numeric(16, 2);
|
|
189
|
+
v_side_left_neg numeric(16, 2);
|
|
190
|
+
v_side_credit boolean;
|
|
191
|
+
v_label text;
|
|
192
|
+
v_rate numeric(18, 8);
|
|
193
|
+
v_home char(3);
|
|
194
|
+
v_foreign boolean;
|
|
195
|
+
v_total_cur numeric(16, 2);
|
|
196
|
+
v_cash boolean;
|
|
197
|
+
v_transition uuid;
|
|
198
|
+
v_postings integer;
|
|
199
|
+
r record;
|
|
200
|
+
p record;
|
|
201
|
+
g record;
|
|
202
|
+
begin
|
|
203
|
+
select * into v_doc from documents where id = p_document_id for update;
|
|
204
|
+
if not found then
|
|
205
|
+
raise exception 'unknown_document: document % does not exist', p_document_id;
|
|
206
|
+
end if;
|
|
207
|
+
if v_doc.state = 'posted' then
|
|
208
|
+
raise exception 'document_already_posted: document % is already posted', p_document_id;
|
|
209
|
+
end if;
|
|
210
|
+
if v_doc.state = 'cancelled' then
|
|
211
|
+
raise exception 'document_cancelled: document % cannot be posted', p_document_id;
|
|
212
|
+
end if;
|
|
213
|
+
if v_doc.entry_id is not null then
|
|
214
|
+
raise exception 'document_already_booked: document % already points at entry %',
|
|
215
|
+
p_document_id, v_doc.entry_id;
|
|
216
|
+
end if;
|
|
217
|
+
|
|
218
|
+
if v_doc.doc_type in ('sale_quote', 'purchase_order') then
|
|
219
|
+
raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
|
|
220
|
+
end if;
|
|
221
|
+
|
|
222
|
+
v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
|
|
223
|
+
v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
|
|
224
|
+
v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
|
|
225
|
+
-- Sale invoice and purchase credit note credit the base; the other two debit it.
|
|
226
|
+
v_base_credit := (v_is_sale <> v_is_credit);
|
|
227
|
+
|
|
228
|
+
v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
|
|
229
|
+
|
|
230
|
+
if not exists (
|
|
231
|
+
select 1 from document_lines
|
|
232
|
+
where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
|
|
233
|
+
) then
|
|
234
|
+
raise exception 'document_empty: document % has no billable line', p_document_id;
|
|
235
|
+
end if;
|
|
236
|
+
|
|
237
|
+
-- A fixed-amount tax has no basis to spread over lines; refuse rather than
|
|
238
|
+
-- guess.
|
|
239
|
+
if exists (
|
|
240
|
+
select 1 from document_lines l join taxes t on t.id = l.tax_id
|
|
241
|
+
where l.document_id = p_document_id and t.amount_type <> 'percent'
|
|
242
|
+
) then
|
|
243
|
+
raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
|
|
244
|
+
end if;
|
|
245
|
+
|
|
246
|
+
-- Every tax used must be in force on the accounting date.
|
|
247
|
+
for r in
|
|
248
|
+
select distinct t.id, t.code, t.valid_from, t.valid_to
|
|
249
|
+
from document_lines l join taxes t on t.id = l.tax_id
|
|
250
|
+
where l.document_id = p_document_id
|
|
251
|
+
loop
|
|
252
|
+
if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
|
|
253
|
+
raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
|
|
254
|
+
end if;
|
|
255
|
+
end loop;
|
|
256
|
+
|
|
257
|
+
-- Totals are derived; make sure they reflect the lines as they stand now.
|
|
258
|
+
perform documents_refresh_totals(p_document_id);
|
|
259
|
+
select * into v_doc from documents where id = p_document_id;
|
|
260
|
+
|
|
261
|
+
select c.currency_code into v_home from companies c where c.id = v_doc.company_id;
|
|
262
|
+
v_rate := v_doc.exchange_rate;
|
|
263
|
+
v_foreign := v_doc.currency_code <> v_home;
|
|
264
|
+
|
|
265
|
+
v_journal := coalesce(
|
|
266
|
+
v_doc.journal_id,
|
|
267
|
+
case when v_is_sale
|
|
268
|
+
then (select sales_journal_id from companies where id = v_doc.company_id)
|
|
269
|
+
else (select purchase_journal_id from companies where id = v_doc.company_id)
|
|
270
|
+
end
|
|
271
|
+
);
|
|
272
|
+
if v_journal is null then
|
|
273
|
+
raise exception 'no_journal: set journal_id on the document or a default journal on the company';
|
|
274
|
+
end if;
|
|
275
|
+
|
|
276
|
+
perform assert_period_open(v_doc.company_id, v_date, true);
|
|
277
|
+
|
|
278
|
+
v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
|
|
279
|
+
|
|
280
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
|
|
281
|
+
description, state, document_id, currency_code)
|
|
282
|
+
values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
|
|
283
|
+
coalesce(v_doc.number, v_doc.supplier_reference),
|
|
284
|
+
v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
|
|
285
|
+
then ' / ' || v_doc.supplier_reference else '' end,
|
|
286
|
+
'draft', p_document_id, v_doc.currency_code)
|
|
287
|
+
returning * into v_entry;
|
|
288
|
+
|
|
289
|
+
-- ------------------------------------------------------------------ bases
|
|
290
|
+
for r in
|
|
291
|
+
select l.account_id,
|
|
292
|
+
l.tax_id,
|
|
293
|
+
sum(l.amount_untaxed) as base_amount,
|
|
294
|
+
min(l.sequence) as seq,
|
|
295
|
+
string_agg(distinct l.name, ', ') as label
|
|
296
|
+
from document_lines l
|
|
297
|
+
where l.document_id = p_document_id
|
|
298
|
+
and l.line_type = 'product'
|
|
299
|
+
group by l.account_id, l.tax_id
|
|
300
|
+
having sum(l.amount_untaxed) <> 0
|
|
301
|
+
order by 4
|
|
302
|
+
loop
|
|
303
|
+
select tp.declaration_box, tp.factor_percent, tp.box_factor_percent,
|
|
304
|
+
coalesce(t.cash_basis, false) as cash_basis
|
|
305
|
+
into p
|
|
306
|
+
from tax_postings tp
|
|
307
|
+
join taxes t on t.id = tp.tax_id
|
|
308
|
+
where tp.tax_id = r.tax_id
|
|
309
|
+
and tp.document_kind = v_kind
|
|
310
|
+
and tp.posting_type = 'base'
|
|
311
|
+
limit 1;
|
|
312
|
+
|
|
313
|
+
v_amount := round(r.base_amount * coalesce(p.factor_percent, 100) / 100, 2);
|
|
314
|
+
v_book := round(v_amount / v_rate, 2);
|
|
315
|
+
v_seq := v_seq + 10;
|
|
316
|
+
|
|
317
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
318
|
+
debit, credit, tax_id, tax_line,
|
|
319
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
320
|
+
values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
|
|
321
|
+
case when v_base_credit then 0 else v_book end,
|
|
322
|
+
case when v_base_credit then v_book else 0 end,
|
|
323
|
+
r.tax_id, false,
|
|
324
|
+
case when coalesce(p.cash_basis, false) then null else p.declaration_box end,
|
|
325
|
+
case when p.declaration_box is null then null
|
|
326
|
+
else round(r.base_amount * coalesce(p.box_factor_percent, 100) / 100 / v_rate, 2) end,
|
|
327
|
+
v_doc.currency_code,
|
|
328
|
+
case when v_foreign then v_amount end);
|
|
329
|
+
end loop;
|
|
330
|
+
|
|
331
|
+
-- ------------------------------------------------------------------ taxes
|
|
332
|
+
for r in
|
|
333
|
+
select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
|
|
334
|
+
from document_tax_summary s
|
|
335
|
+
where s.document_id = p_document_id
|
|
336
|
+
and s.tax_id is not null
|
|
337
|
+
and s.tax_amount <> 0
|
|
338
|
+
order by s.tax_code
|
|
339
|
+
loop
|
|
340
|
+
select t.cash_basis, t.cash_basis_transition_account_id
|
|
341
|
+
into v_cash, v_transition
|
|
342
|
+
from taxes t where t.id = r.tax_id;
|
|
343
|
+
|
|
344
|
+
if v_cash then
|
|
345
|
+
-- A tax that waits needs somewhere to wait. Refuse by name rather than
|
|
346
|
+
-- book it on the account it is due on, which would make it due.
|
|
347
|
+
if v_transition is null then
|
|
348
|
+
raise exception 'no_cash_basis_account: tax % falls due on collection and names no transition account',
|
|
349
|
+
r.tax_code;
|
|
350
|
+
end if;
|
|
351
|
+
-- One posting per side, or the transition lines of a document cannot be
|
|
352
|
+
-- told apart when the matching sends each of them on. A tax whose
|
|
353
|
+
-- postings net out has nothing waiting to collect anyway.
|
|
354
|
+
select count(*) into v_postings
|
|
355
|
+
from tax_postings tp
|
|
356
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
357
|
+
and tp.posting_type = 'tax';
|
|
358
|
+
if v_postings > 1 then
|
|
359
|
+
raise exception 'cash_basis_split_tax: tax % falls due on collection and has % tax postings; it takes one',
|
|
360
|
+
r.tax_code, v_postings;
|
|
361
|
+
end if;
|
|
362
|
+
if exists (select 1 from tax_postings tp
|
|
363
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
364
|
+
and tp.posting_type = 'tax_on_base') then
|
|
365
|
+
raise exception 'cash_basis_tax_on_base: tax % falls due on collection and carries a non-deductible share; a cost is not deferred',
|
|
366
|
+
r.tax_code;
|
|
367
|
+
end if;
|
|
368
|
+
end if;
|
|
369
|
+
|
|
370
|
+
-- The postings of one side share out the tax of the group; the last of
|
|
371
|
+
-- each side takes what is left. Until `tax_on_base` there was never more
|
|
372
|
+
-- than one posting per side, so this changes no existing tax by a cent —
|
|
373
|
+
-- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
|
|
374
|
+
-- where rounding each half on its own would book 0.64 against a document
|
|
375
|
+
-- that totals 0.63.
|
|
376
|
+
v_side_left := null;
|
|
377
|
+
v_side_left_neg := null;
|
|
378
|
+
|
|
379
|
+
for p in
|
|
380
|
+
select tp.posting_type, tp.factor_percent, tp.account_id,
|
|
381
|
+
tp.declaration_box, tp.box_factor_percent,
|
|
382
|
+
case when tp.factor_percent >= 0 then 1 else -1 end as side,
|
|
383
|
+
sum(abs(tp.factor_percent))
|
|
384
|
+
over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
|
|
385
|
+
as side_factor,
|
|
386
|
+
row_number() over (
|
|
387
|
+
partition by case when tp.factor_percent >= 0 then 1 else -1 end
|
|
388
|
+
order by tp.sequence, tp.id)
|
|
389
|
+
= count(*) over (
|
|
390
|
+
partition by case when tp.factor_percent >= 0 then 1 else -1 end)
|
|
391
|
+
as is_last_of_side
|
|
392
|
+
from tax_postings tp
|
|
393
|
+
where tp.tax_id = r.tax_id
|
|
394
|
+
and tp.document_kind = v_kind
|
|
395
|
+
and tp.posting_type in ('tax', 'tax_on_base')
|
|
396
|
+
order by tp.sequence, tp.id
|
|
397
|
+
loop
|
|
398
|
+
-- The tax of the group was rounded once, in the view. Every posting is
|
|
399
|
+
-- a share of that one figure, never of a re-derived one.
|
|
400
|
+
if p.side >= 0 then
|
|
401
|
+
if v_side_left is null then
|
|
402
|
+
v_side_left := round(r.tax_amount * p.side_factor / 100, 2);
|
|
403
|
+
end if;
|
|
404
|
+
if p.is_last_of_side then
|
|
405
|
+
v_amount := v_side_left;
|
|
406
|
+
else
|
|
407
|
+
v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
|
|
408
|
+
v_side_left := v_side_left - v_amount;
|
|
409
|
+
end if;
|
|
410
|
+
else
|
|
411
|
+
if v_side_left_neg is null then
|
|
412
|
+
v_side_left_neg := round(r.tax_amount * p.side_factor / 100, 2);
|
|
413
|
+
end if;
|
|
414
|
+
if p.is_last_of_side then
|
|
415
|
+
v_amount := v_side_left_neg;
|
|
416
|
+
else
|
|
417
|
+
v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
|
|
418
|
+
v_side_left_neg := v_side_left_neg - v_amount;
|
|
419
|
+
end if;
|
|
420
|
+
end if;
|
|
421
|
+
|
|
422
|
+
if v_amount = 0 then
|
|
423
|
+
continue;
|
|
424
|
+
end if;
|
|
425
|
+
-- A positive factor keeps the side of the base, a negative one flips it.
|
|
426
|
+
v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
|
|
427
|
+
-- The box keeps its own rounding: `box_factor_percent` was always
|
|
428
|
+
-- independent from `factor_percent`, because a declaration figure is
|
|
429
|
+
-- not a ledger figure and only the ledger has to balance.
|
|
430
|
+
v_box_amount := case when p.declaration_box is null then null
|
|
431
|
+
else round(r.tax_amount * p.box_factor_percent / 100 / v_rate, 2) end;
|
|
432
|
+
v_book := round(v_amount / v_rate, 2);
|
|
433
|
+
|
|
434
|
+
if p.posting_type = 'tax' then
|
|
435
|
+
v_seq := v_seq + 10;
|
|
436
|
+
|
|
437
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
438
|
+
debit, credit, tax_id, tax_line,
|
|
439
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
440
|
+
values (v_entry.id, v_doc.company_id,
|
|
441
|
+
case when v_cash then v_transition else p.account_id end,
|
|
442
|
+
v_seq, r.tax_name,
|
|
443
|
+
case when v_side_credit then 0 else v_book end,
|
|
444
|
+
case when v_side_credit then v_book else 0 end,
|
|
445
|
+
r.tax_id, true,
|
|
446
|
+
case when v_cash then null else p.declaration_box end,
|
|
447
|
+
v_box_amount, v_doc.currency_code,
|
|
448
|
+
case when v_foreign then v_amount end);
|
|
449
|
+
continue;
|
|
450
|
+
end if;
|
|
451
|
+
|
|
452
|
+
-- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
|
|
453
|
+
-- lines it taxes, split in proportion to their base. The last share
|
|
454
|
+
-- takes whatever is left, so the shares add up to the amount that was
|
|
455
|
+
-- rounded once on the group and the entry still balances to the cent.
|
|
456
|
+
v_left := v_amount;
|
|
457
|
+
v_left_box := v_box_amount;
|
|
458
|
+
|
|
459
|
+
for g in
|
|
460
|
+
select account_id,
|
|
461
|
+
base_amount,
|
|
462
|
+
seq,
|
|
463
|
+
sum(base_amount) over () as total_base,
|
|
464
|
+
row_number() over (order by seq) = count(*) over () as is_last
|
|
465
|
+
from (
|
|
466
|
+
select l.account_id,
|
|
467
|
+
sum(l.amount_untaxed) as base_amount,
|
|
468
|
+
min(l.sequence) as seq
|
|
469
|
+
from document_lines l
|
|
470
|
+
where l.document_id = p_document_id
|
|
471
|
+
and l.line_type = 'product'
|
|
472
|
+
and l.tax_id = r.tax_id
|
|
473
|
+
group by l.account_id
|
|
474
|
+
having sum(l.amount_untaxed) <> 0
|
|
475
|
+
) as groups
|
|
476
|
+
order by seq
|
|
477
|
+
loop
|
|
478
|
+
if g.is_last then
|
|
479
|
+
v_share := v_left;
|
|
480
|
+
v_share_box := v_left_box;
|
|
481
|
+
else
|
|
482
|
+
v_share := round(v_amount * g.base_amount / g.total_base, 2);
|
|
483
|
+
v_share_box := case when v_box_amount is null then null
|
|
484
|
+
else round(v_box_amount * g.base_amount / g.total_base, 2) end;
|
|
485
|
+
v_left := v_left - v_share;
|
|
486
|
+
v_left_box := v_left_box - v_share_box;
|
|
487
|
+
end if;
|
|
488
|
+
|
|
489
|
+
if v_share = 0 then
|
|
490
|
+
continue;
|
|
491
|
+
end if;
|
|
492
|
+
|
|
493
|
+
v_seq := v_seq + 10;
|
|
494
|
+
v_share_book := round(v_share / v_rate, 2);
|
|
495
|
+
|
|
496
|
+
-- `tax_line` stays false: the amount is on a base account and belongs
|
|
497
|
+
-- to the base side of the declaration, which is why the Belgian grids
|
|
498
|
+
-- 82 and 83 report it together with the base.
|
|
499
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
500
|
+
debit, credit, tax_id, tax_line,
|
|
501
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
502
|
+
values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
|
|
503
|
+
case when v_side_credit then 0 else v_share_book end,
|
|
504
|
+
case when v_side_credit then v_share_book else 0 end,
|
|
505
|
+
r.tax_id, false,
|
|
506
|
+
p.declaration_box, v_share_box, v_doc.currency_code,
|
|
507
|
+
case when v_foreign then v_share end);
|
|
508
|
+
end loop;
|
|
509
|
+
end loop;
|
|
510
|
+
end loop;
|
|
511
|
+
|
|
512
|
+
-- ------------------------------------------------------------ counterpart
|
|
513
|
+
select total_debit - total_credit into v_diff from entries where id = v_entry.id;
|
|
514
|
+
select coalesce(sum(case when l.debit > 0 then l.amount_currency else -l.amount_currency end), 0)
|
|
515
|
+
into v_diff_cur
|
|
516
|
+
from entry_lines l where l.entry_id = v_entry.id;
|
|
517
|
+
|
|
518
|
+
if v_diff = 0 then
|
|
519
|
+
raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
|
|
520
|
+
end if;
|
|
521
|
+
|
|
522
|
+
v_contact := commercial_entity(v_doc.contact_id);
|
|
523
|
+
v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
|
|
524
|
+
|
|
525
|
+
select payment_terms_days into v_terms from contacts where id = v_contact;
|
|
526
|
+
v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
|
|
527
|
+
|
|
528
|
+
v_amount := abs(v_diff);
|
|
529
|
+
-- The counterpart balances the entry in both currencies. The total it is
|
|
530
|
+
-- checked against is the document's own, which is the currency
|
|
531
|
+
-- `amount_total` is stated in.
|
|
532
|
+
v_total_cur := case when v_foreign then abs(v_diff_cur) else v_amount end;
|
|
533
|
+
v_seq := v_seq + 10;
|
|
534
|
+
|
|
535
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
536
|
+
debit, credit, contact_id, date_maturity, currency_code,
|
|
537
|
+
amount_currency)
|
|
538
|
+
values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
|
|
539
|
+
case when v_diff > 0 then 0 else v_amount end,
|
|
540
|
+
case when v_diff > 0 then v_amount else 0 end,
|
|
541
|
+
v_contact, v_maturity, v_doc.currency_code,
|
|
542
|
+
case when v_foreign then v_total_cur end);
|
|
543
|
+
|
|
544
|
+
-- The ledger is right by construction. If the header disagrees, the header
|
|
545
|
+
-- is what is wrong, and we say so instead of quietly patching a line.
|
|
546
|
+
if abs(v_total_cur - abs(v_doc.amount_total)) > 0.005 then
|
|
547
|
+
raise exception 'document_total_mismatch: document % totals % but its lines book %',
|
|
548
|
+
p_document_id, v_doc.amount_total, v_total_cur;
|
|
549
|
+
end if;
|
|
550
|
+
|
|
551
|
+
-- --------------------------------------------------------------- posting
|
|
552
|
+
v_entry := post_entry(v_entry.id);
|
|
553
|
+
|
|
554
|
+
update documents
|
|
555
|
+
set state = 'posted',
|
|
556
|
+
number = coalesce(number, v_entry.number),
|
|
557
|
+
entry_id = v_entry.id,
|
|
558
|
+
accounting_date = v_date
|
|
559
|
+
where id = p_document_id;
|
|
560
|
+
|
|
561
|
+
return v_entry;
|
|
562
|
+
end;
|
|
563
|
+
$$;
|
|
564
|
+
|
|
565
|
+
comment on function post_document(uuid) is
|
|
566
|
+
'Books a document: base lines, tax lines from tax_postings — the non-deductible share on the accounts of the lines, a cash-basis tax on its transition account and on no box — a counterpart that balances by construction, and the company currency in the ledger at the rate the document carries.';
|
|
567
|
+
|
|
568
|
+
-- ---------------------------------------------------------------------------
|
|
569
|
+
-- post_payment — the same two lines, in the company's currency
|
|
570
|
+
-- ---------------------------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
create or replace function post_payment(p_payment_id uuid)
|
|
573
|
+
returns entries
|
|
574
|
+
language plpgsql
|
|
575
|
+
as $$
|
|
576
|
+
declare
|
|
577
|
+
v_pay payments%rowtype;
|
|
578
|
+
v_entry entries%rowtype;
|
|
579
|
+
v_money uuid;
|
|
580
|
+
v_third uuid;
|
|
581
|
+
v_contact uuid;
|
|
582
|
+
v_inbound boolean;
|
|
583
|
+
v_label text;
|
|
584
|
+
v_home char(3);
|
|
585
|
+
v_foreign boolean;
|
|
586
|
+
v_book numeric(16, 2);
|
|
587
|
+
begin
|
|
588
|
+
select * into v_pay from payments where id = p_payment_id for update;
|
|
589
|
+
if not found then
|
|
590
|
+
raise exception 'unknown_payment: payment % does not exist', p_payment_id;
|
|
591
|
+
end if;
|
|
592
|
+
if v_pay.entry_id is not null then
|
|
593
|
+
raise exception 'payment_already_booked: payment % already points at entry %',
|
|
594
|
+
p_payment_id, v_pay.entry_id;
|
|
595
|
+
end if;
|
|
596
|
+
if v_pay.state = 'cancelled' then
|
|
597
|
+
raise exception 'payment_cancelled: payment % cannot be booked', p_payment_id;
|
|
598
|
+
end if;
|
|
599
|
+
|
|
600
|
+
v_inbound := v_pay.direction = 'inbound';
|
|
601
|
+
|
|
602
|
+
-- The bank side: the account behind the payment's bank account, or the
|
|
603
|
+
-- default account of the journal it goes through.
|
|
604
|
+
select b.account_id into v_money
|
|
605
|
+
from bank_accounts b
|
|
606
|
+
where b.id = v_pay.bank_account_id;
|
|
607
|
+
|
|
608
|
+
if v_money is null then
|
|
609
|
+
select coalesce(j.default_account_id, b.account_id) into v_money
|
|
610
|
+
from journals j
|
|
611
|
+
left join bank_accounts b on b.id = j.bank_account_id
|
|
612
|
+
where j.id = v_pay.journal_id;
|
|
613
|
+
end if;
|
|
614
|
+
|
|
615
|
+
-- A bank account wired to the journal from its own side counts too: that is
|
|
616
|
+
-- the direction an operator fills in first.
|
|
617
|
+
if v_money is null then
|
|
618
|
+
select b.account_id into v_money
|
|
619
|
+
from bank_accounts b
|
|
620
|
+
where b.journal_id = v_pay.journal_id
|
|
621
|
+
and b.active
|
|
622
|
+
and b.account_id is not null
|
|
623
|
+
order by b.created_at
|
|
624
|
+
limit 1;
|
|
625
|
+
end if;
|
|
626
|
+
|
|
627
|
+
if v_money is null then
|
|
628
|
+
raise exception 'no_bank_account: set bank_account_id on the payment, or default_account_id on journal %',
|
|
629
|
+
v_pay.journal_id;
|
|
630
|
+
end if;
|
|
631
|
+
|
|
632
|
+
-- The third-party side, by role.
|
|
633
|
+
v_contact := commercial_entity(v_pay.contact_id);
|
|
634
|
+
v_third := resolve_counterpart_account(v_pay.company_id, v_contact, v_inbound);
|
|
635
|
+
|
|
636
|
+
v_label := coalesce(v_pay.reference, v_pay.memo, 'payment');
|
|
637
|
+
|
|
638
|
+
select c.currency_code into v_home from companies c where c.id = v_pay.company_id;
|
|
639
|
+
v_foreign := v_pay.currency_code <> v_home;
|
|
640
|
+
v_book := round(v_pay.amount / v_pay.exchange_rate, 2);
|
|
641
|
+
|
|
642
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
|
|
643
|
+
description, state, currency_code)
|
|
644
|
+
values (v_pay.company_id, v_pay.journal_id,
|
|
645
|
+
fiscal_year_at(v_pay.company_id, v_pay.payment_date), v_pay.payment_date,
|
|
646
|
+
v_pay.reference, v_label, 'draft', v_pay.currency_code)
|
|
647
|
+
returning * into v_entry;
|
|
648
|
+
|
|
649
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
650
|
+
debit, credit, currency_code, amount_currency)
|
|
651
|
+
values (v_entry.id, v_pay.company_id, v_money, 10, v_label,
|
|
652
|
+
case when v_inbound then v_book else 0 end,
|
|
653
|
+
case when v_inbound then 0 else v_book end,
|
|
654
|
+
v_pay.currency_code,
|
|
655
|
+
case when v_foreign then v_pay.amount end);
|
|
656
|
+
|
|
657
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
658
|
+
debit, credit, contact_id, currency_code, amount_currency)
|
|
659
|
+
values (v_entry.id, v_pay.company_id, v_third, 20, v_label,
|
|
660
|
+
case when v_inbound then 0 else v_book end,
|
|
661
|
+
case when v_inbound then v_book else 0 end,
|
|
662
|
+
v_contact, v_pay.currency_code,
|
|
663
|
+
case when v_foreign then v_pay.amount end);
|
|
664
|
+
|
|
665
|
+
v_entry := post_entry(v_entry.id);
|
|
666
|
+
|
|
667
|
+
update payments
|
|
668
|
+
set entry_id = v_entry.id,
|
|
669
|
+
state = 'posted'
|
|
670
|
+
where id = p_payment_id;
|
|
671
|
+
|
|
672
|
+
return v_entry;
|
|
673
|
+
end;
|
|
674
|
+
$$;
|
|
675
|
+
|
|
676
|
+
comment on function post_payment(uuid) is
|
|
677
|
+
'Books a payment: the bank side from the payment''s bank account or its journal, the third-party side by role, both in the company currency at the payment''s rate. Matches nothing.';
|
|
678
|
+
|
|
679
|
+
-- ---------------------------------------------------------------------------
|
|
680
|
+
-- settle_cash_basis_tax — the share of a waiting tax that has become due
|
|
681
|
+
--
|
|
682
|
+
-- Called by `reconcile` and by `unreconcile`, and derived entirely from the
|
|
683
|
+
-- ledger: what is waiting on the document, what share of the document has been
|
|
684
|
+
-- settled, and what earlier matchings already sent on. The difference is the
|
|
685
|
+
-- entry this posts, so the function is idempotent and works the same in both
|
|
686
|
+
-- directions — undoing a matching lowers the share and books the mirror.
|
|
687
|
+
--
|
|
688
|
+
-- The share is cumulative and rounded once against the whole, never payment by
|
|
689
|
+
-- payment: three thirds of 210,00 come to 70,00 + 70,00 + 70,00, and a tax of
|
|
690
|
+
-- 0,63 settled in halves comes to 0,32 then 0,31. The last matching always
|
|
691
|
+
-- carries the remainder because at full settlement the share is the whole.
|
|
692
|
+
--
|
|
693
|
+
-- The base of the operation travels with its tax. A cash-basis return declares
|
|
694
|
+
-- the base collected, so the base line of the document waits too — with no
|
|
695
|
+
-- ledger movement, because revenue was earned when it was invoiced. That is
|
|
696
|
+
-- what a line with a box and no amount is: a figure on a declaration, which
|
|
697
|
+
-- this schema has kept apart from a figure in the ledger since the tax engine.
|
|
698
|
+
-- ---------------------------------------------------------------------------
|
|
699
|
+
|
|
700
|
+
create or replace function settle_cash_basis_tax(
|
|
701
|
+
p_document_id uuid,
|
|
702
|
+
p_date date
|
|
703
|
+
)
|
|
704
|
+
returns uuid
|
|
705
|
+
language plpgsql
|
|
706
|
+
as $$
|
|
707
|
+
declare
|
|
708
|
+
v_doc documents%rowtype;
|
|
709
|
+
v_company companies%rowtype;
|
|
710
|
+
v_entry entries%rowtype;
|
|
711
|
+
v_kind tax_document_kind;
|
|
712
|
+
v_total numeric(16, 2);
|
|
713
|
+
v_paid numeric(16, 2);
|
|
714
|
+
v_ratio numeric;
|
|
715
|
+
v_seq integer := 0;
|
|
716
|
+
v_final uuid;
|
|
717
|
+
v_box text;
|
|
718
|
+
v_target uuid;
|
|
719
|
+
v_due numeric(16, 2);
|
|
720
|
+
v_done numeric(16, 2);
|
|
721
|
+
v_delta numeric(16, 2);
|
|
722
|
+
v_box_due numeric(16, 2);
|
|
723
|
+
v_box_done numeric(16, 2);
|
|
724
|
+
v_box_delta numeric(16, 2);
|
|
725
|
+
v_credit boolean;
|
|
726
|
+
w record;
|
|
727
|
+
begin
|
|
728
|
+
select * into v_doc from documents where id = p_document_id;
|
|
729
|
+
if not found or v_doc.entry_id is null then
|
|
730
|
+
return null;
|
|
731
|
+
end if;
|
|
732
|
+
|
|
733
|
+
-- Nothing is waiting on this document: the answer for every tax that falls
|
|
734
|
+
-- due when it is invoiced, which is most of them.
|
|
735
|
+
if not exists (
|
|
736
|
+
select 1
|
|
737
|
+
from entry_lines q join taxes t on t.id = q.tax_id
|
|
738
|
+
where q.entry_id = v_doc.entry_id
|
|
739
|
+
and t.cash_basis
|
|
740
|
+
and q.declaration_box is null
|
|
741
|
+
and q.box_amount is not null
|
|
742
|
+
) then
|
|
743
|
+
return null;
|
|
744
|
+
end if;
|
|
745
|
+
|
|
746
|
+
select * into v_company from companies where id = v_doc.company_id;
|
|
747
|
+
if v_company.miscellaneous_journal_id is null then
|
|
748
|
+
raise exception 'no_miscellaneous_journal: company % has no journal for the transfer of a cash-basis tax',
|
|
749
|
+
v_doc.company_id;
|
|
750
|
+
end if;
|
|
751
|
+
|
|
752
|
+
v_kind := case when v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note')
|
|
753
|
+
then 'credit_note' else 'invoice' end::tax_document_kind;
|
|
754
|
+
|
|
755
|
+
-- What share of this document has been settled, read on its own third-party
|
|
756
|
+
-- lines and in the ledger's currency on both sides of the division.
|
|
757
|
+
select coalesce(sum(abs(tl.debit - tl.credit)), 0), coalesce(sum(tl.matched_amount), 0)
|
|
758
|
+
into v_total, v_paid
|
|
759
|
+
from entry_lines tl join accounts a on a.id = tl.account_id
|
|
760
|
+
where tl.entry_id = v_doc.entry_id
|
|
761
|
+
and a.reconcilable
|
|
762
|
+
and a.account_type in ('asset_receivable', 'liability_payable');
|
|
763
|
+
|
|
764
|
+
if v_total = 0 then
|
|
765
|
+
return null;
|
|
766
|
+
end if;
|
|
767
|
+
-- An overpayment settles the document, and no more: a tax is due on what
|
|
768
|
+
-- was invoiced.
|
|
769
|
+
v_ratio := least(1, greatest(0, v_paid / v_total));
|
|
770
|
+
|
|
771
|
+
for w in
|
|
772
|
+
select l2.id, l2.account_id, l2.tax_id, l2.tax_line, l2.box_amount,
|
|
773
|
+
l2.debit, l2.credit, l2.name, l2.sequence
|
|
774
|
+
from entry_lines l2 join taxes t on t.id = l2.tax_id
|
|
775
|
+
where l2.entry_id = v_doc.entry_id
|
|
776
|
+
and t.cash_basis
|
|
777
|
+
and l2.declaration_box is null
|
|
778
|
+
and l2.box_amount is not null
|
|
779
|
+
order by l2.sequence
|
|
780
|
+
loop
|
|
781
|
+
select tp.account_id, tp.declaration_box
|
|
782
|
+
into v_final, v_box
|
|
783
|
+
from tax_postings tp
|
|
784
|
+
where tp.tax_id = w.tax_id
|
|
785
|
+
and tp.document_kind = v_kind
|
|
786
|
+
and tp.posting_type = (case when w.tax_line then 'tax' else 'base' end)::tax_posting_type
|
|
787
|
+
limit 1;
|
|
788
|
+
|
|
789
|
+
-- The account the transfer of this line lands on, which is also how an
|
|
790
|
+
-- earlier transfer of the same line is recognised.
|
|
791
|
+
v_target := case when w.tax_line then v_final else w.account_id end;
|
|
792
|
+
if v_target is null then
|
|
793
|
+
raise exception 'no_cash_basis_target: the tax of line % names no account to fall due on', w.id;
|
|
794
|
+
end if;
|
|
795
|
+
-- A line only waits because a box was worked out for it, and that box came
|
|
796
|
+
-- from this very posting. If it has none, the amount would wait for ever.
|
|
797
|
+
if v_box is null then
|
|
798
|
+
raise exception 'no_cash_basis_box: the tax of line % holds an amount for a box the posting does not name', w.id;
|
|
799
|
+
end if;
|
|
800
|
+
|
|
801
|
+
if w.tax_line then
|
|
802
|
+
v_due := round((w.debit + w.credit) * v_ratio, 2);
|
|
803
|
+
select coalesce(sum(case when w.credit > 0 then x.credit - x.debit
|
|
804
|
+
else x.debit - x.credit end), 0)
|
|
805
|
+
into v_done
|
|
806
|
+
from entry_lines x join entries e on e.id = x.entry_id
|
|
807
|
+
where e.document_id = p_document_id
|
|
808
|
+
and e.id <> v_doc.entry_id
|
|
809
|
+
and x.tax_id = w.tax_id
|
|
810
|
+
and x.tax_line
|
|
811
|
+
and x.account_id = v_target;
|
|
812
|
+
else
|
|
813
|
+
v_due := 0;
|
|
814
|
+
v_done := 0;
|
|
815
|
+
end if;
|
|
816
|
+
v_delta := v_due - v_done;
|
|
817
|
+
|
|
818
|
+
v_box_due := round(w.box_amount * v_ratio, 2);
|
|
819
|
+
select coalesce(sum(x.box_amount), 0)
|
|
820
|
+
into v_box_done
|
|
821
|
+
from entry_lines x join entries e on e.id = x.entry_id
|
|
822
|
+
where e.document_id = p_document_id
|
|
823
|
+
and e.id <> v_doc.entry_id
|
|
824
|
+
and x.tax_id = w.tax_id
|
|
825
|
+
and x.tax_line = w.tax_line
|
|
826
|
+
and x.account_id = v_target;
|
|
827
|
+
v_box_delta := v_box_due - v_box_done;
|
|
828
|
+
|
|
829
|
+
if v_delta = 0 and v_box_delta = 0 then
|
|
830
|
+
continue;
|
|
831
|
+
end if;
|
|
832
|
+
|
|
833
|
+
if v_entry.id is null then
|
|
834
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
|
|
835
|
+
description, state, document_id, currency_code)
|
|
836
|
+
values (v_doc.company_id, v_company.miscellaneous_journal_id,
|
|
837
|
+
fiscal_year_at(v_doc.company_id, p_date), p_date,
|
|
838
|
+
v_doc.number,
|
|
839
|
+
coalesce(v_doc.number, 'document') || ' — tax due on settlement',
|
|
840
|
+
'draft', p_document_id, v_company.currency_code)
|
|
841
|
+
returning * into v_entry;
|
|
842
|
+
end if;
|
|
843
|
+
|
|
844
|
+
-- The side the document put the tax on, kept when the share grows and
|
|
845
|
+
-- flipped when it shrinks.
|
|
846
|
+
v_credit := (w.credit > 0) = (v_delta > 0);
|
|
847
|
+
|
|
848
|
+
v_seq := v_seq + 10;
|
|
849
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
850
|
+
debit, credit, tax_id, tax_line,
|
|
851
|
+
declaration_box, box_amount, currency_code)
|
|
852
|
+
values (v_entry.id, v_doc.company_id, v_target, v_seq, w.name,
|
|
853
|
+
case when v_credit then 0 else abs(v_delta) end,
|
|
854
|
+
case when v_credit then abs(v_delta) else 0 end,
|
|
855
|
+
w.tax_id, w.tax_line,
|
|
856
|
+
v_box, v_box_delta,
|
|
857
|
+
v_company.currency_code);
|
|
858
|
+
|
|
859
|
+
if v_delta <> 0 then
|
|
860
|
+
v_seq := v_seq + 10;
|
|
861
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
862
|
+
debit, credit, tax_id, tax_line, currency_code)
|
|
863
|
+
values (v_entry.id, v_doc.company_id, w.account_id, v_seq, w.name,
|
|
864
|
+
case when v_credit then abs(v_delta) else 0 end,
|
|
865
|
+
case when v_credit then 0 else abs(v_delta) end,
|
|
866
|
+
w.tax_id, true, v_company.currency_code);
|
|
867
|
+
end if;
|
|
868
|
+
end loop;
|
|
869
|
+
|
|
870
|
+
if v_entry.id is null then
|
|
871
|
+
return null;
|
|
872
|
+
end if;
|
|
873
|
+
|
|
874
|
+
v_entry := post_entry(v_entry.id);
|
|
875
|
+
return v_entry.id;
|
|
876
|
+
end;
|
|
877
|
+
$$;
|
|
878
|
+
|
|
879
|
+
comment on function settle_cash_basis_tax(uuid, date) is
|
|
880
|
+
'Moves the share of a cash-basis tax that settlement has made due, from the transition account to the account and the box it is declared on. Derived from the ledger, so it is the same call whether a matching was made or undone.';
|
|
881
|
+
|
|
882
|
+
-- ---------------------------------------------------------------------------
|
|
883
|
+
-- reconcile — the same matching, plus what the matching reveals
|
|
884
|
+
--
|
|
885
|
+
-- Two lines in the same currency, and that currency is not the company's: the
|
|
886
|
+
-- matching is worked out in *their* currency, because that is where they are
|
|
887
|
+
-- equal, and each side turns it back into the company's at the rate it was
|
|
888
|
+
-- booked with. The two figures differ by the movement of the rate, and that
|
|
889
|
+
-- difference is realised the moment the money arrives. It is booked on the
|
|
890
|
+
-- accounts the pack names, on the third-party account so that account goes to
|
|
891
|
+
-- nil, and matched under the same letter.
|
|
892
|
+
--
|
|
893
|
+
-- `p_amount` is read in the currency the two lines share, which is the
|
|
894
|
+
-- currency an operator is looking at when a dollar invoice is settled by a
|
|
895
|
+
-- dollar payment. Nothing else changes: two lines in the company's currency
|
|
896
|
+
-- are matched exactly as before, and every line booked before this migration
|
|
897
|
+
-- carries no currency amount at all.
|
|
898
|
+
--
|
|
899
|
+
-- The unmatched residual that a rate creates has no other way out. Leaving it
|
|
900
|
+
-- on the receivable would mean a customer who has paid in full still owes a
|
|
901
|
+
-- few cents of a currency they never used.
|
|
902
|
+
-- ---------------------------------------------------------------------------
|
|
903
|
+
|
|
904
|
+
create or replace function reconcile(
|
|
905
|
+
p_line_a uuid,
|
|
906
|
+
p_line_b uuid,
|
|
907
|
+
p_amount numeric default null
|
|
908
|
+
)
|
|
909
|
+
returns reconciliations
|
|
910
|
+
language plpgsql
|
|
911
|
+
as $$
|
|
912
|
+
declare
|
|
913
|
+
v_a entry_lines%rowtype;
|
|
914
|
+
v_b entry_lines%rowtype;
|
|
915
|
+
v_debit entry_lines%rowtype;
|
|
916
|
+
v_credit entry_lines%rowtype;
|
|
917
|
+
v_amount numeric(16, 2);
|
|
918
|
+
v_letter text;
|
|
919
|
+
v_result reconciliations%rowtype;
|
|
920
|
+
v_reconcilable boolean;
|
|
921
|
+
v_home char(3);
|
|
922
|
+
v_country char(2);
|
|
923
|
+
v_journal uuid;
|
|
924
|
+
v_fx boolean := false;
|
|
925
|
+
v_open_d numeric(16, 2);
|
|
926
|
+
v_open_c numeric(16, 2);
|
|
927
|
+
v_cur_d numeric(16, 2);
|
|
928
|
+
v_cur_c numeric(16, 2);
|
|
929
|
+
v_cur numeric(16, 2);
|
|
930
|
+
v_comp_d numeric(16, 2);
|
|
931
|
+
v_comp_c numeric(16, 2);
|
|
932
|
+
v_gap numeric(16, 2) := 0;
|
|
933
|
+
v_gain uuid;
|
|
934
|
+
v_loss uuid;
|
|
935
|
+
v_result_account uuid;
|
|
936
|
+
v_fx_entry entries%rowtype;
|
|
937
|
+
v_fx_line uuid;
|
|
938
|
+
v_date date;
|
|
939
|
+
v_transfer uuid;
|
|
940
|
+
v_doc uuid;
|
|
941
|
+
begin
|
|
942
|
+
select * into v_a from entry_lines where id = p_line_a for update;
|
|
943
|
+
if not found then raise exception 'unknown_entry_line: %', p_line_a; end if;
|
|
944
|
+
select * into v_b from entry_lines where id = p_line_b for update;
|
|
945
|
+
if not found then raise exception 'unknown_entry_line: %', p_line_b; end if;
|
|
946
|
+
|
|
947
|
+
if v_a.account_id <> v_b.account_id then
|
|
948
|
+
raise exception 'reconcile_account_mismatch: lines are on different accounts';
|
|
949
|
+
end if;
|
|
950
|
+
if v_a.company_id <> v_b.company_id then
|
|
951
|
+
raise exception 'reconcile_company_mismatch: lines belong to different companies';
|
|
952
|
+
end if;
|
|
953
|
+
|
|
954
|
+
select a.reconcilable into v_reconcilable from accounts a where a.id = v_a.account_id;
|
|
955
|
+
if not v_reconcilable then
|
|
956
|
+
raise exception 'account_not_reconcilable: account of line % is not reconcilable', p_line_a;
|
|
957
|
+
end if;
|
|
958
|
+
|
|
959
|
+
if (v_a.debit > 0) = (v_b.debit > 0) then
|
|
960
|
+
raise exception 'reconcile_same_side: a debit must be matched against a credit';
|
|
961
|
+
end if;
|
|
962
|
+
|
|
963
|
+
if v_a.debit > 0 then
|
|
964
|
+
v_debit := v_a; v_credit := v_b;
|
|
965
|
+
else
|
|
966
|
+
v_debit := v_b; v_credit := v_a;
|
|
967
|
+
end if;
|
|
968
|
+
|
|
969
|
+
select c.currency_code, c.country, c.miscellaneous_journal_id
|
|
970
|
+
into v_home, v_country, v_journal
|
|
971
|
+
from companies c where c.id = v_debit.company_id;
|
|
972
|
+
|
|
973
|
+
v_open_d := abs(v_debit.debit - v_debit.credit) - v_debit.matched_amount;
|
|
974
|
+
v_open_c := abs(v_credit.debit - v_credit.credit) - v_credit.matched_amount;
|
|
975
|
+
|
|
976
|
+
v_fx := v_debit.currency_code is not null
|
|
977
|
+
and v_debit.currency_code = v_credit.currency_code
|
|
978
|
+
and v_debit.currency_code <> v_home
|
|
979
|
+
and coalesce(v_debit.amount_currency, 0) <> 0
|
|
980
|
+
and coalesce(v_credit.amount_currency, 0) <> 0;
|
|
981
|
+
|
|
982
|
+
if v_fx then
|
|
983
|
+
-- What is still open on each line, in its own currency, in proportion to
|
|
984
|
+
-- what is still open in the ledger.
|
|
985
|
+
v_cur_d := round(v_debit.amount_currency * v_open_d
|
|
986
|
+
/ nullif(abs(v_debit.debit - v_debit.credit), 0), 2);
|
|
987
|
+
v_cur_c := round(v_credit.amount_currency * v_open_c
|
|
988
|
+
/ nullif(abs(v_credit.debit - v_credit.credit), 0), 2);
|
|
989
|
+
v_cur := coalesce(p_amount, least(v_cur_d, v_cur_c));
|
|
990
|
+
|
|
991
|
+
if v_cur is null or v_cur <= 0 then
|
|
992
|
+
raise exception 'reconcile_nothing_left: no open amount to match';
|
|
993
|
+
end if;
|
|
994
|
+
if v_cur > v_cur_d + 0.001 or v_cur > v_cur_c + 0.001 then
|
|
995
|
+
raise exception 'reconcile_over_currency: % exceeds what is open in %',
|
|
996
|
+
v_cur, v_debit.currency_code;
|
|
997
|
+
end if;
|
|
998
|
+
|
|
999
|
+
v_comp_d := round(v_cur * abs(v_debit.debit - v_debit.credit) / v_debit.amount_currency, 2);
|
|
1000
|
+
v_comp_c := round(v_cur * abs(v_credit.debit - v_credit.credit) / v_credit.amount_currency, 2);
|
|
1001
|
+
v_amount := least(v_comp_d, v_comp_c);
|
|
1002
|
+
v_gap := v_comp_d - v_comp_c;
|
|
1003
|
+
else
|
|
1004
|
+
v_amount := coalesce(p_amount, least(v_open_d, v_open_c));
|
|
1005
|
+
end if;
|
|
1006
|
+
|
|
1007
|
+
if v_amount is null or v_amount <= 0 then
|
|
1008
|
+
raise exception 'reconcile_nothing_left: no open amount to match';
|
|
1009
|
+
end if;
|
|
1010
|
+
if v_amount > v_open_d + 0.001 then
|
|
1011
|
+
raise exception 'reconcile_over_debit: % exceeds the open amount of the debit line', v_amount;
|
|
1012
|
+
end if;
|
|
1013
|
+
if v_amount > v_open_c + 0.001 then
|
|
1014
|
+
raise exception 'reconcile_over_credit: % exceeds the open amount of the credit line', v_amount;
|
|
1015
|
+
end if;
|
|
1016
|
+
|
|
1017
|
+
-- Reuse a letter already carried by either side, otherwise draw a new one.
|
|
1018
|
+
v_letter := coalesce(v_debit.matching_number, v_credit.matching_number,
|
|
1019
|
+
next_matching_number(v_debit.company_id));
|
|
1020
|
+
|
|
1021
|
+
-- Two chains meeting: merge them under a single letter.
|
|
1022
|
+
if v_debit.matching_number is not null
|
|
1023
|
+
and v_credit.matching_number is not null
|
|
1024
|
+
and v_debit.matching_number <> v_credit.matching_number then
|
|
1025
|
+
update reconciliations
|
|
1026
|
+
set matching_number = v_letter
|
|
1027
|
+
where company_id = v_debit.company_id
|
|
1028
|
+
and matching_number = v_credit.matching_number;
|
|
1029
|
+
end if;
|
|
1030
|
+
|
|
1031
|
+
-- The date the settlement is complete on: the later of the two entries.
|
|
1032
|
+
select greatest(ed.entry_date, ec.entry_date) into v_date
|
|
1033
|
+
from entries ed, entries ec
|
|
1034
|
+
where ed.id = v_debit.entry_id and ec.id = v_credit.entry_id;
|
|
1035
|
+
|
|
1036
|
+
-- ------------------------------------------------------- exchange difference
|
|
1037
|
+
if v_gap <> 0 then
|
|
1038
|
+
select account_id_by_code(v_debit.company_id, cd.fx_gain_code),
|
|
1039
|
+
account_id_by_code(v_debit.company_id, cd.fx_loss_code)
|
|
1040
|
+
into v_gain, v_loss
|
|
1041
|
+
from country_defaults cd where cd.country = v_country;
|
|
1042
|
+
|
|
1043
|
+
-- A debit side short of the credit side means more of the company's money
|
|
1044
|
+
-- came in than the receivable was booked at: a gain.
|
|
1045
|
+
v_result_account := case when v_gap < 0 then v_gain else v_loss end;
|
|
1046
|
+
if v_result_account is null then
|
|
1047
|
+
raise exception 'no_fx_accounts: matching % with % realises % and the country model names no exchange gain and loss account',
|
|
1048
|
+
p_line_a, p_line_b, v_gap;
|
|
1049
|
+
end if;
|
|
1050
|
+
if v_journal is null then
|
|
1051
|
+
raise exception 'no_miscellaneous_journal: company % has no journal for an exchange difference',
|
|
1052
|
+
v_debit.company_id;
|
|
1053
|
+
end if;
|
|
1054
|
+
|
|
1055
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date,
|
|
1056
|
+
description, state, currency_code)
|
|
1057
|
+
values (v_debit.company_id, v_journal, fiscal_year_at(v_debit.company_id, v_date), v_date,
|
|
1058
|
+
'Exchange difference on ' || v_letter, 'draft', v_home)
|
|
1059
|
+
returning * into v_fx_entry;
|
|
1060
|
+
|
|
1061
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
1062
|
+
debit, credit, contact_id, currency_code)
|
|
1063
|
+
values (v_fx_entry.id, v_debit.company_id, v_debit.account_id, 10,
|
|
1064
|
+
'Exchange difference',
|
|
1065
|
+
case when v_gap < 0 then abs(v_gap) else 0 end,
|
|
1066
|
+
case when v_gap < 0 then 0 else abs(v_gap) end,
|
|
1067
|
+
coalesce(v_debit.contact_id, v_credit.contact_id), v_home)
|
|
1068
|
+
returning id into v_fx_line;
|
|
1069
|
+
|
|
1070
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
1071
|
+
debit, credit, currency_code)
|
|
1072
|
+
values (v_fx_entry.id, v_debit.company_id, v_result_account, 20,
|
|
1073
|
+
'Exchange difference',
|
|
1074
|
+
case when v_gap < 0 then 0 else abs(v_gap) end,
|
|
1075
|
+
case when v_gap < 0 then abs(v_gap) else 0 end,
|
|
1076
|
+
v_home);
|
|
1077
|
+
|
|
1078
|
+
v_fx_entry := post_entry(v_fx_entry.id);
|
|
1079
|
+
|
|
1080
|
+
-- And it is matched with the side it completes, under the same letter.
|
|
1081
|
+
if v_gap < 0 then
|
|
1082
|
+
insert into reconciliations (company_id, debit_line_id, credit_line_id, amount, matching_number)
|
|
1083
|
+
values (v_debit.company_id, v_fx_line, v_credit.id, abs(v_gap), v_letter);
|
|
1084
|
+
else
|
|
1085
|
+
insert into reconciliations (company_id, debit_line_id, credit_line_id, amount, matching_number)
|
|
1086
|
+
values (v_debit.company_id, v_debit.id, v_fx_line, abs(v_gap), v_letter);
|
|
1087
|
+
end if;
|
|
1088
|
+
end if;
|
|
1089
|
+
|
|
1090
|
+
insert into reconciliations (company_id, debit_line_id, credit_line_id, amount,
|
|
1091
|
+
matching_number, fx_entry_id)
|
|
1092
|
+
values (v_debit.company_id, v_debit.id, v_credit.id, v_amount, v_letter, v_fx_entry.id)
|
|
1093
|
+
returning * into v_result;
|
|
1094
|
+
|
|
1095
|
+
-- ------------------------------------------------------- a tax falling due
|
|
1096
|
+
for v_doc in
|
|
1097
|
+
select distinct d.id
|
|
1098
|
+
from entry_lines l join documents d on d.entry_id = l.entry_id
|
|
1099
|
+
where l.id in (v_debit.id, v_credit.id)
|
|
1100
|
+
loop
|
|
1101
|
+
v_transfer := coalesce(settle_cash_basis_tax(v_doc, v_date), v_transfer);
|
|
1102
|
+
end loop;
|
|
1103
|
+
|
|
1104
|
+
if v_transfer is not null then
|
|
1105
|
+
update reconciliations set tax_transfer_entry_id = v_transfer where id = v_result.id
|
|
1106
|
+
returning * into v_result;
|
|
1107
|
+
end if;
|
|
1108
|
+
|
|
1109
|
+
return v_result;
|
|
1110
|
+
end;
|
|
1111
|
+
$$;
|
|
1112
|
+
|
|
1113
|
+
comment on function reconcile(uuid, uuid, numeric) is
|
|
1114
|
+
'Matches a debit line against a credit line, in the currency the two share when it is not the company''s, and books what the matching reveals: the realised exchange difference, and the share of a cash-basis tax that has become due.';
|
|
1115
|
+
|
|
1116
|
+
-- ---------------------------------------------------------------------------
|
|
1117
|
+
-- unreconcile — and what has to be taken back with the matching
|
|
1118
|
+
-- ---------------------------------------------------------------------------
|
|
1119
|
+
|
|
1120
|
+
create or replace function unreconcile(p_reconciliation_id uuid)
|
|
1121
|
+
returns void
|
|
1122
|
+
language plpgsql
|
|
1123
|
+
as $$
|
|
1124
|
+
declare
|
|
1125
|
+
v_row reconciliations%rowtype;
|
|
1126
|
+
v_date date;
|
|
1127
|
+
v_fx entries%rowtype;
|
|
1128
|
+
v_mirror entries%rowtype;
|
|
1129
|
+
v_doc uuid;
|
|
1130
|
+
fl record;
|
|
1131
|
+
begin
|
|
1132
|
+
select * into v_row from reconciliations where id = p_reconciliation_id;
|
|
1133
|
+
if not found then
|
|
1134
|
+
raise exception 'unknown_reconciliation: %', p_reconciliation_id;
|
|
1135
|
+
end if;
|
|
1136
|
+
|
|
1137
|
+
select greatest(ed.entry_date, ec.entry_date) into v_date
|
|
1138
|
+
from entry_lines ld join entries ed on ed.id = ld.entry_id,
|
|
1139
|
+
entry_lines lc join entries ec on ec.id = lc.entry_id
|
|
1140
|
+
where ld.id = v_row.debit_line_id and lc.id = v_row.credit_line_id;
|
|
1141
|
+
|
|
1142
|
+
delete from reconciliations where id = p_reconciliation_id;
|
|
1143
|
+
|
|
1144
|
+
-- An exchange difference was realised by this matching and by nothing else,
|
|
1145
|
+
-- so undoing the matching takes it back: its own matching goes, and a mirror
|
|
1146
|
+
-- entry cancels it on the same date. The period has to be open, which is the
|
|
1147
|
+
-- honest refusal — a difference that has been declared is not unmade quietly.
|
|
1148
|
+
if v_row.fx_entry_id is not null then
|
|
1149
|
+
select * into v_fx from entries where id = v_row.fx_entry_id;
|
|
1150
|
+
|
|
1151
|
+
delete from reconciliations r
|
|
1152
|
+
where r.company_id = v_row.company_id
|
|
1153
|
+
and (r.debit_line_id in (select id from entry_lines where entry_id = v_fx.id)
|
|
1154
|
+
or r.credit_line_id in (select id from entry_lines where entry_id = v_fx.id));
|
|
1155
|
+
|
|
1156
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date,
|
|
1157
|
+
description, state, currency_code, reversed_entry_id)
|
|
1158
|
+
values (v_fx.company_id, v_fx.journal_id, v_fx.fiscal_year_id, v_fx.entry_date,
|
|
1159
|
+
'Exchange difference undone', 'draft', v_fx.currency_code, v_fx.id)
|
|
1160
|
+
returning * into v_mirror;
|
|
1161
|
+
|
|
1162
|
+
for fl in select * from entry_lines where entry_id = v_fx.id order by sequence loop
|
|
1163
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
1164
|
+
debit, credit, contact_id, currency_code)
|
|
1165
|
+
values (v_mirror.id, fl.company_id, fl.account_id, fl.sequence, fl.name,
|
|
1166
|
+
fl.credit, fl.debit, fl.contact_id, fl.currency_code);
|
|
1167
|
+
end loop;
|
|
1168
|
+
|
|
1169
|
+
perform post_entry(v_mirror.id);
|
|
1170
|
+
end if;
|
|
1171
|
+
|
|
1172
|
+
-- The share of a cash-basis tax that is due has just fallen; the same
|
|
1173
|
+
-- function books the difference, which is now the other way round.
|
|
1174
|
+
for v_doc in
|
|
1175
|
+
select distinct d.id
|
|
1176
|
+
from entry_lines l join documents d on d.entry_id = l.entry_id
|
|
1177
|
+
where l.id in (v_row.debit_line_id, v_row.credit_line_id)
|
|
1178
|
+
loop
|
|
1179
|
+
perform settle_cash_basis_tax(v_doc, v_date);
|
|
1180
|
+
end loop;
|
|
1181
|
+
end;
|
|
1182
|
+
$$;
|
|
1183
|
+
|
|
1184
|
+
comment on function unreconcile(uuid) is
|
|
1185
|
+
'Undoes a matching, and with it what the matching had booked: the exchange difference it realised and the share of a cash-basis tax it had made due.';
|
|
1186
|
+
|
|
1187
|
+
-- Rule 6 of supabase/migrations/README.md: a function created here comes out
|
|
1188
|
+
-- executable by PUBLIC otherwise.
|
|
1189
|
+
revoke execute on all functions in schema public from public;
|