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,1318 @@
|
|
|
1
|
+
-- Ekwo OS — a tax follows the territory of the parties.
|
|
2
|
+
--
|
|
3
|
+
-- Two packs stopped at this wall from opposite sides, and `docs/international.md`
|
|
4
|
+
-- records both. `packs/gb/` cannot carry the Northern Ireland taxes, because a
|
|
5
|
+
-- company in Manchester would be offered them; `packs/us/` offers a Californian
|
|
6
|
+
-- company the New York and Oregon codes, because `jurisdiction` is a label on
|
|
7
|
+
-- the tax and nothing reads it. Both notes proposed the same fix — let a tax
|
|
8
|
+
-- name a territory, and let a party record the one it is in — and the American
|
|
9
|
+
-- one added the half the British one did not need: a sale is taxed where the
|
|
10
|
+
-- goods are delivered, so the condition is on the **buyer** and on the place of
|
|
11
|
+
-- supply, not only on the seller.
|
|
12
|
+
--
|
|
13
|
+
-- The whole argument, with the three shapes weighed against this one, is in
|
|
14
|
+
-- `docs/decisions.md`, "A tax follows the territory of the parties". What
|
|
15
|
+
-- follows is the schema.
|
|
16
|
+
--
|
|
17
|
+
-- ---------------------------------------------------------------------------
|
|
18
|
+
-- A territory is a column, not a composition
|
|
19
|
+
-- ---------------------------------------------------------------------------
|
|
20
|
+
--
|
|
21
|
+
-- A company and a contact each carry a country, and a country is not a
|
|
22
|
+
-- territory: the tax that matters here is levied by California and not by the
|
|
23
|
+
-- United States, and Northern Ireland is `XI` — a code of the Union's own
|
|
24
|
+
-- systems, which ISO 3166-2 does not carry and which no concatenation of a
|
|
25
|
+
-- country and a region reaches. `fiscal_country || '-' || region` gets `US-CA`
|
|
26
|
+
-- and never gets `XI`, and a rule with one exception written into it collects
|
|
27
|
+
-- the second exception in silence.
|
|
28
|
+
--
|
|
29
|
+
-- `region` stays what it is: ISO 3166-2 **without** the prefix, for the
|
|
30
|
+
-- Canadian pack that will want to ask "which province" as a question with a
|
|
31
|
+
-- short list of answers. That is a different question from "which body of tax
|
|
32
|
+
-- law is this party under", and this migration answers the second one.
|
|
33
|
+
--
|
|
34
|
+
-- Four columns, all of them foreign keys into `territories`, which is what
|
|
35
|
+
-- makes them reviewable: a code nobody can look up is a string, and a string
|
|
36
|
+
-- is what `jurisdiction` has been since the day it was added.
|
|
37
|
+
--
|
|
38
|
+
-- `companies.territory_code` where this company is established
|
|
39
|
+
-- `contacts.territory_code` where this party is
|
|
40
|
+
-- `documents.supply_territory_code` where the supply takes place
|
|
41
|
+
-- `taxes.applies_seller_territory` where the seller has to be
|
|
42
|
+
-- `taxes.applies_buyer_territory` where the buyer has to be
|
|
43
|
+
-- `taxes.applies_supply_territory` where the supply has to be
|
|
44
|
+
--
|
|
45
|
+
-- All six are nullable, and null is the ordinary case. A Belgian company will
|
|
46
|
+
-- never set one; asking every installation in Europe to answer a question the
|
|
47
|
+
-- common system does not pose would be the country-default mistake with its
|
|
48
|
+
-- sign flipped.
|
|
49
|
+
|
|
50
|
+
alter table companies add column if not exists territory_code text references territories(code);
|
|
51
|
+
alter table contacts add column if not exists territory_code text references territories(code);
|
|
52
|
+
alter table documents add column if not exists supply_territory_code text references territories(code);
|
|
53
|
+
|
|
54
|
+
-- A foreign key is read the way `currency_code` is: by the row that holds it,
|
|
55
|
+
-- and by the check that a territory is still referenced before it is taken
|
|
56
|
+
-- away. One index per key, leading with the key's own column.
|
|
57
|
+
create index if not exists companies_territory_idx on companies (territory_code);
|
|
58
|
+
create index if not exists contacts_territory_idx on contacts (territory_code);
|
|
59
|
+
create index if not exists documents_supply_territory_idx on documents (supply_territory_code);
|
|
60
|
+
|
|
61
|
+
comment on column companies.territory_code is
|
|
62
|
+
'Territory of `territories` this company is established in for tax, where the country is not precise enough: US-CA for a Californian filer, XI for a Northern Irish one. Null everywhere the country is the answer, which is every country of the common system of VAT — the resolution then reads fiscal_country. Distinct from `region`, which is a province code without a prefix and answers a different question.';
|
|
63
|
+
comment on column contacts.territory_code is
|
|
64
|
+
'Territory of `territories` this party is in, where its country is not precise enough. Null resolves to `country`.';
|
|
65
|
+
comment on column documents.supply_territory_code is
|
|
66
|
+
'Territory of `territories` the supply takes place in, where `delivery_country` is not precise enough — a delivery to US-CA and one to US-NY are both US. Null resolves to delivery_country, and then to the buyer''s territory: a supply nobody said anything else about is delivered to the person who bought it.';
|
|
67
|
+
|
|
68
|
+
alter table tax_templates
|
|
69
|
+
add column if not exists applies_seller_territory text references territories(code),
|
|
70
|
+
add column if not exists applies_buyer_territory text references territories(code),
|
|
71
|
+
add column if not exists applies_supply_territory text references territories(code);
|
|
72
|
+
|
|
73
|
+
alter table taxes
|
|
74
|
+
add column if not exists applies_seller_territory text references territories(code),
|
|
75
|
+
add column if not exists applies_buyer_territory text references territories(code),
|
|
76
|
+
add column if not exists applies_supply_territory text references territories(code);
|
|
77
|
+
|
|
78
|
+
create index if not exists tax_templates_applies_seller_territory_idx on tax_templates (applies_seller_territory);
|
|
79
|
+
create index if not exists tax_templates_applies_buyer_territory_idx on tax_templates (applies_buyer_territory);
|
|
80
|
+
create index if not exists tax_templates_applies_supply_territory_idx on tax_templates (applies_supply_territory);
|
|
81
|
+
create index if not exists taxes_applies_seller_territory_idx on taxes (applies_seller_territory);
|
|
82
|
+
create index if not exists taxes_applies_buyer_territory_idx on taxes (applies_buyer_territory);
|
|
83
|
+
create index if not exists taxes_applies_supply_territory_idx on taxes (applies_supply_territory);
|
|
84
|
+
|
|
85
|
+
comment on column tax_templates.applies_seller_territory is
|
|
86
|
+
'Territory the seller has to be in for this tax to apply, from `applies_when.seller_in` of the pack. Null means the tax says nothing about the seller.';
|
|
87
|
+
comment on column tax_templates.applies_buyer_territory is
|
|
88
|
+
'Territory the buyer has to be in, from `applies_when.buyer_in`. Null means the tax says nothing about the buyer.';
|
|
89
|
+
comment on column tax_templates.applies_supply_territory is
|
|
90
|
+
'Territory the supply has to take place in, from `applies_when.supply_in`. A sale is taxed where the goods are delivered, which is why this is not the same column as the buyer''s.';
|
|
91
|
+
comment on column taxes.applies_seller_territory is
|
|
92
|
+
'Territory the seller has to be in for this tax to apply. post_document() refuses a document that contradicts it; it never chooses a tax for anybody.';
|
|
93
|
+
comment on column taxes.applies_buyer_territory is
|
|
94
|
+
'Territory the buyer has to be in for this tax to apply.';
|
|
95
|
+
comment on column taxes.applies_supply_territory is
|
|
96
|
+
'Territory the supply has to take place in for this tax to apply.';
|
|
97
|
+
|
|
98
|
+
-- ---------------------------------------------------------------------------
|
|
99
|
+
-- A condition is satisfied by the territory named and by everything inside it
|
|
100
|
+
--
|
|
101
|
+
-- `territories.parent_code` already draws the tree: `XI` hangs off `GB`,
|
|
102
|
+
-- `ES-CE` off `ES`, `US-CA` off `US`. So `seller_in: 'GB'` covers a seller in
|
|
103
|
+
-- Northern Ireland and `seller_in: 'XI'` does not cover a seller in Great
|
|
104
|
+
-- Britain, which is exactly what lets one country's pack carry two sets of
|
|
105
|
+
-- taxes and offer each where it applies.
|
|
106
|
+
--
|
|
107
|
+
-- The walk is bounded by the tree and the tree is two deep everywhere today;
|
|
108
|
+
-- the recursion is written all the same, because a table nobody can add a
|
|
109
|
+
-- third level to is a table somebody will add a third level to.
|
|
110
|
+
--
|
|
111
|
+
-- A code the table does not carry answers false rather than raising: the
|
|
112
|
+
-- foreign keys above mean the only way to get one here is a party column
|
|
113
|
+
-- resolving to a country that has no row, and a supply to a place the
|
|
114
|
+
-- reference data has never heard of does not satisfy a condition naming a
|
|
115
|
+
-- place it has.
|
|
116
|
+
-- ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
create or replace function territory_within(p_code text, p_of text)
|
|
119
|
+
returns boolean
|
|
120
|
+
language sql
|
|
121
|
+
stable
|
|
122
|
+
as $$
|
|
123
|
+
with recursive up as (
|
|
124
|
+
select t.code, t.parent_code
|
|
125
|
+
from territories t
|
|
126
|
+
where t.code = upper(btrim(p_code))
|
|
127
|
+
union all
|
|
128
|
+
select t.code, t.parent_code
|
|
129
|
+
from territories t
|
|
130
|
+
join up on up.parent_code = t.code
|
|
131
|
+
)
|
|
132
|
+
select exists (select 1 from up where up.code = upper(btrim(p_of)));
|
|
133
|
+
$$;
|
|
134
|
+
|
|
135
|
+
comment on function territory_within(text, text) is
|
|
136
|
+
'True when the first territory is the second one or lies inside it, following territories.parent_code: US-CA is within US, XI is within GB, and neither is within the other. False for a code this table does not carry.';
|
|
137
|
+
|
|
138
|
+
-- ---------------------------------------------------------------------------
|
|
139
|
+
-- Where the three parties of a document are
|
|
140
|
+
--
|
|
141
|
+
-- `p_party` is a closed vocabulary of three values and raises on a fourth,
|
|
142
|
+
-- which is how every other vocabulary of this schema behaves.
|
|
143
|
+
--
|
|
144
|
+
-- `seller` the company on a sale, the contact on a purchase. Decided by the
|
|
145
|
+
-- kind of document and never by the row: a purchase invoice is
|
|
146
|
+
-- somebody else's sale.
|
|
147
|
+
-- `buyer` the other one.
|
|
148
|
+
-- `supply` the place of supply: `supply_territory_code`, failing that
|
|
149
|
+
-- `delivery_country` — BG-15 of EN 16931, which `documents` has
|
|
150
|
+
-- carried since the day it existed and which is exactly the
|
|
151
|
+
-- statement that the place of delivery is not the billing address
|
|
152
|
+
-- — and failing that the buyer's territory.
|
|
153
|
+
--
|
|
154
|
+
-- The last rung is the one worth arguing about and the argument is that the
|
|
155
|
+
-- alternative is worse: a supply with no delivery address is not an unknown
|
|
156
|
+
-- place, it is the ordinary case of a seller and a buyer in one room. Refusing
|
|
157
|
+
-- it would refuse every invoice in Europe the day a pack there conditioned one
|
|
158
|
+
-- tax on a territory.
|
|
159
|
+
--
|
|
160
|
+
-- Null comes back where the ladder runs out — a contact with no country and no
|
|
161
|
+
-- territory, on a document with no delivery address. `post_document()` turns
|
|
162
|
+
-- that into a refusal that names the party, the tax and the column that would
|
|
163
|
+
-- answer, and only for a tax that actually asked.
|
|
164
|
+
-- ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
create or replace function document_territory(p_document_id uuid, p_party text)
|
|
167
|
+
returns text
|
|
168
|
+
language plpgsql
|
|
169
|
+
stable
|
|
170
|
+
as $$
|
|
171
|
+
declare
|
|
172
|
+
v_doc documents%rowtype;
|
|
173
|
+
v_is_sale boolean;
|
|
174
|
+
v_company text;
|
|
175
|
+
v_contact text;
|
|
176
|
+
v_buyer text;
|
|
177
|
+
begin
|
|
178
|
+
if p_party not in ('seller', 'buyer', 'supply') then
|
|
179
|
+
raise exception 'unknown_party: % is not seller, buyer or supply', p_party;
|
|
180
|
+
end if;
|
|
181
|
+
|
|
182
|
+
select * into v_doc from documents where id = p_document_id;
|
|
183
|
+
if not found then
|
|
184
|
+
raise exception 'unknown_document: document % does not exist', p_document_id;
|
|
185
|
+
end if;
|
|
186
|
+
|
|
187
|
+
v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note', 'sale_quote');
|
|
188
|
+
|
|
189
|
+
select coalesce(c.territory_code, c.fiscal_country) into v_company
|
|
190
|
+
from companies c where c.id = v_doc.company_id;
|
|
191
|
+
select coalesce(k.territory_code, k.country) into v_contact
|
|
192
|
+
from contacts k where k.id = v_doc.contact_id;
|
|
193
|
+
|
|
194
|
+
v_buyer := case when v_is_sale then v_contact else v_company end;
|
|
195
|
+
|
|
196
|
+
if p_party = 'seller' then
|
|
197
|
+
return case when v_is_sale then v_company else v_contact end;
|
|
198
|
+
elsif p_party = 'buyer' then
|
|
199
|
+
return v_buyer;
|
|
200
|
+
end if;
|
|
201
|
+
|
|
202
|
+
return coalesce(v_doc.supply_territory_code, v_doc.delivery_country, v_buyer);
|
|
203
|
+
end;
|
|
204
|
+
$$;
|
|
205
|
+
|
|
206
|
+
comment on function document_territory(uuid, text) is
|
|
207
|
+
'The territory of one party to a document: seller, buyer or supply. The seller is the company on a sale and the contact on a purchase, each resolving to its own territory_code and failing that to its country. The supply is the document''s supply_territory_code, failing that its delivery_country (BG-15), failing that the buyer''s territory. Null where none of those was ever recorded.';
|
|
208
|
+
|
|
209
|
+
revoke execute on function territory_within(text, text) from public, anon;
|
|
210
|
+
revoke execute on function document_territory(uuid, text) from public, anon;
|
|
211
|
+
grant execute on function territory_within(text, text) to authenticated, service_role;
|
|
212
|
+
grant execute on function document_territory(uuid, text) to authenticated, service_role;
|
|
213
|
+
|
|
214
|
+
-- ---------------------------------------------------------------------------
|
|
215
|
+
-- The readers
|
|
216
|
+
--
|
|
217
|
+
-- Four functions are replaced, and three of them only to carry three columns
|
|
218
|
+
-- through a copy. `post_document()` is the one that gains a behaviour.
|
|
219
|
+
--
|
|
220
|
+
-- `install_country_template()` and `pack_upgrade()` copy a template into a
|
|
221
|
+
-- company, and a column missing on one side of a copy is the defect
|
|
222
|
+
-- `price_include` was for a whole release. `pack_upgrade_diff()` compares the
|
|
223
|
+
-- two, so that a pack which moves a tax from one territory to another shows up
|
|
224
|
+
-- as a difference a company is asked about rather than as a silent divergence.
|
|
225
|
+
-- ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
create or replace function install_country_template(
|
|
228
|
+
p_company_id uuid,
|
|
229
|
+
p_country char(2),
|
|
230
|
+
p_language char(2) default null,
|
|
231
|
+
p_chart_code text default null
|
|
232
|
+
)
|
|
233
|
+
returns void
|
|
234
|
+
language plpgsql
|
|
235
|
+
as $$
|
|
236
|
+
declare
|
|
237
|
+
v_defaults country_defaults%rowtype;
|
|
238
|
+
r record;
|
|
239
|
+
v_tax_id uuid;
|
|
240
|
+
v_language char(2);
|
|
241
|
+
v_version text;
|
|
242
|
+
v_chart text;
|
|
243
|
+
v_charts text;
|
|
244
|
+
begin
|
|
245
|
+
if not exists (select 1 from companies where id = p_company_id) then
|
|
246
|
+
raise exception 'unknown_company: %', p_company_id;
|
|
247
|
+
end if;
|
|
248
|
+
|
|
249
|
+
-- Which chart. The caller names one, or the pack's default stands. A named
|
|
250
|
+
-- chart that does not exist is an error that lists what does: guessing here
|
|
251
|
+
-- would install the wrong plan of accounts and nobody would notice for a
|
|
252
|
+
-- year.
|
|
253
|
+
if p_chart_code is null then
|
|
254
|
+
select c.code into v_chart
|
|
255
|
+
from chart_templates c
|
|
256
|
+
where c.country = p_country and c.is_default;
|
|
257
|
+
if v_chart is null then
|
|
258
|
+
-- No pack for this country, or a pack that names no default chart.
|
|
259
|
+
raise exception 'unknown_country_template: no chart of accounts seeded for %', p_country;
|
|
260
|
+
end if;
|
|
261
|
+
else
|
|
262
|
+
select c.code into v_chart
|
|
263
|
+
from chart_templates c
|
|
264
|
+
where c.country = p_country and c.code = p_chart_code;
|
|
265
|
+
if v_chart is null then
|
|
266
|
+
select string_agg(c.code, ', ' order by c.code) into v_charts
|
|
267
|
+
from chart_templates c where c.country = p_country;
|
|
268
|
+
raise exception 'unknown_chart: % has no chart %. It has: %',
|
|
269
|
+
p_country, p_chart_code, coalesce(v_charts, 'none');
|
|
270
|
+
end if;
|
|
271
|
+
end if;
|
|
272
|
+
|
|
273
|
+
if not exists (
|
|
274
|
+
select 1 from account_templates
|
|
275
|
+
where country = p_country and chart_code = v_chart
|
|
276
|
+
) then
|
|
277
|
+
raise exception 'unknown_country_template: chart % of % holds no account', v_chart, p_country;
|
|
278
|
+
end if;
|
|
279
|
+
|
|
280
|
+
-- The language of the copy: what the caller asked for, else what the
|
|
281
|
+
-- company keeps its books in.
|
|
282
|
+
select coalesce(p_language, c.language) into v_language
|
|
283
|
+
from companies c where c.id = p_company_id;
|
|
284
|
+
|
|
285
|
+
-- 1. Accounts, without the hierarchy.
|
|
286
|
+
insert into accounts (company_id, code, name, name_i18n, statement_hint,
|
|
287
|
+
account_type, reconcilable)
|
|
288
|
+
select p_company_id, t.code,
|
|
289
|
+
label_for(t.name, t.name_i18n, array[v_language]),
|
|
290
|
+
t.name_i18n, t.statement_hint, t.account_type, t.reconcilable
|
|
291
|
+
from account_templates t
|
|
292
|
+
where t.country = p_country
|
|
293
|
+
and t.chart_code = v_chart
|
|
294
|
+
on conflict (company_id, code) do nothing;
|
|
295
|
+
|
|
296
|
+
-- 2. Hierarchy, now that every code exists.
|
|
297
|
+
update accounts a
|
|
298
|
+
set parent_id = p.id
|
|
299
|
+
from account_templates t
|
|
300
|
+
join accounts p on p.company_id = p_company_id and p.code = t.parent_code
|
|
301
|
+
where a.company_id = p_company_id
|
|
302
|
+
and a.code = t.code
|
|
303
|
+
and t.country = p_country
|
|
304
|
+
and t.chart_code = v_chart
|
|
305
|
+
and t.parent_code is not null
|
|
306
|
+
and a.parent_id is null;
|
|
307
|
+
|
|
308
|
+
-- 3. Journals. Common to every chart of the country.
|
|
309
|
+
insert into journals (company_id, code, name, name_i18n, journal_type)
|
|
310
|
+
select p_company_id, t.code,
|
|
311
|
+
label_for(t.name, t.name_i18n, array[v_language]),
|
|
312
|
+
t.name_i18n, t.journal_type
|
|
313
|
+
from journal_templates t
|
|
314
|
+
where t.country = p_country
|
|
315
|
+
on conflict (company_id, code) do nothing;
|
|
316
|
+
|
|
317
|
+
-- 4. Taxes and their postings. Also common: a chart changes how a company
|
|
318
|
+
-- presents itself, not what it owes.
|
|
319
|
+
for r in
|
|
320
|
+
select * from tax_templates where country = p_country order by sequence, code
|
|
321
|
+
loop
|
|
322
|
+
v_tax_id := null;
|
|
323
|
+
insert into taxes (company_id, code, name, name_i18n, description, amount_type, amount,
|
|
324
|
+
applies_to, treatment, country, valid_from, valid_to,
|
|
325
|
+
legal_reference, vat_category, exemption_code, sequence,
|
|
326
|
+
tax_kind, recoverable, jurisdiction, price_include,
|
|
327
|
+
cash_basis, cash_basis_transition_account_id,
|
|
328
|
+
applies_seller_territory, applies_buyer_territory,
|
|
329
|
+
applies_supply_territory)
|
|
330
|
+
values (p_company_id, r.code,
|
|
331
|
+
label_for(r.name, r.name_i18n, array[v_language]),
|
|
332
|
+
r.name_i18n, r.description, r.amount_type, r.amount,
|
|
333
|
+
r.applies_to, r.treatment, p_country, r.valid_from, r.valid_to,
|
|
334
|
+
r.legal_reference, r.vat_category, r.exemption_code, r.sequence,
|
|
335
|
+
r.tax_kind, r.recoverable, r.jurisdiction, r.price_include,
|
|
336
|
+
r.cash_basis, account_id_by_code(p_company_id, r.cash_basis_transition_account_code),
|
|
337
|
+
r.applies_seller_territory, r.applies_buyer_territory,
|
|
338
|
+
r.applies_supply_territory)
|
|
339
|
+
on conflict (company_id, code) do nothing
|
|
340
|
+
returning id into v_tax_id;
|
|
341
|
+
|
|
342
|
+
if v_tax_id is null then
|
|
343
|
+
continue;
|
|
344
|
+
end if;
|
|
345
|
+
|
|
346
|
+
insert into tax_postings (tax_id, company_id, document_kind, posting_type,
|
|
347
|
+
factor_percent, account_id, declaration_box,
|
|
348
|
+
declaration_boxes, box_factor_percent, report_code,
|
|
349
|
+
sequence)
|
|
350
|
+
select v_tax_id, p_company_id, tp.document_kind, tp.posting_type,
|
|
351
|
+
tp.factor_percent, account_id_by_code(p_company_id, tp.account_code),
|
|
352
|
+
tp.declaration_box, tp.declaration_boxes, tp.box_factor_percent,
|
|
353
|
+
tp.report_code, tp.sequence
|
|
354
|
+
from tax_posting_templates tp
|
|
355
|
+
where tp.tax_template_id = r.id
|
|
356
|
+
order by tp.sequence;
|
|
357
|
+
end loop;
|
|
358
|
+
|
|
359
|
+
-- 5. Roles.
|
|
360
|
+
select * into v_defaults from country_defaults where country = p_country;
|
|
361
|
+
if found then
|
|
362
|
+
update companies c
|
|
363
|
+
set receivable_account_id = coalesce(c.receivable_account_id, account_id_by_code(p_company_id, v_defaults.receivable_code)),
|
|
364
|
+
payable_account_id = coalesce(c.payable_account_id, account_id_by_code(p_company_id, v_defaults.payable_code)),
|
|
365
|
+
suspense_account_id = coalesce(c.suspense_account_id, account_id_by_code(p_company_id, v_defaults.suspense_code)),
|
|
366
|
+
rounding_account_id = coalesce(c.rounding_account_id, account_id_by_code(p_company_id, v_defaults.rounding_code)),
|
|
367
|
+
retained_earnings_account_id = coalesce(c.retained_earnings_account_id, account_id_by_code(p_company_id, v_defaults.retained_earnings_code)),
|
|
368
|
+
default_sales_account_id = coalesce(c.default_sales_account_id, account_id_by_code(p_company_id, v_defaults.sales_account_code)),
|
|
369
|
+
default_purchase_account_id = coalesce(c.default_purchase_account_id, account_id_by_code(p_company_id, v_defaults.purchase_account_code)),
|
|
370
|
+
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)),
|
|
371
|
+
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)),
|
|
372
|
+
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))
|
|
373
|
+
where c.id = p_company_id;
|
|
374
|
+
|
|
375
|
+
-- 6. The financial journals point at their account.
|
|
376
|
+
update journals j
|
|
377
|
+
set default_account_id = account_id_by_code(p_company_id, v_defaults.bank_account_code)
|
|
378
|
+
where j.company_id = p_company_id
|
|
379
|
+
and j.journal_type = 'bank'
|
|
380
|
+
and j.default_account_id is null
|
|
381
|
+
and v_defaults.bank_account_code is not null;
|
|
382
|
+
|
|
383
|
+
update journals j
|
|
384
|
+
set default_account_id = account_id_by_code(p_company_id, v_defaults.cash_account_code)
|
|
385
|
+
where j.company_id = p_company_id
|
|
386
|
+
and j.journal_type = 'cash'
|
|
387
|
+
and j.default_account_id is null
|
|
388
|
+
and v_defaults.cash_account_code is not null;
|
|
389
|
+
end if;
|
|
390
|
+
|
|
391
|
+
-- 7. What was copied, from which version, and on which chart.
|
|
392
|
+
v_version := coalesce((select version from country_packs where country = p_country), '1.0.0');
|
|
393
|
+
|
|
394
|
+
insert into company_packs (company_id, country, version, chart_code)
|
|
395
|
+
values (p_company_id, p_country, v_version, v_chart)
|
|
396
|
+
on conflict (company_id, country) do update
|
|
397
|
+
set version = excluded.version,
|
|
398
|
+
chart_code = excluded.chart_code,
|
|
399
|
+
upgraded_at = now()
|
|
400
|
+
where company_packs.version is distinct from excluded.version
|
|
401
|
+
or company_packs.chart_code is distinct from excluded.chart_code;
|
|
402
|
+
|
|
403
|
+
-- 8. The working chart. Everything wired above is pinned, so a company opens
|
|
404
|
+
-- on the accounts its country model actually points at rather than on the
|
|
405
|
+
-- whole transcription.
|
|
406
|
+
perform pin_referenced_accounts(p_company_id);
|
|
407
|
+
end;
|
|
408
|
+
$$;
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
create or replace function pack_upgrade_diff(
|
|
412
|
+
p_company_id uuid,
|
|
413
|
+
p_country char(2) default null
|
|
414
|
+
)
|
|
415
|
+
returns table (
|
|
416
|
+
object text,
|
|
417
|
+
key text,
|
|
418
|
+
change text,
|
|
419
|
+
rule pack_change_rule,
|
|
420
|
+
detail jsonb
|
|
421
|
+
)
|
|
422
|
+
language plpgsql
|
|
423
|
+
stable
|
|
424
|
+
as $$
|
|
425
|
+
declare
|
|
426
|
+
v_country char(2);
|
|
427
|
+
v_chart text;
|
|
428
|
+
begin
|
|
429
|
+
select coalesce(p_country, c.country) into v_country
|
|
430
|
+
from companies c where c.id = p_company_id;
|
|
431
|
+
if v_country is null then
|
|
432
|
+
raise exception 'unknown_company: %', p_company_id;
|
|
433
|
+
end if;
|
|
434
|
+
|
|
435
|
+
select cp.chart_code into v_chart
|
|
436
|
+
from company_packs cp
|
|
437
|
+
where cp.company_id = p_company_id and cp.country = v_country;
|
|
438
|
+
if v_chart is null then
|
|
439
|
+
raise exception 'no_pack_installed: company % holds no % pack, so there is nothing to upgrade from', p_company_id, v_country;
|
|
440
|
+
end if;
|
|
441
|
+
|
|
442
|
+
-- Accounts -----------------------------------------------------------------
|
|
443
|
+
return query
|
|
444
|
+
select 'account', t.code, 'missing', 'addition'::pack_change_rule,
|
|
445
|
+
jsonb_build_object('pack', jsonb_build_object('name', t.name, 'account_type', t.account_type))
|
|
446
|
+
from account_templates t
|
|
447
|
+
where t.country = v_country and t.chart_code = v_chart
|
|
448
|
+
and not exists (select 1 from accounts a where a.company_id = p_company_id and a.code = t.code);
|
|
449
|
+
|
|
450
|
+
return query
|
|
451
|
+
select 'account', a.code, 'differs', 'review'::pack_change_rule,
|
|
452
|
+
jsonb_build_object(
|
|
453
|
+
'pack', jsonb_build_object('name', t.name, 'account_type', t.account_type),
|
|
454
|
+
'company', jsonb_build_object('name', a.name, 'account_type', a.account_type))
|
|
455
|
+
from accounts a
|
|
456
|
+
join account_templates t
|
|
457
|
+
on t.country = v_country and t.chart_code = v_chart and t.code = a.code
|
|
458
|
+
where a.company_id = p_company_id
|
|
459
|
+
and (label_for(t.name, t.name_i18n, preferred_languages(p_company_id)) is distinct from a.name
|
|
460
|
+
or t.account_type is distinct from a.account_type);
|
|
461
|
+
|
|
462
|
+
return query
|
|
463
|
+
select 'account', a.code, 'company_only', 'review'::pack_change_rule,
|
|
464
|
+
jsonb_build_object('company', jsonb_build_object('name', a.name, 'account_type', a.account_type))
|
|
465
|
+
from accounts a
|
|
466
|
+
where a.company_id = p_company_id
|
|
467
|
+
and not exists (select 1 from account_templates t
|
|
468
|
+
where t.country = v_country and t.chart_code = v_chart and t.code = a.code);
|
|
469
|
+
|
|
470
|
+
-- Journals -----------------------------------------------------------------
|
|
471
|
+
return query
|
|
472
|
+
select 'journal', t.code, 'missing', 'addition'::pack_change_rule,
|
|
473
|
+
jsonb_build_object('pack', jsonb_build_object('name', t.name, 'journal_type', t.journal_type))
|
|
474
|
+
from journal_templates t
|
|
475
|
+
where t.country = v_country
|
|
476
|
+
and not exists (select 1 from journals j where j.company_id = p_company_id and j.code = t.code);
|
|
477
|
+
|
|
478
|
+
return query
|
|
479
|
+
select 'journal', j.code, 'differs', 'review'::pack_change_rule,
|
|
480
|
+
jsonb_build_object(
|
|
481
|
+
'pack', jsonb_build_object('name', t.name, 'journal_type', t.journal_type),
|
|
482
|
+
'company', jsonb_build_object('name', j.name, 'journal_type', j.journal_type))
|
|
483
|
+
from journals j
|
|
484
|
+
join journal_templates t on t.country = v_country and t.code = j.code
|
|
485
|
+
where j.company_id = p_company_id
|
|
486
|
+
and (t.name is distinct from j.name or t.journal_type is distinct from j.journal_type);
|
|
487
|
+
|
|
488
|
+
-- Taxes --------------------------------------------------------------------
|
|
489
|
+
return query
|
|
490
|
+
select 'tax', t.code, 'missing', 'addition'::pack_change_rule,
|
|
491
|
+
jsonb_build_object('pack', jsonb_build_object(
|
|
492
|
+
'name', t.name, 'amount', t.amount, 'amount_type', t.amount_type,
|
|
493
|
+
'valid_from', t.valid_from, 'valid_to', t.valid_to))
|
|
494
|
+
from tax_templates t
|
|
495
|
+
where t.country = v_country
|
|
496
|
+
and not exists (select 1 from taxes x where x.company_id = p_company_id and x.code = t.code);
|
|
497
|
+
|
|
498
|
+
-- A validity the pack has closed and the company still holds open, or holds
|
|
499
|
+
-- open longer. This is a rate change arriving: the old tax stops on the day
|
|
500
|
+
-- the pack says, and the new rate is an addition above.
|
|
501
|
+
return query
|
|
502
|
+
select 'tax', x.code, 'valid_to', 'closure'::pack_change_rule,
|
|
503
|
+
jsonb_build_object('pack', jsonb_build_object('valid_to', t.valid_to),
|
|
504
|
+
'company', jsonb_build_object('valid_to', x.valid_to))
|
|
505
|
+
from taxes x
|
|
506
|
+
join tax_templates t on t.country = v_country and t.code = x.code
|
|
507
|
+
where x.company_id = p_company_id
|
|
508
|
+
and t.valid_to is not null
|
|
509
|
+
and (x.valid_to is null or x.valid_to > t.valid_to);
|
|
510
|
+
|
|
511
|
+
return query
|
|
512
|
+
select 'tax', x.code, 'differs', 'review'::pack_change_rule,
|
|
513
|
+
jsonb_build_object(
|
|
514
|
+
'pack', jsonb_build_object('name', t.name, 'amount', t.amount,
|
|
515
|
+
'amount_type', t.amount_type, 'treatment', t.treatment,
|
|
516
|
+
'applies_to', t.applies_to, 'tax_kind', t.tax_kind,
|
|
517
|
+
'recoverable', t.recoverable, 'valid_from', t.valid_from,
|
|
518
|
+
'applies_seller_territory', t.applies_seller_territory,
|
|
519
|
+
'applies_buyer_territory', t.applies_buyer_territory,
|
|
520
|
+
'applies_supply_territory', t.applies_supply_territory),
|
|
521
|
+
'company', jsonb_build_object('name', x.name, 'amount', x.amount,
|
|
522
|
+
'amount_type', x.amount_type, 'treatment', x.treatment,
|
|
523
|
+
'applies_to', x.applies_to, 'tax_kind', x.tax_kind,
|
|
524
|
+
'recoverable', x.recoverable, 'valid_from', x.valid_from,
|
|
525
|
+
'applies_seller_territory', x.applies_seller_territory,
|
|
526
|
+
'applies_buyer_territory', x.applies_buyer_territory,
|
|
527
|
+
'applies_supply_territory', x.applies_supply_territory))
|
|
528
|
+
from taxes x
|
|
529
|
+
join tax_templates t on t.country = v_country and t.code = x.code
|
|
530
|
+
where x.company_id = p_company_id
|
|
531
|
+
and (label_for(t.name, null, preferred_languages(p_company_id)) is distinct from x.name
|
|
532
|
+
or t.amount is distinct from x.amount
|
|
533
|
+
or t.amount_type is distinct from x.amount_type
|
|
534
|
+
or t.treatment is distinct from x.treatment
|
|
535
|
+
or t.applies_to is distinct from x.applies_to
|
|
536
|
+
or t.tax_kind is distinct from x.tax_kind
|
|
537
|
+
or t.recoverable is distinct from x.recoverable
|
|
538
|
+
or t.valid_from is distinct from x.valid_from
|
|
539
|
+
-- Where a tax applies is a fact about the tax, and a pack that moves
|
|
540
|
+
-- it is a pack the company has to be shown.
|
|
541
|
+
or t.applies_seller_territory is distinct from x.applies_seller_territory
|
|
542
|
+
or t.applies_buyer_territory is distinct from x.applies_buyer_territory
|
|
543
|
+
or t.applies_supply_territory is distinct from x.applies_supply_territory);
|
|
544
|
+
|
|
545
|
+
return query
|
|
546
|
+
select 'tax', x.code, 'company_only', 'review'::pack_change_rule,
|
|
547
|
+
jsonb_build_object('company', jsonb_build_object('name', x.name, 'amount', x.amount))
|
|
548
|
+
from taxes x
|
|
549
|
+
where x.company_id = p_company_id
|
|
550
|
+
and x.country = v_country
|
|
551
|
+
and not exists (select 1 from tax_templates t where t.country = v_country and t.code = x.code);
|
|
552
|
+
|
|
553
|
+
-- What a tax books. Compared as a whole per tax rather than line by line: a
|
|
554
|
+
-- posting has no identity of its own, and "this tax now books its
|
|
555
|
+
-- non-deductible share somewhere else" is one fact, not four.
|
|
556
|
+
return query
|
|
557
|
+
with pack as (
|
|
558
|
+
select t.code,
|
|
559
|
+
jsonb_agg(jsonb_build_object('document_kind', tp.document_kind,
|
|
560
|
+
'posting_type', tp.posting_type,
|
|
561
|
+
'factor_percent', tp.factor_percent,
|
|
562
|
+
'account_code', tp.account_code,
|
|
563
|
+
'declaration_box', tp.declaration_box,
|
|
564
|
+
'declaration_boxes', to_jsonb(tp.declaration_boxes),
|
|
565
|
+
'box_factor_percent', tp.box_factor_percent,
|
|
566
|
+
'report_code', tp.report_code)
|
|
567
|
+
order by tp.document_kind, tp.posting_type, tp.sequence) as postings
|
|
568
|
+
from tax_templates t
|
|
569
|
+
join tax_posting_templates tp on tp.tax_template_id = t.id
|
|
570
|
+
where t.country = v_country
|
|
571
|
+
group by t.code
|
|
572
|
+
), held as (
|
|
573
|
+
select x.code,
|
|
574
|
+
jsonb_agg(jsonb_build_object('document_kind', p.document_kind,
|
|
575
|
+
'posting_type', p.posting_type,
|
|
576
|
+
'factor_percent', p.factor_percent,
|
|
577
|
+
'account_code', a.code,
|
|
578
|
+
'declaration_box', p.declaration_box,
|
|
579
|
+
'declaration_boxes', to_jsonb(p.declaration_boxes),
|
|
580
|
+
'box_factor_percent', p.box_factor_percent,
|
|
581
|
+
'report_code', p.report_code)
|
|
582
|
+
order by p.document_kind, p.posting_type, p.sequence) as postings
|
|
583
|
+
from taxes x
|
|
584
|
+
join tax_postings p on p.tax_id = x.id
|
|
585
|
+
left join accounts a on a.id = p.account_id
|
|
586
|
+
where x.company_id = p_company_id
|
|
587
|
+
group by x.code
|
|
588
|
+
)
|
|
589
|
+
select 'tax_posting', pack.code, 'differs', 'review'::pack_change_rule,
|
|
590
|
+
jsonb_build_object('pack', pack.postings, 'company', held.postings)
|
|
591
|
+
from pack
|
|
592
|
+
join held on held.code = pack.code
|
|
593
|
+
where pack.postings is distinct from held.postings;
|
|
594
|
+
end;
|
|
595
|
+
$$;
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
create or replace function pack_upgrade(
|
|
599
|
+
p_company_id uuid,
|
|
600
|
+
p_country char(2) default null,
|
|
601
|
+
p_apply boolean default false
|
|
602
|
+
)
|
|
603
|
+
returns jsonb
|
|
604
|
+
language plpgsql
|
|
605
|
+
security definer
|
|
606
|
+
set search_path = public, pg_temp
|
|
607
|
+
as $$
|
|
608
|
+
declare
|
|
609
|
+
v_country char(2);
|
|
610
|
+
v_chart text;
|
|
611
|
+
v_from text;
|
|
612
|
+
v_to text;
|
|
613
|
+
v_applied jsonb := '[]'::jsonb;
|
|
614
|
+
v_listed jsonb := '[]'::jsonb;
|
|
615
|
+
v_refused jsonb := '[]'::jsonb;
|
|
616
|
+
d record;
|
|
617
|
+
v_tax_id uuid;
|
|
618
|
+
v_template tax_templates%rowtype;
|
|
619
|
+
begin
|
|
620
|
+
select coalesce(p_country, c.country) into v_country
|
|
621
|
+
from companies c where c.id = p_company_id;
|
|
622
|
+
if v_country is null then
|
|
623
|
+
raise exception 'unknown_company: %', p_company_id;
|
|
624
|
+
end if;
|
|
625
|
+
|
|
626
|
+
if auth.uid() is not null and not has_capability(p_company_id, 'company.write') then
|
|
627
|
+
raise exception 'not_allowed: upgrading a country pack needs company.write';
|
|
628
|
+
end if;
|
|
629
|
+
|
|
630
|
+
select cp.version, cp.chart_code into v_from, v_chart
|
|
631
|
+
from company_packs cp
|
|
632
|
+
where cp.company_id = p_company_id and cp.country = v_country;
|
|
633
|
+
if v_chart is null then
|
|
634
|
+
raise exception 'no_pack_installed: company % holds no % pack, so there is nothing to upgrade from', p_company_id, v_country;
|
|
635
|
+
end if;
|
|
636
|
+
|
|
637
|
+
select version into v_to from country_packs where country = v_country;
|
|
638
|
+
if v_to is null then
|
|
639
|
+
raise exception 'no_pack_loaded: this installation holds no % pack. Apply the seeds of this release first.', v_country;
|
|
640
|
+
end if;
|
|
641
|
+
|
|
642
|
+
for d in select * from pack_upgrade_diff(p_company_id, v_country) loop
|
|
643
|
+
-- Never, whatever the caller asked. An upgrade adds and closes; it does
|
|
644
|
+
-- not take a row out of a company's books.
|
|
645
|
+
if d.change = 'company_only' then
|
|
646
|
+
v_refused := v_refused || jsonb_build_object('object', d.object, 'key', d.key,
|
|
647
|
+
'change', d.change, 'rule', d.rule,
|
|
648
|
+
'detail', d.detail);
|
|
649
|
+
continue;
|
|
650
|
+
end if;
|
|
651
|
+
|
|
652
|
+
if d.rule = 'review' and not p_apply then
|
|
653
|
+
v_listed := v_listed || jsonb_build_object('object', d.object, 'key', d.key,
|
|
654
|
+
'change', d.change, 'rule', d.rule,
|
|
655
|
+
'detail', d.detail);
|
|
656
|
+
continue;
|
|
657
|
+
end if;
|
|
658
|
+
|
|
659
|
+
if d.object = 'account' and d.change = 'missing' then
|
|
660
|
+
insert into accounts (company_id, code, name, name_i18n, statement_hint, account_type, reconcilable)
|
|
661
|
+
select p_company_id, t.code, label_for(t.name, t.name_i18n, preferred_languages(p_company_id)),
|
|
662
|
+
t.name_i18n, t.statement_hint, t.account_type, t.reconcilable
|
|
663
|
+
from account_templates t
|
|
664
|
+
where t.country = v_country and t.chart_code = v_chart and t.code = d.key
|
|
665
|
+
on conflict (company_id, code) do nothing;
|
|
666
|
+
|
|
667
|
+
elsif d.object = 'account' and d.change = 'differs' then
|
|
668
|
+
update accounts a
|
|
669
|
+
set name = label_for(t.name, t.name_i18n, preferred_languages(p_company_id)),
|
|
670
|
+
name_i18n = t.name_i18n,
|
|
671
|
+
account_type = t.account_type
|
|
672
|
+
from account_templates t
|
|
673
|
+
where t.country = v_country and t.chart_code = v_chart and t.code = a.code
|
|
674
|
+
and a.company_id = p_company_id and a.code = d.key;
|
|
675
|
+
|
|
676
|
+
elsif d.object = 'journal' and d.change = 'missing' then
|
|
677
|
+
insert into journals (company_id, code, name, journal_type)
|
|
678
|
+
select p_company_id, t.code, t.name, t.journal_type
|
|
679
|
+
from journal_templates t
|
|
680
|
+
where t.country = v_country and t.code = d.key
|
|
681
|
+
on conflict (company_id, code) do nothing;
|
|
682
|
+
|
|
683
|
+
elsif d.object = 'journal' and d.change = 'differs' then
|
|
684
|
+
update journals j
|
|
685
|
+
set name = t.name, journal_type = t.journal_type
|
|
686
|
+
from journal_templates t
|
|
687
|
+
where t.country = v_country and t.code = j.code
|
|
688
|
+
and j.company_id = p_company_id and j.code = d.key;
|
|
689
|
+
|
|
690
|
+
elsif d.object = 'tax' and d.change = 'missing' then
|
|
691
|
+
select * into v_template from tax_templates where country = v_country and code = d.key;
|
|
692
|
+
insert into taxes (company_id, code, name, description, amount_type, amount,
|
|
693
|
+
applies_to, treatment, country, valid_from, valid_to,
|
|
694
|
+
legal_reference, vat_category, exemption_code, sequence,
|
|
695
|
+
tax_kind, recoverable, jurisdiction, price_include,
|
|
696
|
+
cash_basis, cash_basis_transition_account_id,
|
|
697
|
+
applies_seller_territory, applies_buyer_territory,
|
|
698
|
+
applies_supply_territory)
|
|
699
|
+
values (p_company_id, v_template.code, v_template.name, v_template.description,
|
|
700
|
+
v_template.amount_type, v_template.amount, v_template.applies_to,
|
|
701
|
+
v_template.treatment, v_country, v_template.valid_from, v_template.valid_to,
|
|
702
|
+
v_template.legal_reference, v_template.vat_category, v_template.exemption_code,
|
|
703
|
+
v_template.sequence, v_template.tax_kind, v_template.recoverable,
|
|
704
|
+
v_template.jurisdiction, v_template.price_include, v_template.cash_basis,
|
|
705
|
+
account_id_by_code(p_company_id, v_template.cash_basis_transition_account_code),
|
|
706
|
+
v_template.applies_seller_territory, v_template.applies_buyer_territory,
|
|
707
|
+
v_template.applies_supply_territory)
|
|
708
|
+
on conflict (company_id, code) do nothing
|
|
709
|
+
returning id into v_tax_id;
|
|
710
|
+
|
|
711
|
+
if v_tax_id is not null then
|
|
712
|
+
insert into tax_postings (tax_id, company_id, document_kind, posting_type,
|
|
713
|
+
factor_percent, account_id, declaration_box,
|
|
714
|
+
declaration_boxes, box_factor_percent, report_code,
|
|
715
|
+
sequence)
|
|
716
|
+
select v_tax_id, p_company_id, tp.document_kind, tp.posting_type,
|
|
717
|
+
tp.factor_percent, account_id_by_code(p_company_id, tp.account_code),
|
|
718
|
+
tp.declaration_box, tp.declaration_boxes, tp.box_factor_percent,
|
|
719
|
+
tp.report_code, tp.sequence
|
|
720
|
+
from tax_posting_templates tp
|
|
721
|
+
where tp.tax_template_id = v_template.id
|
|
722
|
+
order by tp.sequence;
|
|
723
|
+
end if;
|
|
724
|
+
|
|
725
|
+
elsif d.object = 'tax' and d.change = 'valid_to' then
|
|
726
|
+
update taxes x
|
|
727
|
+
set valid_to = t.valid_to
|
|
728
|
+
from tax_templates t
|
|
729
|
+
where t.country = v_country and t.code = x.code
|
|
730
|
+
and x.company_id = p_company_id and x.code = d.key;
|
|
731
|
+
|
|
732
|
+
elsif d.object = 'tax' and d.change = 'differs' then
|
|
733
|
+
update taxes x
|
|
734
|
+
set name = t.name, description = t.description, amount = t.amount,
|
|
735
|
+
amount_type = t.amount_type, applies_to = t.applies_to,
|
|
736
|
+
treatment = t.treatment, valid_from = t.valid_from,
|
|
737
|
+
legal_reference = t.legal_reference, vat_category = t.vat_category,
|
|
738
|
+
exemption_code = t.exemption_code, tax_kind = t.tax_kind,
|
|
739
|
+
recoverable = t.recoverable, jurisdiction = t.jurisdiction,
|
|
740
|
+
price_include = t.price_include, cash_basis = t.cash_basis,
|
|
741
|
+
applies_seller_territory = t.applies_seller_territory,
|
|
742
|
+
applies_buyer_territory = t.applies_buyer_territory,
|
|
743
|
+
applies_supply_territory = t.applies_supply_territory
|
|
744
|
+
from tax_templates t
|
|
745
|
+
where t.country = v_country and t.code = x.code
|
|
746
|
+
and x.company_id = p_company_id and x.code = d.key;
|
|
747
|
+
|
|
748
|
+
elsif d.object = 'tax_posting' and d.change = 'differs' then
|
|
749
|
+
select id into v_tax_id from taxes where company_id = p_company_id and code = d.key;
|
|
750
|
+
delete from tax_postings where tax_id = v_tax_id;
|
|
751
|
+
insert into tax_postings (tax_id, company_id, document_kind, posting_type,
|
|
752
|
+
factor_percent, account_id, declaration_box,
|
|
753
|
+
declaration_boxes, box_factor_percent, report_code,
|
|
754
|
+
sequence)
|
|
755
|
+
select v_tax_id, p_company_id, tp.document_kind, tp.posting_type,
|
|
756
|
+
tp.factor_percent, account_id_by_code(p_company_id, tp.account_code),
|
|
757
|
+
tp.declaration_box, tp.declaration_boxes, tp.box_factor_percent,
|
|
758
|
+
tp.report_code, tp.sequence
|
|
759
|
+
from tax_posting_templates tp
|
|
760
|
+
join tax_templates t on t.id = tp.tax_template_id
|
|
761
|
+
where t.country = v_country and t.code = d.key
|
|
762
|
+
order by tp.sequence;
|
|
763
|
+
|
|
764
|
+
else
|
|
765
|
+
-- A difference this function does not know how to apply is listed
|
|
766
|
+
-- rather than guessed at. Raising here would make a new kind of
|
|
767
|
+
-- difference break every upgrade.
|
|
768
|
+
v_listed := v_listed || jsonb_build_object('object', d.object, 'key', d.key,
|
|
769
|
+
'change', d.change, 'rule', d.rule,
|
|
770
|
+
'detail', d.detail);
|
|
771
|
+
continue;
|
|
772
|
+
end if;
|
|
773
|
+
|
|
774
|
+
v_applied := v_applied || jsonb_build_object('object', d.object, 'key', d.key,
|
|
775
|
+
'change', d.change, 'rule', d.rule,
|
|
776
|
+
'detail', d.detail);
|
|
777
|
+
end loop;
|
|
778
|
+
|
|
779
|
+
-- The version moves only when nothing is left waiting. A company that still
|
|
780
|
+
-- holds a difference it has not decided on has not finished upgrading, and
|
|
781
|
+
-- saying otherwise would hide the difference at the next run.
|
|
782
|
+
if jsonb_array_length(v_listed) = 0 then
|
|
783
|
+
update company_packs
|
|
784
|
+
set version = v_to, upgraded_at = now()
|
|
785
|
+
where company_id = p_company_id and country = v_country;
|
|
786
|
+
end if;
|
|
787
|
+
|
|
788
|
+
perform audit_record(
|
|
789
|
+
p_company_id, 'company_packs', null, v_country, 'update', 'pack_upgraded',
|
|
790
|
+
jsonb_build_object('version', v_from),
|
|
791
|
+
jsonb_build_object('version', case when jsonb_array_length(v_listed) = 0 then v_to else v_from end,
|
|
792
|
+
'pack_version', v_to,
|
|
793
|
+
'chart_code', v_chart,
|
|
794
|
+
'applied', v_applied,
|
|
795
|
+
'listed', v_listed,
|
|
796
|
+
'never_applied', v_refused));
|
|
797
|
+
|
|
798
|
+
return jsonb_build_object(
|
|
799
|
+
'company_id', p_company_id,
|
|
800
|
+
'country', v_country,
|
|
801
|
+
'chart_code', v_chart,
|
|
802
|
+
'from_version', v_from,
|
|
803
|
+
'to_version', v_to,
|
|
804
|
+
'version_moved', jsonb_array_length(v_listed) = 0,
|
|
805
|
+
'applied', v_applied,
|
|
806
|
+
'listed', v_listed,
|
|
807
|
+
'never_applied', v_refused);
|
|
808
|
+
end;
|
|
809
|
+
$$;
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
-- ---------------------------------------------------------------------------
|
|
813
|
+
-- post_document, which refuses a tax the document contradicts
|
|
814
|
+
-- ---------------------------------------------------------------------------
|
|
815
|
+
|
|
816
|
+
create or replace function post_document(p_document_id uuid)
|
|
817
|
+
returns entries
|
|
818
|
+
language plpgsql
|
|
819
|
+
as $$
|
|
820
|
+
declare
|
|
821
|
+
v_doc documents%rowtype;
|
|
822
|
+
v_entry entries%rowtype;
|
|
823
|
+
v_journal uuid;
|
|
824
|
+
v_date date;
|
|
825
|
+
v_is_sale boolean;
|
|
826
|
+
v_is_credit boolean;
|
|
827
|
+
v_kind tax_document_kind;
|
|
828
|
+
v_base_credit boolean;
|
|
829
|
+
v_seq integer := 0;
|
|
830
|
+
v_contact uuid;
|
|
831
|
+
v_maturity date;
|
|
832
|
+
v_terms smallint;
|
|
833
|
+
v_counterpart uuid;
|
|
834
|
+
-- Plain `numeric`, not `numeric(16, 2)`: a local that carries a scale is a
|
|
835
|
+
-- second rounding rule hiding in a declaration, and it is not the currency's.
|
|
836
|
+
-- The only thing that rounds here is `round_amount`.
|
|
837
|
+
v_diff numeric;
|
|
838
|
+
v_diff_cur numeric;
|
|
839
|
+
v_amount numeric;
|
|
840
|
+
v_book numeric;
|
|
841
|
+
v_box_amount numeric;
|
|
842
|
+
v_share numeric;
|
|
843
|
+
v_share_book numeric;
|
|
844
|
+
v_share_box numeric;
|
|
845
|
+
v_left numeric;
|
|
846
|
+
v_left_box numeric;
|
|
847
|
+
v_side_left numeric;
|
|
848
|
+
v_side_left_neg numeric;
|
|
849
|
+
v_side_credit boolean;
|
|
850
|
+
v_label text;
|
|
851
|
+
v_rate numeric(18, 8);
|
|
852
|
+
v_home char(3);
|
|
853
|
+
v_foreign boolean;
|
|
854
|
+
v_total_cur numeric;
|
|
855
|
+
-- Two currencies, one method: the document is stated in its own and the
|
|
856
|
+
-- ledger keeps the company's, and a yen invoice paid in euros rounds each
|
|
857
|
+
-- side at the decimals that side has.
|
|
858
|
+
v_round money_rounding;
|
|
859
|
+
v_book_round money_rounding;
|
|
860
|
+
v_cash boolean;
|
|
861
|
+
v_transition uuid;
|
|
862
|
+
v_postings integer;
|
|
863
|
+
-- Where the three parties of this document are, resolved once and only when
|
|
864
|
+
-- a tax on it asks. `document_territory()` is the ladder.
|
|
865
|
+
v_terr_seller text;
|
|
866
|
+
v_terr_buyer text;
|
|
867
|
+
v_terr_supply text;
|
|
868
|
+
r record;
|
|
869
|
+
p record;
|
|
870
|
+
g record;
|
|
871
|
+
begin
|
|
872
|
+
select * into v_doc from documents where id = p_document_id for update;
|
|
873
|
+
if not found then
|
|
874
|
+
raise exception 'unknown_document: document % does not exist', p_document_id;
|
|
875
|
+
end if;
|
|
876
|
+
if v_doc.state = 'posted' then
|
|
877
|
+
raise exception 'document_already_posted: document % is already posted', p_document_id;
|
|
878
|
+
end if;
|
|
879
|
+
if v_doc.state = 'cancelled' then
|
|
880
|
+
raise exception 'document_cancelled: document % cannot be posted', p_document_id;
|
|
881
|
+
end if;
|
|
882
|
+
if v_doc.entry_id is not null then
|
|
883
|
+
raise exception 'document_already_booked: document % already points at entry %',
|
|
884
|
+
p_document_id, v_doc.entry_id;
|
|
885
|
+
end if;
|
|
886
|
+
|
|
887
|
+
if v_doc.doc_type in ('sale_quote', 'purchase_order') then
|
|
888
|
+
raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
|
|
889
|
+
end if;
|
|
890
|
+
|
|
891
|
+
v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
|
|
892
|
+
v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
|
|
893
|
+
v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
|
|
894
|
+
-- Sale invoice and purchase credit note credit the base; the other two debit it.
|
|
895
|
+
v_base_credit := (v_is_sale <> v_is_credit);
|
|
896
|
+
|
|
897
|
+
v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
|
|
898
|
+
|
|
899
|
+
if not exists (
|
|
900
|
+
select 1 from document_lines
|
|
901
|
+
where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
|
|
902
|
+
) then
|
|
903
|
+
raise exception 'document_empty: document % has no billable line', p_document_id;
|
|
904
|
+
end if;
|
|
905
|
+
|
|
906
|
+
-- A fixed-amount tax has no basis to spread over lines; refuse rather than
|
|
907
|
+
-- guess.
|
|
908
|
+
if exists (
|
|
909
|
+
select 1 from document_lines l join taxes t on t.id = l.tax_id
|
|
910
|
+
where l.document_id = p_document_id and t.amount_type <> 'percent'
|
|
911
|
+
) then
|
|
912
|
+
raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
|
|
913
|
+
end if;
|
|
914
|
+
|
|
915
|
+
-- Every tax used must be in force on the accounting date.
|
|
916
|
+
for r in
|
|
917
|
+
select distinct t.id, t.code, t.valid_from, t.valid_to
|
|
918
|
+
from document_lines l join taxes t on t.id = l.tax_id
|
|
919
|
+
where l.document_id = p_document_id
|
|
920
|
+
loop
|
|
921
|
+
if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
|
|
922
|
+
raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
|
|
923
|
+
end if;
|
|
924
|
+
end loop;
|
|
925
|
+
|
|
926
|
+
-- Every tax that names a territory must find the parties where it says they
|
|
927
|
+
-- are. This is the one thing the core may do with a territory condition: it
|
|
928
|
+
-- refuses a tax that cannot apply, and it never chooses, substitutes or
|
|
929
|
+
-- suggests one — the core picks no tax for anybody, in any country.
|
|
930
|
+
--
|
|
931
|
+
-- The resolution is skipped entirely where nothing asks, which is every
|
|
932
|
+
-- document of every pack written before this migration.
|
|
933
|
+
if exists (
|
|
934
|
+
select 1
|
|
935
|
+
from document_lines l
|
|
936
|
+
join taxes t on t.id = l.tax_id
|
|
937
|
+
where l.document_id = p_document_id
|
|
938
|
+
and (t.applies_seller_territory is not null
|
|
939
|
+
or t.applies_buyer_territory is not null
|
|
940
|
+
or t.applies_supply_territory is not null)
|
|
941
|
+
) then
|
|
942
|
+
v_terr_seller := document_territory(p_document_id, 'seller');
|
|
943
|
+
v_terr_buyer := document_territory(p_document_id, 'buyer');
|
|
944
|
+
v_terr_supply := document_territory(p_document_id, 'supply');
|
|
945
|
+
|
|
946
|
+
for r in
|
|
947
|
+
select distinct t.code,
|
|
948
|
+
t.applies_seller_territory as seller,
|
|
949
|
+
t.applies_buyer_territory as buyer,
|
|
950
|
+
t.applies_supply_territory as supply
|
|
951
|
+
from document_lines l
|
|
952
|
+
join taxes t on t.id = l.tax_id
|
|
953
|
+
where l.document_id = p_document_id
|
|
954
|
+
and (t.applies_seller_territory is not null
|
|
955
|
+
or t.applies_buyer_territory is not null
|
|
956
|
+
or t.applies_supply_territory is not null)
|
|
957
|
+
order by 1
|
|
958
|
+
loop
|
|
959
|
+
for p in
|
|
960
|
+
select v.party, v.wanted, v.actual, v.hint
|
|
961
|
+
from (values
|
|
962
|
+
('seller', r.seller, v_terr_seller,
|
|
963
|
+
'territory_code on the party that sells, or the country beside it'),
|
|
964
|
+
('buyer', r.buyer, v_terr_buyer,
|
|
965
|
+
'territory_code on the party that buys, or the country beside it'),
|
|
966
|
+
('supply', r.supply, v_terr_supply,
|
|
967
|
+
'supply_territory_code or delivery_country on the document')
|
|
968
|
+
) as v (party, wanted, actual, hint)
|
|
969
|
+
where v.wanted is not null
|
|
970
|
+
order by v.party
|
|
971
|
+
loop
|
|
972
|
+
if p.actual is null then
|
|
973
|
+
raise exception 'no_party_territory: tax % applies where the % is in %, and nothing on this document says where the % is; set %',
|
|
974
|
+
r.code, p.party, p.wanted, p.party, p.hint;
|
|
975
|
+
end if;
|
|
976
|
+
if not territory_within(p.actual, p.wanted) then
|
|
977
|
+
raise exception 'tax_territory_mismatch: tax % applies where the % is in %; on this document the % is in %',
|
|
978
|
+
r.code, p.party, p.wanted, p.party, p.actual;
|
|
979
|
+
end if;
|
|
980
|
+
end loop;
|
|
981
|
+
end loop;
|
|
982
|
+
end if;
|
|
983
|
+
|
|
984
|
+
-- Totals are derived; make sure they reflect the lines as they stand now.
|
|
985
|
+
perform documents_refresh_totals(p_document_id);
|
|
986
|
+
select * into v_doc from documents where id = p_document_id;
|
|
987
|
+
|
|
988
|
+
select c.currency_code into v_home from companies c where c.id = v_doc.company_id;
|
|
989
|
+
v_rate := v_doc.exchange_rate;
|
|
990
|
+
v_foreign := v_doc.currency_code <> v_home;
|
|
991
|
+
v_round := rounding_of(v_doc.company_id, v_doc.currency_code);
|
|
992
|
+
v_book_round := rounding_of(v_doc.company_id);
|
|
993
|
+
|
|
994
|
+
v_journal := coalesce(
|
|
995
|
+
v_doc.journal_id,
|
|
996
|
+
case when v_is_sale
|
|
997
|
+
then (select sales_journal_id from companies where id = v_doc.company_id)
|
|
998
|
+
else (select purchase_journal_id from companies where id = v_doc.company_id)
|
|
999
|
+
end
|
|
1000
|
+
);
|
|
1001
|
+
if v_journal is null then
|
|
1002
|
+
raise exception 'no_journal: set journal_id on the document or a default journal on the company';
|
|
1003
|
+
end if;
|
|
1004
|
+
|
|
1005
|
+
perform assert_period_open(v_doc.company_id, v_date, true);
|
|
1006
|
+
|
|
1007
|
+
v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
|
|
1008
|
+
|
|
1009
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
|
|
1010
|
+
description, state, document_id, currency_code)
|
|
1011
|
+
values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
|
|
1012
|
+
coalesce(v_doc.number, v_doc.supplier_reference),
|
|
1013
|
+
v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
|
|
1014
|
+
then ' / ' || v_doc.supplier_reference else '' end,
|
|
1015
|
+
'draft', p_document_id, v_doc.currency_code)
|
|
1016
|
+
returning * into v_entry;
|
|
1017
|
+
|
|
1018
|
+
-- ------------------------------------------------------------------ bases
|
|
1019
|
+
for r in
|
|
1020
|
+
select l.account_id,
|
|
1021
|
+
l.tax_id,
|
|
1022
|
+
sum(l.amount_untaxed) as base_amount,
|
|
1023
|
+
min(l.sequence) as seq,
|
|
1024
|
+
string_agg(distinct l.name, ', ') as label
|
|
1025
|
+
from document_lines l
|
|
1026
|
+
where l.document_id = p_document_id
|
|
1027
|
+
and l.line_type = 'product'
|
|
1028
|
+
group by l.account_id, l.tax_id
|
|
1029
|
+
having sum(l.amount_untaxed) <> 0
|
|
1030
|
+
order by 4
|
|
1031
|
+
loop
|
|
1032
|
+
select tp.posting_type, tp.declaration_box, tp.factor_percent, tp.box_factor_percent,
|
|
1033
|
+
coalesce(t.cash_basis, false) as cash_basis
|
|
1034
|
+
into p
|
|
1035
|
+
from tax_postings tp
|
|
1036
|
+
join taxes t on t.id = tp.tax_id
|
|
1037
|
+
where tp.tax_id = r.tax_id
|
|
1038
|
+
and tp.document_kind = v_kind
|
|
1039
|
+
and tp.posting_type = 'base'
|
|
1040
|
+
limit 1;
|
|
1041
|
+
|
|
1042
|
+
v_amount := round_amount(r.base_amount * coalesce(p.factor_percent, 100) / 100, v_round);
|
|
1043
|
+
v_book := round_amount(v_amount / v_rate, v_book_round);
|
|
1044
|
+
v_seq := v_seq + 10;
|
|
1045
|
+
|
|
1046
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
1047
|
+
debit, credit, tax_id, tax_line, posting_type,
|
|
1048
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
1049
|
+
values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
|
|
1050
|
+
case when v_base_credit then 0 else v_book end,
|
|
1051
|
+
case when v_base_credit then v_book else 0 end,
|
|
1052
|
+
r.tax_id, false, p.posting_type,
|
|
1053
|
+
case when coalesce(p.cash_basis, false) then null else p.declaration_box end,
|
|
1054
|
+
case when p.declaration_box is null then null
|
|
1055
|
+
else round_amount(r.base_amount * coalesce(p.box_factor_percent, 100) / 100 / v_rate,
|
|
1056
|
+
v_book_round) end,
|
|
1057
|
+
v_doc.currency_code,
|
|
1058
|
+
case when v_foreign then v_amount end);
|
|
1059
|
+
end loop;
|
|
1060
|
+
|
|
1061
|
+
-- ------------------------------------------------------------------ taxes
|
|
1062
|
+
for r in
|
|
1063
|
+
select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
|
|
1064
|
+
from document_tax_summary s
|
|
1065
|
+
where s.document_id = p_document_id
|
|
1066
|
+
and s.tax_id is not null
|
|
1067
|
+
and s.tax_amount <> 0
|
|
1068
|
+
order by s.tax_code
|
|
1069
|
+
loop
|
|
1070
|
+
select t.cash_basis, t.cash_basis_transition_account_id
|
|
1071
|
+
into v_cash, v_transition
|
|
1072
|
+
from taxes t where t.id = r.tax_id;
|
|
1073
|
+
|
|
1074
|
+
if v_cash then
|
|
1075
|
+
-- A tax that waits needs somewhere to wait. Refuse by name rather than
|
|
1076
|
+
-- book it on the account it is due on, which would make it due.
|
|
1077
|
+
if v_transition is null then
|
|
1078
|
+
raise exception 'no_cash_basis_account: tax % falls due on collection and names no transition account',
|
|
1079
|
+
r.tax_code;
|
|
1080
|
+
end if;
|
|
1081
|
+
-- One posting per side, or the transition lines of a document cannot be
|
|
1082
|
+
-- told apart when the matching sends each of them on. A tax whose
|
|
1083
|
+
-- postings net out has nothing waiting to collect anyway.
|
|
1084
|
+
select count(*) into v_postings
|
|
1085
|
+
from tax_postings tp
|
|
1086
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
1087
|
+
and tp.posting_type = 'tax';
|
|
1088
|
+
if v_postings > 1 then
|
|
1089
|
+
raise exception 'cash_basis_split_tax: tax % falls due on collection and has % tax postings; it takes one',
|
|
1090
|
+
r.tax_code, v_postings;
|
|
1091
|
+
end if;
|
|
1092
|
+
if exists (select 1 from tax_postings tp
|
|
1093
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
1094
|
+
and tp.posting_type = 'tax_on_base') then
|
|
1095
|
+
raise exception 'cash_basis_tax_on_base: tax % falls due on collection and carries a non-deductible share; a cost is not deferred',
|
|
1096
|
+
r.tax_code;
|
|
1097
|
+
end if;
|
|
1098
|
+
-- And it needs a box to fall due *into*. `settle_cash_basis_tax()` only
|
|
1099
|
+
-- ever looks at lines that carry a `box_amount`, and a posting with no
|
|
1100
|
+
-- `declaration_box` produces none — so the amount would sit on the
|
|
1101
|
+
-- transition account for ever, settled by nothing and reported by
|
|
1102
|
+
-- nothing, with no error anywhere. Refuse it here, where the pack can
|
|
1103
|
+
-- still be corrected, rather than discover it in a balance years later.
|
|
1104
|
+
if not exists (select 1 from tax_postings tp
|
|
1105
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
1106
|
+
and tp.posting_type = 'tax'
|
|
1107
|
+
and tp.declaration_box is not null) then
|
|
1108
|
+
raise exception 'no_cash_basis_box: tax % falls due on collection and its posting names no declaration box; the amount would wait on the transition account and never settle',
|
|
1109
|
+
r.tax_code;
|
|
1110
|
+
end if;
|
|
1111
|
+
end if;
|
|
1112
|
+
|
|
1113
|
+
-- The postings of one side share out the tax of the group; the last of
|
|
1114
|
+
-- each side takes what is left. Until `tax_on_base` there was never more
|
|
1115
|
+
-- than one posting per side, so this changes no existing tax by a cent —
|
|
1116
|
+
-- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
|
|
1117
|
+
-- where rounding each half on its own would book 0.64 against a document
|
|
1118
|
+
-- that totals 0.63.
|
|
1119
|
+
v_side_left := null;
|
|
1120
|
+
v_side_left_neg := null;
|
|
1121
|
+
|
|
1122
|
+
for p in
|
|
1123
|
+
select tp.posting_type, tp.factor_percent, tp.account_id,
|
|
1124
|
+
tp.declaration_box, tp.box_factor_percent,
|
|
1125
|
+
case when tp.factor_percent >= 0 then 1 else -1 end as side,
|
|
1126
|
+
sum(abs(tp.factor_percent))
|
|
1127
|
+
over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
|
|
1128
|
+
as side_factor,
|
|
1129
|
+
row_number() over (
|
|
1130
|
+
partition by case when tp.factor_percent >= 0 then 1 else -1 end
|
|
1131
|
+
order by tp.sequence, tp.id)
|
|
1132
|
+
= count(*) over (
|
|
1133
|
+
partition by case when tp.factor_percent >= 0 then 1 else -1 end)
|
|
1134
|
+
as is_last_of_side
|
|
1135
|
+
from tax_postings tp
|
|
1136
|
+
where tp.tax_id = r.tax_id
|
|
1137
|
+
and tp.document_kind = v_kind
|
|
1138
|
+
and tp.posting_type in ('tax', 'tax_on_base')
|
|
1139
|
+
order by tp.sequence, tp.id
|
|
1140
|
+
loop
|
|
1141
|
+
-- The tax of the group was rounded once, in the view. Every posting is
|
|
1142
|
+
-- a share of that one figure, never of a re-derived one.
|
|
1143
|
+
if p.side >= 0 then
|
|
1144
|
+
if v_side_left is null then
|
|
1145
|
+
v_side_left := round_amount(r.tax_amount * p.side_factor / 100, v_round);
|
|
1146
|
+
end if;
|
|
1147
|
+
if p.is_last_of_side then
|
|
1148
|
+
v_amount := v_side_left;
|
|
1149
|
+
else
|
|
1150
|
+
v_amount := round_amount(r.tax_amount * abs(p.factor_percent) / 100, v_round);
|
|
1151
|
+
v_side_left := v_side_left - v_amount;
|
|
1152
|
+
end if;
|
|
1153
|
+
else
|
|
1154
|
+
if v_side_left_neg is null then
|
|
1155
|
+
v_side_left_neg := round_amount(r.tax_amount * p.side_factor / 100, v_round);
|
|
1156
|
+
end if;
|
|
1157
|
+
if p.is_last_of_side then
|
|
1158
|
+
v_amount := v_side_left_neg;
|
|
1159
|
+
else
|
|
1160
|
+
v_amount := round_amount(r.tax_amount * abs(p.factor_percent) / 100, v_round);
|
|
1161
|
+
v_side_left_neg := v_side_left_neg - v_amount;
|
|
1162
|
+
end if;
|
|
1163
|
+
end if;
|
|
1164
|
+
|
|
1165
|
+
if v_amount = 0 then
|
|
1166
|
+
continue;
|
|
1167
|
+
end if;
|
|
1168
|
+
-- A positive factor keeps the side of the base, a negative one flips it.
|
|
1169
|
+
v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
|
|
1170
|
+
-- The box keeps its own rounding: `box_factor_percent` was always
|
|
1171
|
+
-- independent from `factor_percent`, because a declaration figure is
|
|
1172
|
+
-- not a ledger figure and only the ledger has to balance.
|
|
1173
|
+
v_box_amount := case when p.declaration_box is null then null
|
|
1174
|
+
else round_amount(r.tax_amount * p.box_factor_percent / 100 / v_rate,
|
|
1175
|
+
v_book_round) end;
|
|
1176
|
+
v_book := round_amount(v_amount / v_rate, v_book_round);
|
|
1177
|
+
|
|
1178
|
+
if p.posting_type = 'tax' then
|
|
1179
|
+
v_seq := v_seq + 10;
|
|
1180
|
+
|
|
1181
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
1182
|
+
debit, credit, tax_id, tax_line, posting_type,
|
|
1183
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
1184
|
+
values (v_entry.id, v_doc.company_id,
|
|
1185
|
+
case when v_cash then v_transition else p.account_id end,
|
|
1186
|
+
v_seq, r.tax_name,
|
|
1187
|
+
case when v_side_credit then 0 else v_book end,
|
|
1188
|
+
case when v_side_credit then v_book else 0 end,
|
|
1189
|
+
r.tax_id, true, p.posting_type,
|
|
1190
|
+
case when v_cash then null else p.declaration_box end,
|
|
1191
|
+
v_box_amount, v_doc.currency_code,
|
|
1192
|
+
case when v_foreign then v_amount end);
|
|
1193
|
+
continue;
|
|
1194
|
+
end if;
|
|
1195
|
+
|
|
1196
|
+
-- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
|
|
1197
|
+
-- lines it taxes, split in proportion to their base. The last share
|
|
1198
|
+
-- takes whatever is left, so the shares add up to the amount that was
|
|
1199
|
+
-- rounded once on the group and the entry still balances to the cent.
|
|
1200
|
+
v_left := v_amount;
|
|
1201
|
+
v_left_box := v_box_amount;
|
|
1202
|
+
|
|
1203
|
+
for g in
|
|
1204
|
+
select account_id,
|
|
1205
|
+
base_amount,
|
|
1206
|
+
seq,
|
|
1207
|
+
sum(base_amount) over () as total_base,
|
|
1208
|
+
row_number() over (order by seq) = count(*) over () as is_last
|
|
1209
|
+
from (
|
|
1210
|
+
select l.account_id,
|
|
1211
|
+
sum(l.amount_untaxed) as base_amount,
|
|
1212
|
+
min(l.sequence) as seq
|
|
1213
|
+
from document_lines l
|
|
1214
|
+
where l.document_id = p_document_id
|
|
1215
|
+
and l.line_type = 'product'
|
|
1216
|
+
and l.tax_id = r.tax_id
|
|
1217
|
+
group by l.account_id
|
|
1218
|
+
having sum(l.amount_untaxed) <> 0
|
|
1219
|
+
) as groups
|
|
1220
|
+
order by seq
|
|
1221
|
+
loop
|
|
1222
|
+
if g.is_last then
|
|
1223
|
+
v_share := v_left;
|
|
1224
|
+
v_share_box := v_left_box;
|
|
1225
|
+
else
|
|
1226
|
+
v_share := round_amount(v_amount * g.base_amount / g.total_base, v_round);
|
|
1227
|
+
v_share_box := case when v_box_amount is null then null
|
|
1228
|
+
else round_amount(v_box_amount * g.base_amount / g.total_base,
|
|
1229
|
+
v_book_round) end;
|
|
1230
|
+
v_left := v_left - v_share;
|
|
1231
|
+
v_left_box := v_left_box - v_share_box;
|
|
1232
|
+
end if;
|
|
1233
|
+
|
|
1234
|
+
if v_share = 0 then
|
|
1235
|
+
continue;
|
|
1236
|
+
end if;
|
|
1237
|
+
|
|
1238
|
+
v_seq := v_seq + 10;
|
|
1239
|
+
v_share_book := round_amount(v_share / v_rate, v_book_round);
|
|
1240
|
+
|
|
1241
|
+
-- `tax_line` stays false: the amount is on a base account and belongs
|
|
1242
|
+
-- to the base side of the declaration, which is why the Belgian grids
|
|
1243
|
+
-- 82 and 83 report it together with the base.
|
|
1244
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
1245
|
+
debit, credit, tax_id, tax_line, posting_type,
|
|
1246
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
1247
|
+
values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
|
|
1248
|
+
case when v_side_credit then 0 else v_share_book end,
|
|
1249
|
+
case when v_side_credit then v_share_book else 0 end,
|
|
1250
|
+
r.tax_id, false, p.posting_type,
|
|
1251
|
+
p.declaration_box, v_share_box, v_doc.currency_code,
|
|
1252
|
+
case when v_foreign then v_share end);
|
|
1253
|
+
end loop;
|
|
1254
|
+
end loop;
|
|
1255
|
+
end loop;
|
|
1256
|
+
|
|
1257
|
+
-- ------------------------------------------------------------ counterpart
|
|
1258
|
+
select total_debit - total_credit into v_diff from entries where id = v_entry.id;
|
|
1259
|
+
select coalesce(sum(case when l.debit > 0 then l.amount_currency else -l.amount_currency end), 0)
|
|
1260
|
+
into v_diff_cur
|
|
1261
|
+
from entry_lines l where l.entry_id = v_entry.id;
|
|
1262
|
+
|
|
1263
|
+
if v_diff = 0 then
|
|
1264
|
+
raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
|
|
1265
|
+
end if;
|
|
1266
|
+
|
|
1267
|
+
v_contact := commercial_entity(v_doc.contact_id);
|
|
1268
|
+
v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
|
|
1269
|
+
|
|
1270
|
+
select payment_terms_days into v_terms from contacts where id = v_contact;
|
|
1271
|
+
v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
|
|
1272
|
+
|
|
1273
|
+
v_amount := abs(v_diff);
|
|
1274
|
+
-- The counterpart balances the entry in both currencies. The total it is
|
|
1275
|
+
-- checked against is the document's own, which is the currency
|
|
1276
|
+
-- `amount_total` is stated in.
|
|
1277
|
+
v_total_cur := case when v_foreign then abs(v_diff_cur) else v_amount end;
|
|
1278
|
+
v_seq := v_seq + 10;
|
|
1279
|
+
|
|
1280
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
1281
|
+
debit, credit, contact_id, date_maturity, currency_code,
|
|
1282
|
+
amount_currency)
|
|
1283
|
+
values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
|
|
1284
|
+
case when v_diff > 0 then 0 else v_amount end,
|
|
1285
|
+
case when v_diff > 0 then v_amount else 0 end,
|
|
1286
|
+
v_contact, v_maturity, v_doc.currency_code,
|
|
1287
|
+
case when v_foreign then v_total_cur end);
|
|
1288
|
+
|
|
1289
|
+
-- The ledger is right by construction. If the header disagrees, the header
|
|
1290
|
+
-- is what is wrong, and we say so instead of quietly patching a line.
|
|
1291
|
+
-- Half a unit of the document's own currency, which is what `0.005` used to
|
|
1292
|
+
-- mean when every currency was assumed to have cents.
|
|
1293
|
+
if abs(v_total_cur - abs(v_doc.amount_total)) > currency_unit(v_round) / 2 then
|
|
1294
|
+
raise exception 'document_total_mismatch: document % totals % but its lines book %',
|
|
1295
|
+
p_document_id, v_doc.amount_total, v_total_cur;
|
|
1296
|
+
end if;
|
|
1297
|
+
|
|
1298
|
+
-- --------------------------------------------------------------- posting
|
|
1299
|
+
v_entry := post_entry(v_entry.id);
|
|
1300
|
+
|
|
1301
|
+
update documents
|
|
1302
|
+
set state = 'posted',
|
|
1303
|
+
number = coalesce(number, v_entry.number),
|
|
1304
|
+
entry_id = v_entry.id,
|
|
1305
|
+
accounting_date = v_date
|
|
1306
|
+
where id = p_document_id;
|
|
1307
|
+
|
|
1308
|
+
return v_entry;
|
|
1309
|
+
end;
|
|
1310
|
+
$$;
|
|
1311
|
+
|
|
1312
|
+
comment on function post_document(uuid) is
|
|
1313
|
+
'Books a document: one entry, the bases on the accounts of the lines, the tax of each group rounded once and shared over the postings of the tax, each ledger line naming the posting type that wrote it. Refuses a tax whose applies_*_territory the document contradicts, by name, before anything reaches the ledger — and chooses no tax for anybody.';
|
|
1314
|
+
|
|
1315
|
+
-- No new table, so no new policy: the six columns sit on tables whose row
|
|
1316
|
+
-- level security already decides who may read them, and `territories` keeps
|
|
1317
|
+
-- the select policy it was created with. The two new functions are granted
|
|
1318
|
+
-- above, beside their revokes.
|