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,776 @@
|
|
|
1
|
+
-- Ekwo OS — one tax engine, several kinds.
|
|
2
|
+
--
|
|
3
|
+
-- The core knew one tax: European VAT, fully deductible, computed on a price
|
|
4
|
+
-- that excludes it, rounded to the cent. Everything else a country does was
|
|
5
|
+
-- unsayable, and the pack format already had words for it: `kind`,
|
|
6
|
+
-- `recoverable`, `price_include`, `jurisdiction`, `cash_basis`,
|
|
7
|
+
-- `rounding_method` and `cash_rounding_unit` are in `packs/schema/pack.1.json`
|
|
8
|
+
-- today, marked *deferred*, and the compiler drops them on the floor. This
|
|
9
|
+
-- migration gives them a column, so the pack stops promising what the schema
|
|
10
|
+
-- cannot hold.
|
|
11
|
+
--
|
|
12
|
+
-- What each one is for, and what it is *not*:
|
|
13
|
+
--
|
|
14
|
+
-- `tax_kind` vat | gst | sales_tax | withholding | other. A **label that
|
|
15
|
+
-- drives the reports**, never the calculation. GST is
|
|
16
|
+
-- computed exactly like VAT; a report that lists "the VAT" of
|
|
17
|
+
-- a Canadian company needs to know which of two taxes it is
|
|
18
|
+
-- looking at, and guessing from the code is how a localisation
|
|
19
|
+
-- ends up in application code. Default `vat`, which is what
|
|
20
|
+
-- every tax of Belgium and France is.
|
|
21
|
+
-- `recoverable` false for an American sales tax and for a Canadian PST: the
|
|
22
|
+
-- buyer never gets it back. Documentation and a report filter;
|
|
23
|
+
-- the *ledger* consequence of non-recoverability is the
|
|
24
|
+
-- `tax_on_base` posting below, which says where the money
|
|
25
|
+
-- goes rather than merely that it is lost.
|
|
26
|
+
-- `price_include` the unit price already holds the tax (UK and Australian
|
|
27
|
+
-- retail). `taxes` had it since day one and `tax_templates`
|
|
28
|
+
-- did not, so a pack could not express it — a column missing
|
|
29
|
+
-- on one side of a copy. Added here, nothing reads it yet;
|
|
30
|
+
-- the gross-to-net computation belongs with the country that
|
|
31
|
+
-- needs it.
|
|
32
|
+
-- `jurisdiction` ISO 3166-2 *with* the country prefix — `CA-QC`, `US-CA` —
|
|
33
|
+
-- because a sales tax is levied by a state, not by a country.
|
|
34
|
+
-- Null in Europe. Distinct from `companies.region`, which is
|
|
35
|
+
-- where a *party* sits and carries no prefix.
|
|
36
|
+
-- `cash_basis` and `cash_basis_transition_account_*`: **columns only, and
|
|
37
|
+
-- deliberately unread**. The tax falls due when the invoice
|
|
38
|
+
-- is paid — French VAT on services, the British cash
|
|
39
|
+
-- accounting scheme — and the amount waits on a transition
|
|
40
|
+
-- account until then. P0-6 implements the behaviour; putting
|
|
41
|
+
-- the columns here means `taxes` and `tax_postings` migrate
|
|
42
|
+
-- once rather than twice, which is the same argument
|
|
43
|
+
-- `20260912080311` made for `report_code`.
|
|
44
|
+
--
|
|
45
|
+
-- And on the country model, two columns the manifest already declares:
|
|
46
|
+
--
|
|
47
|
+
-- `rounding_method` half_up | half_even | down | up. Belgium and France
|
|
48
|
+
-- both round half away from zero, per tax group, which is
|
|
49
|
+
-- what `round()` on a numeric does and what `post_document`
|
|
50
|
+
-- has always done — so `half_up` is today's behaviour written
|
|
51
|
+
-- down, not a change. Half-even is the Anglo-Saxon banker's
|
|
52
|
+
-- rounding.
|
|
53
|
+
-- `cash_rounding_unit` the smallest coin, when it is not the cent: 0.05 in
|
|
54
|
+
-- Switzerland, where an invoice total is rounded to the
|
|
55
|
+
-- nearest five centimes and the difference goes to the
|
|
56
|
+
-- rounding account. 0 means the cent is the unit, which is
|
|
57
|
+
-- every country of phase 0.
|
|
58
|
+
--
|
|
59
|
+
-- These two, like `cash_basis`, have no reader yet. That is the third and
|
|
60
|
+
-- last time this schema takes that liberty (`region` was the first), and for
|
|
61
|
+
-- the reason `20260912080311` set out: the alternative is migrating a table
|
|
62
|
+
-- of tax rows twice. `post_document` is unchanged for every tax that exists
|
|
63
|
+
-- today, and the tests that prove it are untouched.
|
|
64
|
+
--
|
|
65
|
+
-- ---------------------------------------------------------------------------
|
|
66
|
+
-- The one behaviour this migration does add: `tax_on_base`
|
|
67
|
+
-- ---------------------------------------------------------------------------
|
|
68
|
+
--
|
|
69
|
+
-- Non-deductible VAT is not a claim on the State, so it cannot sit on a VAT
|
|
70
|
+
-- account. It is a cost, and it follows the account of the thing that was
|
|
71
|
+
-- bought. A Belgian company car, 1 000 € at 21 %, deduction capped at 50 % by
|
|
72
|
+
-- art. 45 § 2 CTVA:
|
|
73
|
+
--
|
|
74
|
+
-- 242000 Matériel roulant debit 1 000.00 box 83 1 000.00
|
|
75
|
+
-- 411000 TVA déductible debit 105.00 box 59 105.00
|
|
76
|
+
-- 242000 TVA non déductible debit 105.00 box 83 105.00
|
|
77
|
+
-- 440000 Fournisseur credit 1 210.00
|
|
78
|
+
--
|
|
79
|
+
-- The supplier is owed the full 1 210 €; half the VAT is recovered and half
|
|
80
|
+
-- is part of what the car cost. Grid 83 comes out at 1 105 €, because the
|
|
81
|
+
-- Belgian return asks for the base *plus* the non-deductible VAT — the form
|
|
82
|
+
-- says « montant (TVA déductible non comprise) », which excludes the
|
|
83
|
+
-- deductible VAT and not all of it. That falls out of `box_factor_percent`
|
|
84
|
+
-- being independent from `factor_percent`, with no new column: the base
|
|
85
|
+
-- posting reports 100 % of the base in box 83 and the `tax_on_base` posting
|
|
86
|
+
-- reports 50 % of the tax in the same box.
|
|
87
|
+
--
|
|
88
|
+
-- Three rules the posting keeps:
|
|
89
|
+
--
|
|
90
|
+
-- * **It carries no account**, exactly like `base`, and for the same
|
|
91
|
+
-- reason: the account is the one the document line names. The check
|
|
92
|
+
-- constraint says so.
|
|
93
|
+
-- * **It is split across the accounts of the lines it taxes**, in
|
|
94
|
+
-- proportion to their base, with the last share carrying the rounding
|
|
95
|
+
-- difference. An invoice with a car and its insurance on two accounts
|
|
96
|
+
-- gets the non-deductible VAT on both, and the entry still balances to
|
|
97
|
+
-- the cent because the shares sum to an amount that was rounded **once**,
|
|
98
|
+
-- on the tax group (EN 16931 BR-CO-14, unchanged).
|
|
99
|
+
-- * **`tax_line` stays false.** The amount lands on a base account and
|
|
100
|
+
-- belongs to the base side of the grid — which is precisely why Belgium
|
|
101
|
+
-- puts it in 82/83. Marking it as a tax line would split grid 83 into two
|
|
102
|
+
-- rows in `vat_return()` and would tell every reader that filters on
|
|
103
|
+
-- `tax_line` to go looking for it among the VAT accounts, where it is not.
|
|
104
|
+
--
|
|
105
|
+
-- A fully non-deductible tax is the same thing at 100 %: one `tax_on_base`
|
|
106
|
+
-- posting, no `tax` posting, and `recoverable = false` to say it in one word.
|
|
107
|
+
|
|
108
|
+
-- ---------------------------------------------------------------------------
|
|
109
|
+
-- tax_kind
|
|
110
|
+
-- ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
do $$
|
|
113
|
+
begin
|
|
114
|
+
if not exists (select 1 from pg_type where typname = 'tax_kind') then
|
|
115
|
+
create type tax_kind as enum ('vat', 'gst', 'sales_tax', 'withholding', 'other');
|
|
116
|
+
end if;
|
|
117
|
+
end;
|
|
118
|
+
$$;
|
|
119
|
+
|
|
120
|
+
comment on type tax_kind is
|
|
121
|
+
'What kind of tax this is. Drives the reports and nothing else: a GST is computed exactly like a VAT.';
|
|
122
|
+
|
|
123
|
+
-- ---------------------------------------------------------------------------
|
|
124
|
+
-- rounding_method
|
|
125
|
+
-- ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
do $$
|
|
128
|
+
begin
|
|
129
|
+
if not exists (select 1 from pg_type where typname = 'rounding_method') then
|
|
130
|
+
create type rounding_method as enum ('half_up', 'half_even', 'down', 'up');
|
|
131
|
+
end if;
|
|
132
|
+
end;
|
|
133
|
+
$$;
|
|
134
|
+
|
|
135
|
+
comment on type rounding_method is
|
|
136
|
+
'How a country rounds a tax amount. half_up is half away from zero, which is what Postgres round() does and what every posting has always used.';
|
|
137
|
+
|
|
138
|
+
-- ---------------------------------------------------------------------------
|
|
139
|
+
-- The reference taxes
|
|
140
|
+
-- ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
alter table tax_templates
|
|
143
|
+
add column if not exists tax_kind tax_kind not null default 'vat',
|
|
144
|
+
add column if not exists recoverable boolean not null default true,
|
|
145
|
+
add column if not exists jurisdiction text,
|
|
146
|
+
add column if not exists price_include boolean not null default false,
|
|
147
|
+
add column if not exists cash_basis boolean not null default false,
|
|
148
|
+
add column if not exists cash_basis_transition_account_code text;
|
|
149
|
+
|
|
150
|
+
comment on column tax_templates.tax_kind is
|
|
151
|
+
'vat, gst, sales_tax, withholding, other. A label for the reports, never an input to the calculation.';
|
|
152
|
+
comment on column tax_templates.recoverable is
|
|
153
|
+
'False when the buyer never gets the tax back: American sales tax, Canadian PST. Where it lands is said by a tax_on_base posting.';
|
|
154
|
+
comment on column tax_templates.jurisdiction is
|
|
155
|
+
'ISO 3166-2 with the country prefix — CA-QC, US-CA — for a tax levied by a state. Null in Europe.';
|
|
156
|
+
comment on column tax_templates.price_include is
|
|
157
|
+
'The unit price already holds the tax (UK and Australian retail). `taxes` carried this from the start and the template did not.';
|
|
158
|
+
comment on column tax_templates.cash_basis is
|
|
159
|
+
'The tax falls due when the invoice is paid. Column only: P0-6 implements the behaviour, this migration just stops the pack from losing the value.';
|
|
160
|
+
comment on column tax_templates.cash_basis_transition_account_code is
|
|
161
|
+
'Account the tax waits on until the invoice is paid. Column only, read by P0-6.';
|
|
162
|
+
|
|
163
|
+
alter table taxes
|
|
164
|
+
add column if not exists tax_kind tax_kind not null default 'vat',
|
|
165
|
+
add column if not exists recoverable boolean not null default true,
|
|
166
|
+
add column if not exists jurisdiction text,
|
|
167
|
+
add column if not exists cash_basis boolean not null default false,
|
|
168
|
+
add column if not exists cash_basis_transition_account_id uuid references accounts(id) on delete restrict;
|
|
169
|
+
|
|
170
|
+
comment on column taxes.tax_kind is
|
|
171
|
+
'vat, gst, sales_tax, withholding, other. A label for the reports, never an input to the calculation.';
|
|
172
|
+
comment on column taxes.recoverable is
|
|
173
|
+
'False when the buyer never gets the tax back. The ledger consequence is a tax_on_base posting, not this column.';
|
|
174
|
+
comment on column taxes.jurisdiction is
|
|
175
|
+
'ISO 3166-2 with the country prefix, for a tax levied by a state. Null in Europe.';
|
|
176
|
+
comment on column taxes.cash_basis is
|
|
177
|
+
'The tax falls due when the invoice is paid. Column only until P0-6.';
|
|
178
|
+
comment on column taxes.cash_basis_transition_account_id is
|
|
179
|
+
'Account the tax waits on until the invoice is paid. Column only until P0-6.';
|
|
180
|
+
|
|
181
|
+
-- A tax belongs to a company and so does the account it points at; the
|
|
182
|
+
-- composite key is how every other reference in this schema says it.
|
|
183
|
+
alter table taxes
|
|
184
|
+
add constraint taxes_cash_basis_account_company
|
|
185
|
+
foreign key (cash_basis_transition_account_id, company_id) references accounts(id, company_id);
|
|
186
|
+
|
|
187
|
+
-- ---------------------------------------------------------------------------
|
|
188
|
+
-- Which posting type carries an account
|
|
189
|
+
--
|
|
190
|
+
-- Two constraints said it before, one per type, and neither had an opinion
|
|
191
|
+
-- about a type that did not exist. `tax_on_base` must carry no account — the
|
|
192
|
+
-- account is the line's — so the rule is now stated once, for every value of
|
|
193
|
+
-- the enum, and a value added later has to come back here rather than slip
|
|
194
|
+
-- through. This is the promised replacement of `tax_has_account`: a `tax`
|
|
195
|
+
-- still needs an account, and `tax_on_base` is exempt.
|
|
196
|
+
-- ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
alter table tax_postings
|
|
199
|
+
drop constraint if exists tax_postings_base_has_no_account,
|
|
200
|
+
drop constraint if exists tax_postings_tax_has_account;
|
|
201
|
+
|
|
202
|
+
alter table tax_postings
|
|
203
|
+
add constraint tax_postings_account_by_type check (
|
|
204
|
+
case posting_type when 'tax' then account_id is not null else account_id is null end
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
alter table tax_posting_templates
|
|
208
|
+
drop constraint if exists tax_posting_templates_base_has_no_account,
|
|
209
|
+
drop constraint if exists tax_posting_templates_tax_has_account;
|
|
210
|
+
|
|
211
|
+
alter table tax_posting_templates
|
|
212
|
+
add constraint tax_posting_templates_account_by_type check (
|
|
213
|
+
case posting_type when 'tax' then account_code is not null else account_code is null end
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
-- ---------------------------------------------------------------------------
|
|
217
|
+
-- The country model
|
|
218
|
+
-- ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
alter table country_defaults
|
|
221
|
+
add column if not exists rounding_method rounding_method not null default 'half_up',
|
|
222
|
+
add column if not exists cash_rounding_unit numeric(8, 4) not null default 0;
|
|
223
|
+
|
|
224
|
+
comment on column country_defaults.rounding_method is
|
|
225
|
+
'How this country rounds a tax amount. half_up is what post_document does today, in every country, so the default changes nothing.';
|
|
226
|
+
comment on column country_defaults.cash_rounding_unit is
|
|
227
|
+
'Smallest coin when it is not the cent: 0.05 in Switzerland. 0 means the cent, which is every country of phase 0.';
|
|
228
|
+
|
|
229
|
+
alter table country_defaults
|
|
230
|
+
add constraint country_defaults_cash_rounding_unit_positive check (cash_rounding_unit >= 0);
|
|
231
|
+
|
|
232
|
+
-- ---------------------------------------------------------------------------
|
|
233
|
+
-- document_tax_summary — what the other party actually pays
|
|
234
|
+
--
|
|
235
|
+
-- `tax_charged` sums the accounting factors of the *tax* postings, so a tax
|
|
236
|
+
-- whose postings net to zero adds nothing to the document total. That is what
|
|
237
|
+
-- makes a self-assessed purchase come out at the net amount. A `tax_on_base`
|
|
238
|
+
-- posting is money that leaves the company exactly like a `tax` posting — the
|
|
239
|
+
-- supplier of a company car is owed 1 210 € and not 1 105 € — so it counts
|
|
240
|
+
-- here too. Without this line the document total would be short by the
|
|
241
|
+
-- non-deductible half and `post_document` would refuse the document with
|
|
242
|
+
-- `document_total_mismatch`, which is at least an honest failure.
|
|
243
|
+
-- ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
create or replace view document_tax_summary
|
|
246
|
+
with (security_invoker = true) as
|
|
247
|
+
select l.document_id,
|
|
248
|
+
l.company_id,
|
|
249
|
+
d.doc_type,
|
|
250
|
+
l.tax_id,
|
|
251
|
+
t.code as tax_code,
|
|
252
|
+
t.name as tax_name,
|
|
253
|
+
t.vat_category,
|
|
254
|
+
t.amount as tax_rate,
|
|
255
|
+
sum(l.amount_untaxed) as base_amount,
|
|
256
|
+
-- Gross tax: what the VAT return reports.
|
|
257
|
+
round(sum(l.amount_untaxed) * coalesce(t.amount, 0) / 100, 2) as tax_amount,
|
|
258
|
+
-- Charged tax: what the other party actually pays. Zero when the tax
|
|
259
|
+
-- postings net out, which is exactly what self-assessment means.
|
|
260
|
+
round(
|
|
261
|
+
sum(l.amount_untaxed) * coalesce(t.amount, 0) / 100
|
|
262
|
+
* coalesce((
|
|
263
|
+
select sum(tp.factor_percent)
|
|
264
|
+
from tax_postings tp
|
|
265
|
+
where tp.tax_id = t.id
|
|
266
|
+
and tp.posting_type in ('tax', 'tax_on_base')
|
|
267
|
+
and tp.document_kind = case
|
|
268
|
+
when d.doc_type in ('sale_credit_note', 'purchase_credit_note')
|
|
269
|
+
then 'credit_note'::tax_document_kind
|
|
270
|
+
else 'invoice'::tax_document_kind
|
|
271
|
+
end
|
|
272
|
+
), 100) / 100,
|
|
273
|
+
2) as tax_charged
|
|
274
|
+
from document_lines l
|
|
275
|
+
join documents d on d.id = l.document_id
|
|
276
|
+
left join taxes t on t.id = l.tax_id
|
|
277
|
+
where l.line_type = 'product'
|
|
278
|
+
group by l.document_id, l.company_id, d.doc_type, l.tax_id,
|
|
279
|
+
t.id, t.code, t.name, t.vat_category, t.amount;
|
|
280
|
+
|
|
281
|
+
comment on view document_tax_summary is
|
|
282
|
+
'VAT breakdown of a document, one row per tax, rounded on the group basis (EN 16931 BR-CO-14).';
|
|
283
|
+
|
|
284
|
+
-- ---------------------------------------------------------------------------
|
|
285
|
+
-- post_document — the same function, plus one posting type
|
|
286
|
+
--
|
|
287
|
+
-- Everything about an existing tax is byte for byte what it was: the base
|
|
288
|
+
-- loop, the counterpart, the rounding, the refusals. The tax loop now also
|
|
289
|
+
-- reads `tax_on_base` postings, and for those it writes the amount on the
|
|
290
|
+
-- accounts of the lines instead of on an account of its own.
|
|
291
|
+
-- ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
create or replace function post_document(p_document_id uuid)
|
|
294
|
+
returns entries
|
|
295
|
+
language plpgsql
|
|
296
|
+
as $$
|
|
297
|
+
declare
|
|
298
|
+
v_doc documents%rowtype;
|
|
299
|
+
v_entry entries%rowtype;
|
|
300
|
+
v_journal uuid;
|
|
301
|
+
v_date date;
|
|
302
|
+
v_is_sale boolean;
|
|
303
|
+
v_is_credit boolean;
|
|
304
|
+
v_kind tax_document_kind;
|
|
305
|
+
v_base_credit boolean;
|
|
306
|
+
v_seq integer := 0;
|
|
307
|
+
v_contact uuid;
|
|
308
|
+
v_maturity date;
|
|
309
|
+
v_terms smallint;
|
|
310
|
+
v_counterpart uuid;
|
|
311
|
+
v_diff numeric(16, 2);
|
|
312
|
+
v_amount numeric(16, 2);
|
|
313
|
+
v_box_amount numeric(16, 2);
|
|
314
|
+
v_share numeric(16, 2);
|
|
315
|
+
v_share_box numeric(16, 2);
|
|
316
|
+
v_left numeric(16, 2);
|
|
317
|
+
v_left_box numeric(16, 2);
|
|
318
|
+
v_side_left numeric(16, 2);
|
|
319
|
+
v_side_left_neg numeric(16, 2);
|
|
320
|
+
v_side_credit boolean;
|
|
321
|
+
v_label text;
|
|
322
|
+
r record;
|
|
323
|
+
p record;
|
|
324
|
+
g record;
|
|
325
|
+
begin
|
|
326
|
+
select * into v_doc from documents where id = p_document_id for update;
|
|
327
|
+
if not found then
|
|
328
|
+
raise exception 'unknown_document: document % does not exist', p_document_id;
|
|
329
|
+
end if;
|
|
330
|
+
if v_doc.state = 'posted' then
|
|
331
|
+
raise exception 'document_already_posted: document % is already posted', p_document_id;
|
|
332
|
+
end if;
|
|
333
|
+
if v_doc.state = 'cancelled' then
|
|
334
|
+
raise exception 'document_cancelled: document % cannot be posted', p_document_id;
|
|
335
|
+
end if;
|
|
336
|
+
if v_doc.entry_id is not null then
|
|
337
|
+
raise exception 'document_already_booked: document % already points at entry %',
|
|
338
|
+
p_document_id, v_doc.entry_id;
|
|
339
|
+
end if;
|
|
340
|
+
|
|
341
|
+
if v_doc.doc_type in ('sale_quote', 'purchase_order') then
|
|
342
|
+
raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
|
|
343
|
+
end if;
|
|
344
|
+
|
|
345
|
+
v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
|
|
346
|
+
v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
|
|
347
|
+
v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
|
|
348
|
+
-- Sale invoice and purchase credit note credit the base; the other two debit it.
|
|
349
|
+
v_base_credit := (v_is_sale <> v_is_credit);
|
|
350
|
+
|
|
351
|
+
v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
|
|
352
|
+
|
|
353
|
+
if not exists (
|
|
354
|
+
select 1 from document_lines
|
|
355
|
+
where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
|
|
356
|
+
) then
|
|
357
|
+
raise exception 'document_empty: document % has no billable line', p_document_id;
|
|
358
|
+
end if;
|
|
359
|
+
|
|
360
|
+
-- A fixed-amount tax has no basis to spread over lines; refuse rather than
|
|
361
|
+
-- guess.
|
|
362
|
+
if exists (
|
|
363
|
+
select 1 from document_lines l join taxes t on t.id = l.tax_id
|
|
364
|
+
where l.document_id = p_document_id and t.amount_type <> 'percent'
|
|
365
|
+
) then
|
|
366
|
+
raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
|
|
367
|
+
end if;
|
|
368
|
+
|
|
369
|
+
-- Every tax used must be in force on the accounting date.
|
|
370
|
+
for r in
|
|
371
|
+
select distinct t.id, t.code, t.valid_from, t.valid_to
|
|
372
|
+
from document_lines l join taxes t on t.id = l.tax_id
|
|
373
|
+
where l.document_id = p_document_id
|
|
374
|
+
loop
|
|
375
|
+
if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
|
|
376
|
+
raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
|
|
377
|
+
end if;
|
|
378
|
+
end loop;
|
|
379
|
+
|
|
380
|
+
-- Totals are derived; make sure they reflect the lines as they stand now.
|
|
381
|
+
perform documents_refresh_totals(p_document_id);
|
|
382
|
+
select * into v_doc from documents where id = p_document_id;
|
|
383
|
+
|
|
384
|
+
v_journal := coalesce(
|
|
385
|
+
v_doc.journal_id,
|
|
386
|
+
case when v_is_sale
|
|
387
|
+
then (select sales_journal_id from companies where id = v_doc.company_id)
|
|
388
|
+
else (select purchase_journal_id from companies where id = v_doc.company_id)
|
|
389
|
+
end
|
|
390
|
+
);
|
|
391
|
+
if v_journal is null then
|
|
392
|
+
raise exception 'no_journal: set journal_id on the document or a default journal on the company';
|
|
393
|
+
end if;
|
|
394
|
+
|
|
395
|
+
perform assert_period_open(v_doc.company_id, v_date, true);
|
|
396
|
+
|
|
397
|
+
v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
|
|
398
|
+
|
|
399
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
|
|
400
|
+
description, state, document_id, currency_code)
|
|
401
|
+
values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
|
|
402
|
+
coalesce(v_doc.number, v_doc.supplier_reference),
|
|
403
|
+
v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
|
|
404
|
+
then ' / ' || v_doc.supplier_reference else '' end,
|
|
405
|
+
'draft', p_document_id, v_doc.currency_code)
|
|
406
|
+
returning * into v_entry;
|
|
407
|
+
|
|
408
|
+
-- ------------------------------------------------------------------ bases
|
|
409
|
+
for r in
|
|
410
|
+
select l.account_id,
|
|
411
|
+
l.tax_id,
|
|
412
|
+
sum(l.amount_untaxed) as base_amount,
|
|
413
|
+
min(l.sequence) as seq,
|
|
414
|
+
string_agg(distinct l.name, ', ') as label
|
|
415
|
+
from document_lines l
|
|
416
|
+
where l.document_id = p_document_id
|
|
417
|
+
and l.line_type = 'product'
|
|
418
|
+
group by l.account_id, l.tax_id
|
|
419
|
+
having sum(l.amount_untaxed) <> 0
|
|
420
|
+
order by 4
|
|
421
|
+
loop
|
|
422
|
+
select tp.declaration_box, tp.factor_percent, tp.box_factor_percent
|
|
423
|
+
into p
|
|
424
|
+
from tax_postings tp
|
|
425
|
+
where tp.tax_id = r.tax_id
|
|
426
|
+
and tp.document_kind = v_kind
|
|
427
|
+
and tp.posting_type = 'base'
|
|
428
|
+
limit 1;
|
|
429
|
+
|
|
430
|
+
v_amount := round(r.base_amount * coalesce(p.factor_percent, 100) / 100, 2);
|
|
431
|
+
v_seq := v_seq + 10;
|
|
432
|
+
|
|
433
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
434
|
+
debit, credit, tax_id, tax_line,
|
|
435
|
+
declaration_box, box_amount, currency_code)
|
|
436
|
+
values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
|
|
437
|
+
case when v_base_credit then 0 else v_amount end,
|
|
438
|
+
case when v_base_credit then v_amount else 0 end,
|
|
439
|
+
r.tax_id, false,
|
|
440
|
+
p.declaration_box,
|
|
441
|
+
case when p.declaration_box is null then null
|
|
442
|
+
else round(r.base_amount * coalesce(p.box_factor_percent, 100) / 100, 2) end,
|
|
443
|
+
v_doc.currency_code);
|
|
444
|
+
end loop;
|
|
445
|
+
|
|
446
|
+
-- ------------------------------------------------------------------ taxes
|
|
447
|
+
for r in
|
|
448
|
+
select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
|
|
449
|
+
from document_tax_summary s
|
|
450
|
+
where s.document_id = p_document_id
|
|
451
|
+
and s.tax_id is not null
|
|
452
|
+
and s.tax_amount <> 0
|
|
453
|
+
order by s.tax_code
|
|
454
|
+
loop
|
|
455
|
+
-- The postings of one side share out the tax of the group; the last of
|
|
456
|
+
-- each side takes what is left. Until `tax_on_base` there was never more
|
|
457
|
+
-- than one posting per side, so this changes no existing tax by a cent —
|
|
458
|
+
-- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
|
|
459
|
+
-- where rounding each half on its own would book 0.64 against a document
|
|
460
|
+
-- that totals 0.63.
|
|
461
|
+
v_side_left := null;
|
|
462
|
+
v_side_left_neg := null;
|
|
463
|
+
|
|
464
|
+
for p in
|
|
465
|
+
select tp.posting_type, tp.factor_percent, tp.account_id,
|
|
466
|
+
tp.declaration_box, tp.box_factor_percent,
|
|
467
|
+
case when tp.factor_percent >= 0 then 1 else -1 end as side,
|
|
468
|
+
sum(abs(tp.factor_percent))
|
|
469
|
+
over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
|
|
470
|
+
as side_factor,
|
|
471
|
+
row_number() over (
|
|
472
|
+
partition by case when tp.factor_percent >= 0 then 1 else -1 end
|
|
473
|
+
order by tp.sequence, tp.id)
|
|
474
|
+
= count(*) over (
|
|
475
|
+
partition by case when tp.factor_percent >= 0 then 1 else -1 end)
|
|
476
|
+
as is_last_of_side
|
|
477
|
+
from tax_postings tp
|
|
478
|
+
where tp.tax_id = r.tax_id
|
|
479
|
+
and tp.document_kind = v_kind
|
|
480
|
+
and tp.posting_type in ('tax', 'tax_on_base')
|
|
481
|
+
order by tp.sequence, tp.id
|
|
482
|
+
loop
|
|
483
|
+
-- The tax of the group was rounded once, in the view. Every posting is
|
|
484
|
+
-- a share of that one figure, never of a re-derived one.
|
|
485
|
+
if p.side >= 0 then
|
|
486
|
+
if v_side_left is null then
|
|
487
|
+
v_side_left := round(r.tax_amount * p.side_factor / 100, 2);
|
|
488
|
+
end if;
|
|
489
|
+
if p.is_last_of_side then
|
|
490
|
+
v_amount := v_side_left;
|
|
491
|
+
else
|
|
492
|
+
v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
|
|
493
|
+
v_side_left := v_side_left - v_amount;
|
|
494
|
+
end if;
|
|
495
|
+
else
|
|
496
|
+
if v_side_left_neg is null then
|
|
497
|
+
v_side_left_neg := round(r.tax_amount * p.side_factor / 100, 2);
|
|
498
|
+
end if;
|
|
499
|
+
if p.is_last_of_side then
|
|
500
|
+
v_amount := v_side_left_neg;
|
|
501
|
+
else
|
|
502
|
+
v_amount := round(r.tax_amount * abs(p.factor_percent) / 100, 2);
|
|
503
|
+
v_side_left_neg := v_side_left_neg - v_amount;
|
|
504
|
+
end if;
|
|
505
|
+
end if;
|
|
506
|
+
|
|
507
|
+
if v_amount = 0 then
|
|
508
|
+
continue;
|
|
509
|
+
end if;
|
|
510
|
+
-- A positive factor keeps the side of the base, a negative one flips it.
|
|
511
|
+
v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
|
|
512
|
+
-- The box keeps its own rounding: `box_factor_percent` was always
|
|
513
|
+
-- independent from `factor_percent`, because a declaration figure is
|
|
514
|
+
-- not a ledger figure and only the ledger has to balance.
|
|
515
|
+
v_box_amount := case when p.declaration_box is null then null
|
|
516
|
+
else round(r.tax_amount * p.box_factor_percent / 100, 2) end;
|
|
517
|
+
|
|
518
|
+
if p.posting_type = 'tax' then
|
|
519
|
+
v_seq := v_seq + 10;
|
|
520
|
+
|
|
521
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
522
|
+
debit, credit, tax_id, tax_line,
|
|
523
|
+
declaration_box, box_amount, currency_code)
|
|
524
|
+
values (v_entry.id, v_doc.company_id, p.account_id, v_seq, r.tax_name,
|
|
525
|
+
case when v_side_credit then 0 else v_amount end,
|
|
526
|
+
case when v_side_credit then v_amount else 0 end,
|
|
527
|
+
r.tax_id, true,
|
|
528
|
+
p.declaration_box, v_box_amount, v_doc.currency_code);
|
|
529
|
+
continue;
|
|
530
|
+
end if;
|
|
531
|
+
|
|
532
|
+
-- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
|
|
533
|
+
-- lines it taxes, split in proportion to their base. The last share
|
|
534
|
+
-- takes whatever is left, so the shares add up to the amount that was
|
|
535
|
+
-- rounded once on the group and the entry still balances to the cent.
|
|
536
|
+
v_left := v_amount;
|
|
537
|
+
v_left_box := v_box_amount;
|
|
538
|
+
|
|
539
|
+
for g in
|
|
540
|
+
select account_id,
|
|
541
|
+
base_amount,
|
|
542
|
+
seq,
|
|
543
|
+
sum(base_amount) over () as total_base,
|
|
544
|
+
row_number() over (order by seq) = count(*) over () as is_last
|
|
545
|
+
from (
|
|
546
|
+
select l.account_id,
|
|
547
|
+
sum(l.amount_untaxed) as base_amount,
|
|
548
|
+
min(l.sequence) as seq
|
|
549
|
+
from document_lines l
|
|
550
|
+
where l.document_id = p_document_id
|
|
551
|
+
and l.line_type = 'product'
|
|
552
|
+
and l.tax_id = r.tax_id
|
|
553
|
+
group by l.account_id
|
|
554
|
+
having sum(l.amount_untaxed) <> 0
|
|
555
|
+
) as groups
|
|
556
|
+
order by seq
|
|
557
|
+
loop
|
|
558
|
+
if g.is_last then
|
|
559
|
+
v_share := v_left;
|
|
560
|
+
v_share_box := v_left_box;
|
|
561
|
+
else
|
|
562
|
+
v_share := round(v_amount * g.base_amount / g.total_base, 2);
|
|
563
|
+
v_share_box := case when v_box_amount is null then null
|
|
564
|
+
else round(v_box_amount * g.base_amount / g.total_base, 2) end;
|
|
565
|
+
v_left := v_left - v_share;
|
|
566
|
+
v_left_box := v_left_box - v_share_box;
|
|
567
|
+
end if;
|
|
568
|
+
|
|
569
|
+
if v_share = 0 then
|
|
570
|
+
continue;
|
|
571
|
+
end if;
|
|
572
|
+
|
|
573
|
+
v_seq := v_seq + 10;
|
|
574
|
+
|
|
575
|
+
-- `tax_line` stays false: the amount is on a base account and belongs
|
|
576
|
+
-- to the base side of the declaration, which is why the Belgian grids
|
|
577
|
+
-- 82 and 83 report it together with the base.
|
|
578
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
579
|
+
debit, credit, tax_id, tax_line,
|
|
580
|
+
declaration_box, box_amount, currency_code)
|
|
581
|
+
values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
|
|
582
|
+
case when v_side_credit then 0 else v_share end,
|
|
583
|
+
case when v_side_credit then v_share else 0 end,
|
|
584
|
+
r.tax_id, false,
|
|
585
|
+
p.declaration_box, v_share_box, v_doc.currency_code);
|
|
586
|
+
end loop;
|
|
587
|
+
end loop;
|
|
588
|
+
end loop;
|
|
589
|
+
|
|
590
|
+
-- ------------------------------------------------------------ counterpart
|
|
591
|
+
select total_debit - total_credit into v_diff from entries where id = v_entry.id;
|
|
592
|
+
|
|
593
|
+
if v_diff = 0 then
|
|
594
|
+
raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
|
|
595
|
+
end if;
|
|
596
|
+
|
|
597
|
+
v_contact := commercial_entity(v_doc.contact_id);
|
|
598
|
+
v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
|
|
599
|
+
|
|
600
|
+
select payment_terms_days into v_terms from contacts where id = v_contact;
|
|
601
|
+
v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
|
|
602
|
+
|
|
603
|
+
v_amount := abs(v_diff);
|
|
604
|
+
v_seq := v_seq + 10;
|
|
605
|
+
|
|
606
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
607
|
+
debit, credit, contact_id, date_maturity, currency_code)
|
|
608
|
+
values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
|
|
609
|
+
case when v_diff > 0 then 0 else v_amount end,
|
|
610
|
+
case when v_diff > 0 then v_amount else 0 end,
|
|
611
|
+
v_contact, v_maturity, v_doc.currency_code);
|
|
612
|
+
|
|
613
|
+
-- The ledger is right by construction. If the header disagrees, the header
|
|
614
|
+
-- is what is wrong, and we say so instead of quietly patching a line.
|
|
615
|
+
if abs(v_amount - abs(v_doc.amount_total)) > 0.005 then
|
|
616
|
+
raise exception 'document_total_mismatch: document % totals % but its lines book %',
|
|
617
|
+
p_document_id, v_doc.amount_total, v_amount;
|
|
618
|
+
end if;
|
|
619
|
+
|
|
620
|
+
-- --------------------------------------------------------------- posting
|
|
621
|
+
v_entry := post_entry(v_entry.id);
|
|
622
|
+
|
|
623
|
+
update documents
|
|
624
|
+
set state = 'posted',
|
|
625
|
+
number = coalesce(number, v_entry.number),
|
|
626
|
+
entry_id = v_entry.id,
|
|
627
|
+
accounting_date = v_date
|
|
628
|
+
where id = p_document_id;
|
|
629
|
+
|
|
630
|
+
return v_entry;
|
|
631
|
+
end;
|
|
632
|
+
$$;
|
|
633
|
+
|
|
634
|
+
comment on function post_document(uuid) is
|
|
635
|
+
'Books a document: base lines, tax lines from tax_postings — including the non-deductible share, which lands on the accounts of the lines — and a counterpart that balances by construction.';
|
|
636
|
+
|
|
637
|
+
-- ---------------------------------------------------------------------------
|
|
638
|
+
-- install_country_template carries the new columns into a company
|
|
639
|
+
-- ---------------------------------------------------------------------------
|
|
640
|
+
|
|
641
|
+
create or replace function install_country_template(
|
|
642
|
+
p_company_id uuid,
|
|
643
|
+
p_country char(2),
|
|
644
|
+
p_language char(2) default null
|
|
645
|
+
)
|
|
646
|
+
returns void
|
|
647
|
+
language plpgsql
|
|
648
|
+
as $$
|
|
649
|
+
declare
|
|
650
|
+
v_defaults country_defaults%rowtype;
|
|
651
|
+
r record;
|
|
652
|
+
v_tax_id uuid;
|
|
653
|
+
v_language char(2);
|
|
654
|
+
v_version text;
|
|
655
|
+
begin
|
|
656
|
+
if not exists (select 1 from companies where id = p_company_id) then
|
|
657
|
+
raise exception 'unknown_company: %', p_company_id;
|
|
658
|
+
end if;
|
|
659
|
+
if not exists (select 1 from account_templates where country = p_country) then
|
|
660
|
+
raise exception 'unknown_country_template: no chart of accounts seeded for %', p_country;
|
|
661
|
+
end if;
|
|
662
|
+
|
|
663
|
+
select coalesce(p_language, c.language) into v_language
|
|
664
|
+
from companies c where c.id = p_company_id;
|
|
665
|
+
|
|
666
|
+
-- 1. Accounts, without the hierarchy.
|
|
667
|
+
insert into accounts (company_id, code, name, name_i18n, statement_hint,
|
|
668
|
+
account_type, reconcilable)
|
|
669
|
+
select p_company_id, t.code,
|
|
670
|
+
coalesce(nullif(t.name_i18n ->> v_language, ''), t.name),
|
|
671
|
+
t.name_i18n, t.statement_hint, t.account_type, t.reconcilable
|
|
672
|
+
from account_templates t
|
|
673
|
+
where t.country = p_country
|
|
674
|
+
on conflict (company_id, code) do nothing;
|
|
675
|
+
|
|
676
|
+
-- 2. Hierarchy, now that every code exists.
|
|
677
|
+
update accounts a
|
|
678
|
+
set parent_id = p.id
|
|
679
|
+
from account_templates t
|
|
680
|
+
join accounts p on p.company_id = p_company_id and p.code = t.parent_code
|
|
681
|
+
where a.company_id = p_company_id
|
|
682
|
+
and a.code = t.code
|
|
683
|
+
and t.country = p_country
|
|
684
|
+
and t.parent_code is not null
|
|
685
|
+
and a.parent_id is null;
|
|
686
|
+
|
|
687
|
+
-- 3. Journals.
|
|
688
|
+
insert into journals (company_id, code, name, journal_type)
|
|
689
|
+
select p_company_id, t.code, t.name, t.journal_type
|
|
690
|
+
from journal_templates t
|
|
691
|
+
where t.country = p_country
|
|
692
|
+
on conflict (company_id, code) do nothing;
|
|
693
|
+
|
|
694
|
+
-- 4. Taxes and their postings, each posting keeping the form its box is on.
|
|
695
|
+
for r in
|
|
696
|
+
select * from tax_templates where country = p_country order by sequence, code
|
|
697
|
+
loop
|
|
698
|
+
v_tax_id := null;
|
|
699
|
+
insert into taxes (company_id, code, name, description, amount_type, amount,
|
|
700
|
+
applies_to, treatment, country, valid_from, valid_to,
|
|
701
|
+
legal_reference, vat_category, exemption_code, sequence,
|
|
702
|
+
tax_kind, recoverable, jurisdiction, price_include,
|
|
703
|
+
cash_basis, cash_basis_transition_account_id)
|
|
704
|
+
values (p_company_id, r.code, r.name, r.description, r.amount_type, r.amount,
|
|
705
|
+
r.applies_to, r.treatment, p_country, r.valid_from, r.valid_to,
|
|
706
|
+
r.legal_reference, r.vat_category, r.exemption_code, r.sequence,
|
|
707
|
+
r.tax_kind, r.recoverable, r.jurisdiction, r.price_include,
|
|
708
|
+
r.cash_basis, account_id_by_code(p_company_id, r.cash_basis_transition_account_code))
|
|
709
|
+
on conflict (company_id, code) do nothing
|
|
710
|
+
returning id into v_tax_id;
|
|
711
|
+
|
|
712
|
+
if v_tax_id is null then
|
|
713
|
+
continue;
|
|
714
|
+
end if;
|
|
715
|
+
|
|
716
|
+
insert into tax_postings (tax_id, company_id, document_kind, posting_type,
|
|
717
|
+
factor_percent, account_id, declaration_box,
|
|
718
|
+
box_factor_percent, report_code, sequence)
|
|
719
|
+
select v_tax_id, p_company_id, tp.document_kind, tp.posting_type,
|
|
720
|
+
tp.factor_percent, account_id_by_code(p_company_id, tp.account_code),
|
|
721
|
+
tp.declaration_box, tp.box_factor_percent, tp.report_code, tp.sequence
|
|
722
|
+
from tax_posting_templates tp
|
|
723
|
+
where tp.tax_template_id = r.id
|
|
724
|
+
order by tp.sequence;
|
|
725
|
+
end loop;
|
|
726
|
+
|
|
727
|
+
-- 5. Roles.
|
|
728
|
+
select * into v_defaults from country_defaults where country = p_country;
|
|
729
|
+
if found then
|
|
730
|
+
update companies c
|
|
731
|
+
set receivable_account_id = coalesce(c.receivable_account_id, account_id_by_code(p_company_id, v_defaults.receivable_code)),
|
|
732
|
+
payable_account_id = coalesce(c.payable_account_id, account_id_by_code(p_company_id, v_defaults.payable_code)),
|
|
733
|
+
suspense_account_id = coalesce(c.suspense_account_id, account_id_by_code(p_company_id, v_defaults.suspense_code)),
|
|
734
|
+
rounding_account_id = coalesce(c.rounding_account_id, account_id_by_code(p_company_id, v_defaults.rounding_code)),
|
|
735
|
+
retained_earnings_account_id = coalesce(c.retained_earnings_account_id, account_id_by_code(p_company_id, v_defaults.retained_earnings_code)),
|
|
736
|
+
default_sales_account_id = coalesce(c.default_sales_account_id, account_id_by_code(p_company_id, v_defaults.sales_account_code)),
|
|
737
|
+
default_purchase_account_id = coalesce(c.default_purchase_account_id, account_id_by_code(p_company_id, v_defaults.purchase_account_code)),
|
|
738
|
+
sales_journal_id = coalesce(c.sales_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.sales_journal_code)),
|
|
739
|
+
purchase_journal_id = coalesce(c.purchase_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.purchase_journal_code)),
|
|
740
|
+
miscellaneous_journal_id = coalesce(c.miscellaneous_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.misc_journal_code))
|
|
741
|
+
where c.id = p_company_id;
|
|
742
|
+
|
|
743
|
+
update journals j
|
|
744
|
+
set default_account_id = account_id_by_code(p_company_id, v_defaults.bank_account_code)
|
|
745
|
+
where j.company_id = p_company_id
|
|
746
|
+
and j.journal_type = 'bank'
|
|
747
|
+
and j.default_account_id is null
|
|
748
|
+
and v_defaults.bank_account_code is not null;
|
|
749
|
+
|
|
750
|
+
update journals j
|
|
751
|
+
set default_account_id = account_id_by_code(p_company_id, v_defaults.cash_account_code)
|
|
752
|
+
where j.company_id = p_company_id
|
|
753
|
+
and j.journal_type = 'cash'
|
|
754
|
+
and j.default_account_id is null
|
|
755
|
+
and v_defaults.cash_account_code is not null;
|
|
756
|
+
end if;
|
|
757
|
+
|
|
758
|
+
-- 6. What was copied, and from which version.
|
|
759
|
+
v_version := coalesce((select version from country_packs where country = p_country), '1.0.0');
|
|
760
|
+
|
|
761
|
+
insert into company_packs (company_id, country, version)
|
|
762
|
+
values (p_company_id, p_country, v_version)
|
|
763
|
+
on conflict (company_id, country) do update
|
|
764
|
+
set version = excluded.version,
|
|
765
|
+
upgraded_at = now()
|
|
766
|
+
where company_packs.version is distinct from excluded.version;
|
|
767
|
+
end;
|
|
768
|
+
$$;
|
|
769
|
+
|
|
770
|
+
comment on function install_country_template(uuid, char, char) is
|
|
771
|
+
'Copies a country pack into a company in one language, wires the default roles and the financial journals, and records the pack version in company_packs.';
|
|
772
|
+
|
|
773
|
+
-- A function created now comes out executable by PUBLIC unless this runs, and
|
|
774
|
+
-- on Supabase that is an anonymous RPC endpoint. From PUBLIC only — `anon`
|
|
775
|
+
-- holds explicit grants on the policy helpers. Rule 6 of the README.
|
|
776
|
+
revoke execute on all functions in schema public from public;
|