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,75 @@
|
|
|
1
|
+
-- Ekwo OS — two definer functions checked nobody.
|
|
2
|
+
--
|
|
3
|
+
-- Both were found by `tests/client_preset.test.ts`, which calls every volatile
|
|
4
|
+
-- function a signed-in person may execute, as a guest of the company, and
|
|
5
|
+
-- compares the whole database before and after. Two of them returned quietly.
|
|
6
|
+
--
|
|
7
|
+
-- **`catch_up_journal_sequence()`.** It is SECURITY DEFINER because
|
|
8
|
+
-- `journal_sequences` has a select policy and no other, and its header said
|
|
9
|
+
-- "the caller has already been checked by `post_entry()`". That is true of the
|
|
10
|
+
-- call `post_entry()` makes and of no other: the function is executable by
|
|
11
|
+
-- `authenticated`, takes a journal id, and advanced the counter of that
|
|
12
|
+
-- journal to whatever number it was handed — for a member who only reads, and
|
|
13
|
+
-- for somebody who is not a member of the company at all. `greatest()` kept it
|
|
14
|
+
-- from going backwards, so what it allowed was a hole: the next entry of that
|
|
15
|
+
-- journal numbered 9999 after 0042, in a country whose law forbids exactly
|
|
16
|
+
-- that. It is the sentence `next_entry_number()` was given a guard for on 11
|
|
17
|
+
-- September — "a way to burn numbers in somebody else's journal" — left open
|
|
18
|
+
-- on the function beside it.
|
|
19
|
+
--
|
|
20
|
+
-- It now asks what `next_entry_number()` asks, in the same words: the
|
|
21
|
+
-- installer, or `entries.post` on the company of the journal. Whoever posts an
|
|
22
|
+
-- imported entry holds it, since posting is what calls this. Revoking EXECUTE
|
|
23
|
+
-- instead was not possible: `post_entry()` runs as its caller.
|
|
24
|
+
--
|
|
25
|
+
-- **`touch_api_key()`.** Called by `use_api_key()`, which is definer, so the
|
|
26
|
+
-- inner call runs as the owner and needs no grant of its own. It had one
|
|
27
|
+
-- anyway, from the default privileges of the schema, and stamped
|
|
28
|
+
-- `last_used_at` on any key whose id it was given. The id is not secret the
|
|
29
|
+
-- way the key is, but "this key has not been used for a year" is the signal an
|
|
30
|
+
-- operator withdraws keys on, and anybody signed in could reset it. Nobody but
|
|
31
|
+
-- `use_api_key()` has a reason to call it, so nobody else may.
|
|
32
|
+
|
|
33
|
+
create or replace function catch_up_journal_sequence(p_journal_id uuid, p_date date, p_number text)
|
|
34
|
+
returns void
|
|
35
|
+
language plpgsql
|
|
36
|
+
security definer
|
|
37
|
+
set search_path = public, pg_temp
|
|
38
|
+
as $$
|
|
39
|
+
declare
|
|
40
|
+
v_company uuid;
|
|
41
|
+
v_format text;
|
|
42
|
+
v_period smallint;
|
|
43
|
+
v_counter integer;
|
|
44
|
+
begin
|
|
45
|
+
select j.company_id into v_company from journals j where j.id = p_journal_id;
|
|
46
|
+
if v_company is null then
|
|
47
|
+
return;
|
|
48
|
+
end if;
|
|
49
|
+
|
|
50
|
+
if not is_installer() and not has_capability(v_company, 'entries.post') then
|
|
51
|
+
raise exception 'not_allowed: moving a counter in this company needs entries.post'
|
|
52
|
+
using errcode = '42501';
|
|
53
|
+
end if;
|
|
54
|
+
|
|
55
|
+
select number_format into v_format from numbering_rules(v_company);
|
|
56
|
+
v_counter := number_counter(v_format, p_number);
|
|
57
|
+
if v_counter is null then
|
|
58
|
+
return;
|
|
59
|
+
end if;
|
|
60
|
+
|
|
61
|
+
v_period := case when v_format ~ '\{(YYYY|YY)\}'
|
|
62
|
+
then extract(year from p_date)::smallint
|
|
63
|
+
else 0::smallint end;
|
|
64
|
+
|
|
65
|
+
insert into journal_sequences (journal_id, year, last_number)
|
|
66
|
+
values (p_journal_id, v_period, v_counter)
|
|
67
|
+
on conflict (journal_id, year)
|
|
68
|
+
do update set last_number = greatest(journal_sequences.last_number, excluded.last_number);
|
|
69
|
+
end;
|
|
70
|
+
$$;
|
|
71
|
+
|
|
72
|
+
comment on function catch_up_journal_sequence(uuid, date, text) is
|
|
73
|
+
'Advances the counter of a journal to the counter inside a number that was written by hand, so the next automatic number continues the series. Definer, because nobody writes `journal_sequences` directly — and guarded like `next_entry_number()`: the installer, or entries.post on the company of the journal.';
|
|
74
|
+
|
|
75
|
+
revoke execute on function touch_api_key(uuid) from public, anon, authenticated;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
-- Ekwo OS — every company somebody keeps, read at once.
|
|
2
|
+
--
|
|
3
|
+
-- `upcoming_filings()` and `filings_touched_since()` take a company. Several
|
|
4
|
+
-- companies inside one instance is the normal case — a firm with its clients —
|
|
5
|
+
-- and a firm that keeps forty of them does not ask "what is due" forty times.
|
|
6
|
+
-- It asks once: *all the companies I keep, what falls due in the fortnight,
|
|
7
|
+
-- and what moved after it went.*
|
|
8
|
+
--
|
|
9
|
+
-- **There is no firm in this file, and no list of clients.** The portfolio is
|
|
10
|
+
-- not an object: it is what the caller may read. Both functions walk the
|
|
11
|
+
-- companies on which the caller holds `filings.read`, and that one sentence is
|
|
12
|
+
-- the whole model — an accountant of forty companies gets forty, the person
|
|
13
|
+
-- who runs one of them gets one, and a group that keeps three of its own gets
|
|
14
|
+
-- three without anybody having been called a firm.
|
|
15
|
+
--
|
|
16
|
+
-- **Both are `security invoker`.** They read nothing a caller could not have
|
|
17
|
+
-- read by asking company by company: row level security on `tax_filings`, on
|
|
18
|
+
-- `company_filing_periods` and on the ledger does the sorting, as it does for
|
|
19
|
+
-- the two functions these are built on. The one thing the policies do not
|
|
20
|
+
-- settle is *which companies to walk* — `companies` is readable by a member
|
|
21
|
+
-- who has had `filings.read` taken away, and by the administrator of the
|
|
22
|
+
-- instance, who creates companies and keeps none of their books. Walking those
|
|
23
|
+
-- would list a calendar with every state empty, which reads as "nothing has
|
|
24
|
+
-- been prepared" and means "you may not know". So the list is filtered on the
|
|
25
|
+
-- capability, in the open, and the test takes the capability away to see the
|
|
26
|
+
-- company leave.
|
|
27
|
+
--
|
|
28
|
+
-- **Silence is not an output.** A reading that leaves a company out when it
|
|
29
|
+
-- has nothing to say cannot be told from a reading that never looked at it, and
|
|
30
|
+
-- over forty companies that is the difference that matters. So every company
|
|
31
|
+
-- of the portfolio is in every answer, and a row with no date says why:
|
|
32
|
+
--
|
|
33
|
+
-- no_deadline_rule the pack names no day for this form — a country whose
|
|
34
|
+
-- schedule depends on who is filing — and the period is
|
|
35
|
+
-- listed without one rather than dropped
|
|
36
|
+
-- nothing_due the company files, and nothing of it falls in the window
|
|
37
|
+
-- no_form no cadence recorded and no periodic return the
|
|
38
|
+
-- installation could name for its fiscal country
|
|
39
|
+
--
|
|
40
|
+
-- **The window is on the day a return is due**, which is the question a
|
|
41
|
+
-- portfolio is asked, and not on the period as `upcoming_filings()` reads it.
|
|
42
|
+
-- A period whose pack names no day has no date to test, so it is listed while
|
|
43
|
+
-- the month that follows it overlaps the window — the only place either rule
|
|
44
|
+
-- of the closed vocabulary ever puts a date.
|
|
45
|
+
|
|
46
|
+
-- ---------------------------------------------------------------------------
|
|
47
|
+
-- portfolio_upcoming_filings
|
|
48
|
+
-- ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
create or replace function portfolio_upcoming_filings(
|
|
51
|
+
p_from date,
|
|
52
|
+
p_to date
|
|
53
|
+
)
|
|
54
|
+
returns table (
|
|
55
|
+
company_id uuid,
|
|
56
|
+
company_name text,
|
|
57
|
+
report_code text,
|
|
58
|
+
period_start date,
|
|
59
|
+
period_end date,
|
|
60
|
+
due_date date,
|
|
61
|
+
state tax_filing_state,
|
|
62
|
+
filing_id uuid,
|
|
63
|
+
reason text
|
|
64
|
+
)
|
|
65
|
+
language sql
|
|
66
|
+
stable
|
|
67
|
+
security invoker
|
|
68
|
+
as $$
|
|
69
|
+
with portfolio as (
|
|
70
|
+
select c.id, c.name
|
|
71
|
+
from companies c
|
|
72
|
+
where has_capability(c.id, 'filings.read')
|
|
73
|
+
),
|
|
74
|
+
-- A return is due in the month that follows its period, plus the days an
|
|
75
|
+
-- administration grants. So the periods that can fall due from `p_from` on
|
|
76
|
+
-- ended at most two months and that many days before it, and the calendar
|
|
77
|
+
-- of a company is asked for once, from there.
|
|
78
|
+
reach as (
|
|
79
|
+
select (p_from - interval '2 month'
|
|
80
|
+
- make_interval(days => coalesce(max(t.deadline_plus_days), 0)))::date as start
|
|
81
|
+
from tax_report_templates t
|
|
82
|
+
),
|
|
83
|
+
listed as (
|
|
84
|
+
select p.id as company_id, u.*
|
|
85
|
+
from portfolio p
|
|
86
|
+
cross join reach r
|
|
87
|
+
cross join lateral upcoming_filings(p.id, r.start, p_to) u
|
|
88
|
+
where case
|
|
89
|
+
when u.due_date is not null then u.due_date between p_from and p_to
|
|
90
|
+
else u.period_end < p_to
|
|
91
|
+
and (date_trunc('month', u.period_end + interval '2 month')
|
|
92
|
+
- interval '1 day')::date >= p_from
|
|
93
|
+
end
|
|
94
|
+
)
|
|
95
|
+
select p.id,
|
|
96
|
+
p.name,
|
|
97
|
+
l.report_code,
|
|
98
|
+
l.period_start,
|
|
99
|
+
l.period_end,
|
|
100
|
+
l.due_date,
|
|
101
|
+
l.state,
|
|
102
|
+
l.filing_id,
|
|
103
|
+
case
|
|
104
|
+
when l.company_id is not null and l.due_date is null then 'no_deadline_rule'
|
|
105
|
+
when l.company_id is not null then null
|
|
106
|
+
when periodic_return_code(p.id) is null
|
|
107
|
+
and not exists (select 1 from company_filing_periods f
|
|
108
|
+
where f.company_id = p.id) then 'no_form'
|
|
109
|
+
else 'nothing_due'
|
|
110
|
+
end
|
|
111
|
+
from portfolio p
|
|
112
|
+
left join listed l on l.company_id = p.id
|
|
113
|
+
order by l.due_date nulls last, l.period_end nulls last, p.name, p.id, l.report_code;
|
|
114
|
+
$$;
|
|
115
|
+
|
|
116
|
+
comment on function portfolio_upcoming_filings(date, date) is
|
|
117
|
+
'What falls due between two dates in every company the caller holds filings.read on, and in no other: the periods upcoming_filings() produces, kept when the day they are due is in the window. Every company of the portfolio is in the answer at least once, and a row without a date says why in `reason` — no_deadline_rule where the pack names no day for the form, nothing_due where nothing of the company falls in the window, no_form where the installation carries no return for it. Invoker: it reads what the caller could have read company by company.';
|
|
118
|
+
|
|
119
|
+
revoke execute on function portfolio_upcoming_filings(date, date) from public, anon;
|
|
120
|
+
grant execute on function portfolio_upcoming_filings(date, date) to authenticated, service_role;
|
|
121
|
+
|
|
122
|
+
-- ---------------------------------------------------------------------------
|
|
123
|
+
-- portfolio_filings_touched_since
|
|
124
|
+
--
|
|
125
|
+
-- `filed` is how many declarations of the company had gone and were looked
|
|
126
|
+
-- at, so that a company with no row of disturbance still says what was
|
|
127
|
+
-- examined: six returns and none moved is not the same news as no return.
|
|
128
|
+
-- ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
create or replace function portfolio_filings_touched_since(
|
|
131
|
+
p_from date default null,
|
|
132
|
+
p_to date default null
|
|
133
|
+
)
|
|
134
|
+
returns table (
|
|
135
|
+
company_id uuid,
|
|
136
|
+
company_name text,
|
|
137
|
+
filed integer,
|
|
138
|
+
filing_id uuid,
|
|
139
|
+
report_code text,
|
|
140
|
+
period_start date,
|
|
141
|
+
period_end date,
|
|
142
|
+
state tax_filing_state,
|
|
143
|
+
filed_at timestamptz,
|
|
144
|
+
entries integer,
|
|
145
|
+
last_entry_at timestamptz,
|
|
146
|
+
boxes_moved integer
|
|
147
|
+
)
|
|
148
|
+
language sql
|
|
149
|
+
stable
|
|
150
|
+
security invoker
|
|
151
|
+
as $$
|
|
152
|
+
with portfolio as (
|
|
153
|
+
select c.id, c.name
|
|
154
|
+
from companies c
|
|
155
|
+
where has_capability(c.id, 'filings.read')
|
|
156
|
+
)
|
|
157
|
+
select p.id,
|
|
158
|
+
p.name,
|
|
159
|
+
(select count(*)::integer
|
|
160
|
+
from tax_filings f
|
|
161
|
+
where f.company_id = p.id
|
|
162
|
+
and f.filed_at is not null
|
|
163
|
+
and f.state <> 'superseded'
|
|
164
|
+
and (p_from is null or f.period_end >= p_from)
|
|
165
|
+
and (p_to is null or f.period_start <= p_to)),
|
|
166
|
+
t.filing_id, t.report_code, t.period_start, t.period_end, t.state,
|
|
167
|
+
t.filed_at, t.entries, t.last_entry_at, t.boxes_moved
|
|
168
|
+
from portfolio p
|
|
169
|
+
left join lateral filings_touched_since(p.id, p_from, p_to) t on true
|
|
170
|
+
order by t.last_entry_at desc nulls last, p.name, p.id, t.period_start, t.report_code;
|
|
171
|
+
$$;
|
|
172
|
+
|
|
173
|
+
comment on function portfolio_filings_touched_since(date, date) is
|
|
174
|
+
'Declarations that have gone and whose period the ledger moved afterwards, in every company the caller holds filings.read on, and in no other — filings_touched_since() with the company named, the latest disturbance first. Every company of the portfolio is in the answer at least once: one that was not disturbed is a row with no filing, and `filed` says how many of its declarations were looked at. Invoker: it reads what the caller could have read company by company.';
|
|
175
|
+
|
|
176
|
+
revoke execute on function portfolio_filings_touched_since(date, date) from public, anon;
|
|
177
|
+
grant execute on function portfolio_filings_touched_since(date, date) to authenticated, service_role;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- A session nobody prepared is not the installer
|
|
3
|
+
-- ---------------------------------------------------------------------------
|
|
4
|
+
-- `is_installer()` ended on `current_setting('ekwo.installing', true) = 'on'`.
|
|
5
|
+
-- A setting that was set and emptied answers '', and '' = 'on' is false. A
|
|
6
|
+
-- setting that was NEVER set answers NULL, and NULL = 'on' is NULL — so on a
|
|
7
|
+
-- new connection the function answered NULL, and every guard written
|
|
8
|
+
--
|
|
9
|
+
-- if not is_installer() and not has_capability(…) then raise …
|
|
10
|
+
--
|
|
11
|
+
-- evaluated `NULL and true`, which is NULL, which `if` does not take. The
|
|
12
|
+
-- guard was skipped. A person always carries a `sub` and is answered by
|
|
13
|
+
-- `has_capability()`; the caller with neither a session nor a key — the
|
|
14
|
+
-- `service_role` through PostgREST, a direct connection — walked through
|
|
15
|
+
-- create_company(), the counters, the API keys and the module switches.
|
|
16
|
+
--
|
|
17
|
+
-- The migration that introduced the function was the one about guards that
|
|
18
|
+
-- could answer NULL, and its tests never saw this one: the harness always set
|
|
19
|
+
-- the variable, to 'on' for the owner and to '' for a person. A test now
|
|
20
|
+
-- reloads the database into an instance where nothing was ever set
|
|
21
|
+
-- (tests/fresh_session.test.ts) and asks every argument-less boolean helper
|
|
22
|
+
-- the same question.
|
|
23
|
+
--
|
|
24
|
+
-- `service_role` bypasses row level security on Supabase, by design of the
|
|
25
|
+
-- platform. It does not bypass a function that raises — which is why the
|
|
26
|
+
-- functions are where an installation's rules are, and why this mattered.
|
|
27
|
+
-- ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
create or replace function is_installer()
|
|
30
|
+
returns boolean
|
|
31
|
+
language sql
|
|
32
|
+
stable
|
|
33
|
+
as $$
|
|
34
|
+
select auth.uid() is null
|
|
35
|
+
and nullif(current_setting('ekwo.api_key', true), '') is null
|
|
36
|
+
and coalesce(current_setting('ekwo.installing', true) = 'on', false);
|
|
37
|
+
$$;
|
|
38
|
+
|
|
39
|
+
comment on function is_installer() is
|
|
40
|
+
'Whether the caller is the installation itself — the migration runner, the seeds, the CLI — rather than a person or a machine key. Set by the runner on its own connection; a session or a key can never be it, and neither can a connection where the setting was never made: the answer is false there, never NULL, because the guards negate it.';
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
-- Ekwo OS — a line keeps the category and the rate it was posted with.
|
|
2
|
+
--
|
|
3
|
+
-- `document_lines.vat_category` and `.vat_rate` have been there since the
|
|
4
|
+
-- table was, and three migrations have cited them as the precedent for a
|
|
5
|
+
-- snapshot: `20260915191200` for the language of a document, `20260915195000`
|
|
6
|
+
-- for a price that holds its tax, `20260915181000` for a territory. Nothing
|
|
7
|
+
-- ever wrote them. Every line of every golden year has them null, the view
|
|
8
|
+
-- publishes them as BT-151 and BT-152 all the same, and the first reader that
|
|
9
|
+
-- needed them — the brick that writes a Peppol invoice — found nothing and
|
|
10
|
+
-- went round by `tax_id`. `tests/vat_category.test.ts` had found it earlier
|
|
11
|
+
-- and said so in a comment: "Nothing in the core writes it."
|
|
12
|
+
--
|
|
13
|
+
-- The two ways out were to fill the columns or to take them out of the view.
|
|
14
|
+
-- They are filled, and the reason is the one those three migrations gave
|
|
15
|
+
-- while believing it already true: **what is posted does not move.** A tax
|
|
16
|
+
-- belongs to a company and a pack upgrade rewrites it in place — `amount`,
|
|
17
|
+
-- `vat_category` and the rest, in one `update taxes` — so a view that reads
|
|
18
|
+
-- the category and the rate of a sent invoice through `tax_id` reads what the
|
|
19
|
+
-- pack says today. A rate that changes on 1 January would restate every
|
|
20
|
+
-- invoice of December the first time anybody rendered one.
|
|
21
|
+
--
|
|
22
|
+
-- Four decisions.
|
|
23
|
+
--
|
|
24
|
+
-- **Derived, never keyed.** The category and the rate are facts about the tax
|
|
25
|
+
-- and not choices of the line, which is what `unit_price_includes_tax` is
|
|
26
|
+
-- already: a caller that writes them is overwritten while the document is a
|
|
27
|
+
-- draft. `vat_rate` is a percentage, so a tax that is a fixed amount leaves it
|
|
28
|
+
-- null — BT-152 has no room for an amount, and `post_document()` refuses such
|
|
29
|
+
-- a tax anyway.
|
|
30
|
+
--
|
|
31
|
+
-- **A draft follows its tax; a posted line refuses to move.** The same shape
|
|
32
|
+
-- as `documents_guard_language`: the line trigger reads the tax on every write
|
|
33
|
+
-- of a draft, a tax that changes reaches the drafts that carry it, and from
|
|
34
|
+
-- the moment the document is no longer a draft a change to either column is
|
|
35
|
+
-- refused by name, `document_line_tax_frozen`. Refused rather than quietly
|
|
36
|
+
-- ignored, because the one caller with a reason to write these columns is an
|
|
37
|
+
-- application that used to do the core's job, and it should learn that it no
|
|
38
|
+
-- longer has to.
|
|
39
|
+
--
|
|
40
|
+
-- **The breakdown reads the line, not the tax.** `document_tax_summary` is
|
|
41
|
+
-- BT-118 and BT-119, and it is also where `documents_refresh_totals()` and
|
|
42
|
+
-- `post_document()` take their figures. Reading the category and the rate
|
|
43
|
+
-- from the tax there while the lines carry a snapshot would have made the two
|
|
44
|
+
-- halves of one invoice disagree the day a rate moves — and would have kept
|
|
45
|
+
-- the older hazard, which is that touching a line of a posted document
|
|
46
|
+
-- recomputes its totals at today's rate. The view now groups on what the lines
|
|
47
|
+
-- carry. For a draft that is the tax, by construction; for a posted document
|
|
48
|
+
-- it is the tax as it stood. Same columns, same order, same types.
|
|
49
|
+
--
|
|
50
|
+
-- **The lines already here take the tax of today, once.** Nobody knows what
|
|
51
|
+
-- rate a tax had on the day an old invoice was posted: the ledger kept the
|
|
52
|
+
-- amounts and not the percentage. The tax as it stands is the only evidence
|
|
53
|
+
-- the installation holds, it is what every read of these documents has
|
|
54
|
+
-- answered until now, and it is what the totals were last computed from — so
|
|
55
|
+
-- the backfill changes no figure. It is a guess for the same reason the
|
|
56
|
+
-- backfill of `documents.language` was, and from here on nothing is guessed.
|
|
57
|
+
-- A line somebody's application had already stamped keeps its stamp.
|
|
58
|
+
--
|
|
59
|
+
-- What this does not close, and says so: the *share* of a tax that reaches the
|
|
60
|
+
-- other party (`tax_charged`) is still read from `tax_postings` as they stand,
|
|
61
|
+
-- and the exemption code and the legal reference of a tax are still read from
|
|
62
|
+
-- the tax. None of the three changes a base or a rate; all three are a pack
|
|
63
|
+
-- correcting itself under a document already sent. And no trigger refuses a
|
|
64
|
+
-- change of `quantity`, `unit_price` or `tax_id` on a posted line: the ledger
|
|
65
|
+
-- is immutable and the document that produced it is only partly so. That is
|
|
66
|
+
-- older than this file and wider than it.
|
|
67
|
+
|
|
68
|
+
-- ---------------------------------------------------------------------------
|
|
69
|
+
-- 1. What the lines already here carry
|
|
70
|
+
--
|
|
71
|
+
-- Before the guard exists, because it would refuse exactly this. The row
|
|
72
|
+
-- triggers fire, as they did for the backfill of `documents.language`: the
|
|
73
|
+
-- totals they recompute are read from the same tax the snapshot is being
|
|
74
|
+
-- taken from, so they come back unchanged.
|
|
75
|
+
-- ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
update document_lines l
|
|
78
|
+
set vat_category = t.vat_category,
|
|
79
|
+
vat_rate = case when t.amount_type = 'percent' then t.amount end
|
|
80
|
+
from taxes t, documents d
|
|
81
|
+
where t.id = l.tax_id
|
|
82
|
+
and d.id = l.document_id
|
|
83
|
+
and l.line_type = 'product'
|
|
84
|
+
and (d.state = 'draft' or (l.vat_category is null and l.vat_rate is null))
|
|
85
|
+
and (l.vat_category is distinct from t.vat_category
|
|
86
|
+
or l.vat_rate is distinct from case when t.amount_type = 'percent' then t.amount end);
|
|
87
|
+
|
|
88
|
+
-- ---------------------------------------------------------------------------
|
|
89
|
+
-- 2. Writing it, and refusing to rewrite it
|
|
90
|
+
-- ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
create or replace function document_lines_snapshot_tax()
|
|
93
|
+
returns trigger
|
|
94
|
+
language plpgsql
|
|
95
|
+
as $$
|
|
96
|
+
declare
|
|
97
|
+
v_state doc_state;
|
|
98
|
+
v_number text;
|
|
99
|
+
begin
|
|
100
|
+
select d.state, d.number into v_state, v_number
|
|
101
|
+
from documents d where d.id = new.document_id;
|
|
102
|
+
|
|
103
|
+
if tg_op = 'UPDATE' and v_state <> 'draft' then
|
|
104
|
+
if new.vat_category is distinct from old.vat_category
|
|
105
|
+
or new.vat_rate is distinct from old.vat_rate then
|
|
106
|
+
raise exception 'document_line_tax_frozen: line % of % was posted under category % at % per cent, and keeps both. A document already issued is not rewritten because its tax changed; credit it and issue another.',
|
|
107
|
+
new.sequence, coalesce(v_number, new.document_id::text),
|
|
108
|
+
coalesce(old.vat_category, 'none'), coalesce(old.vat_rate::text, 'no')
|
|
109
|
+
using errcode = '55006';
|
|
110
|
+
end if;
|
|
111
|
+
return new;
|
|
112
|
+
end if;
|
|
113
|
+
|
|
114
|
+
if new.line_type <> 'product' or new.tax_id is null then
|
|
115
|
+
new.vat_category := null;
|
|
116
|
+
new.vat_rate := null;
|
|
117
|
+
return new;
|
|
118
|
+
end if;
|
|
119
|
+
|
|
120
|
+
select t.vat_category,
|
|
121
|
+
case when t.amount_type = 'percent' then t.amount end
|
|
122
|
+
into new.vat_category, new.vat_rate
|
|
123
|
+
from taxes t where t.id = new.tax_id;
|
|
124
|
+
return new;
|
|
125
|
+
end;
|
|
126
|
+
$$;
|
|
127
|
+
|
|
128
|
+
comment on function document_lines_snapshot_tax() is
|
|
129
|
+
'Writes BT-151 and BT-152 on a line from its tax for as long as the document is a draft, and refuses document_line_tax_frozen on a change to either once it is not.';
|
|
130
|
+
|
|
131
|
+
create trigger document_lines_snapshot_tax
|
|
132
|
+
before insert or update on document_lines
|
|
133
|
+
for each row execute function document_lines_snapshot_tax();
|
|
134
|
+
|
|
135
|
+
-- A trigger body is invoked by its table, never called.
|
|
136
|
+
revoke execute on function document_lines_snapshot_tax() from public, anon, authenticated, service_role;
|
|
137
|
+
|
|
138
|
+
comment on column document_lines.vat_category is
|
|
139
|
+
'EN 16931 BT-151 as the line carries it. Derived from the tax while the document is a draft and frozen when it is posted, so a pack upgrade that recategorises a tax cannot rewrite an invoice that has been sent. Never keyed.';
|
|
140
|
+
|
|
141
|
+
comment on column document_lines.vat_rate is
|
|
142
|
+
'EN 16931 BT-152 as the line carries it: the percentage of the tax, derived while the document is a draft and frozen when it is posted, so a later change of rate cannot rewrite an invoice that has been sent. Null where the tax is not a percentage. Never keyed.';
|
|
143
|
+
|
|
144
|
+
-- ---------------------------------------------------------------------------
|
|
145
|
+
-- 3. A tax that changes, and the drafts that carry it
|
|
146
|
+
--
|
|
147
|
+
-- The other half of "a draft follows its tax": a draft nobody touches again
|
|
148
|
+
-- would keep an answer that is no longer the tax's. Bounded to the draft lines
|
|
149
|
+
-- of that one tax — `document_lines_tax_idx` is the index it reads — and the
|
|
150
|
+
-- write goes through the line's own triggers, so the totals of those drafts
|
|
151
|
+
-- follow the new rate too, which until now they did not until somebody edited
|
|
152
|
+
-- a line.
|
|
153
|
+
--
|
|
154
|
+
-- `price_include` is deliberately not among the columns that fire it. The
|
|
155
|
+
-- lines are rewritten one at a time, and a group caught half way between a
|
|
156
|
+
-- price with its tax and a price without is what `mixed_price_include`
|
|
157
|
+
-- refuses: the flag would make the correction of a tax fail on the first
|
|
158
|
+
-- draft with two lines. That snapshot keeps the rule it was published with.
|
|
159
|
+
--
|
|
160
|
+
-- `security definer`, for the reason `contacts_language_reaches_drafts` gives:
|
|
161
|
+
-- whoever may correct a tax is not thereby allowed to write a document, and
|
|
162
|
+
-- should not be refused because a draft sits behind a policy they do not
|
|
163
|
+
-- satisfy. What is written is derived data on rows nothing has issued yet.
|
|
164
|
+
-- ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
create or replace function taxes_reach_draft_lines()
|
|
167
|
+
returns trigger
|
|
168
|
+
language plpgsql
|
|
169
|
+
security definer
|
|
170
|
+
set search_path = public, pg_temp
|
|
171
|
+
as $$
|
|
172
|
+
begin
|
|
173
|
+
update document_lines l
|
|
174
|
+
set vat_category = new.vat_category
|
|
175
|
+
from documents d
|
|
176
|
+
where l.tax_id = new.id
|
|
177
|
+
and d.id = l.document_id
|
|
178
|
+
and d.state = 'draft';
|
|
179
|
+
return null;
|
|
180
|
+
end;
|
|
181
|
+
$$;
|
|
182
|
+
|
|
183
|
+
comment on function taxes_reach_draft_lines() is
|
|
184
|
+
'A tax whose category, rate or kind of amount changes rewrites the snapshot of the draft lines that carry it, and nothing else: a posted line keeps what it was posted with.';
|
|
185
|
+
|
|
186
|
+
create trigger taxes_reach_draft_lines
|
|
187
|
+
after update of vat_category, amount, amount_type on taxes
|
|
188
|
+
for each row
|
|
189
|
+
when (new.vat_category is distinct from old.vat_category
|
|
190
|
+
or new.amount is distinct from old.amount
|
|
191
|
+
or new.amount_type is distinct from old.amount_type)
|
|
192
|
+
execute function taxes_reach_draft_lines();
|
|
193
|
+
|
|
194
|
+
revoke execute on function taxes_reach_draft_lines() from public, anon, authenticated, service_role;
|
|
195
|
+
|
|
196
|
+
-- ---------------------------------------------------------------------------
|
|
197
|
+
-- 4. The breakdown groups on what the lines carry
|
|
198
|
+
--
|
|
199
|
+
-- `20260915195000` unchanged but for where the category and the rate come
|
|
200
|
+
-- from. `tax_rate` keeps the type it had — a view column cannot change type
|
|
201
|
+
-- under `create or replace` — and the arithmetic reads the same figure it
|
|
202
|
+
-- publishes. `security_invoker`, the comment and the grants are those of the
|
|
203
|
+
-- object it replaces; the grants are restated because saying so again is
|
|
204
|
+
-- cheaper than trusting it.
|
|
205
|
+
-- ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
create or replace view document_tax_summary
|
|
208
|
+
with (security_invoker = true) as
|
|
209
|
+
select l.document_id,
|
|
210
|
+
l.company_id,
|
|
211
|
+
d.doc_type,
|
|
212
|
+
l.tax_id,
|
|
213
|
+
t.code as tax_code,
|
|
214
|
+
t.name as tax_name,
|
|
215
|
+
l.vat_category,
|
|
216
|
+
l.vat_rate::numeric(12, 4) as tax_rate,
|
|
217
|
+
sum(l.amount_untaxed) as base_amount,
|
|
218
|
+
-- Gross tax: what the VAT return reports.
|
|
219
|
+
round_amount(
|
|
220
|
+
case when bool_and(l.unit_price_includes_tax)
|
|
221
|
+
then sum(l.amount_incl_tax)
|
|
222
|
+
- sum(l.amount_incl_tax) / (1 + coalesce(l.vat_rate, 0) / 100)
|
|
223
|
+
else sum(l.amount_untaxed) * coalesce(l.vat_rate, 0) / 100
|
|
224
|
+
end,
|
|
225
|
+
rounding_of(l.company_id, d.currency_code)) as tax_amount,
|
|
226
|
+
-- Charged tax: what the other party actually pays. Zero when the tax
|
|
227
|
+
-- postings net out, which is exactly what self-assessment means.
|
|
228
|
+
round_amount(
|
|
229
|
+
case when bool_and(l.unit_price_includes_tax)
|
|
230
|
+
then sum(l.amount_incl_tax)
|
|
231
|
+
- sum(l.amount_incl_tax) / (1 + coalesce(l.vat_rate, 0) / 100)
|
|
232
|
+
else sum(l.amount_untaxed) * coalesce(l.vat_rate, 0) / 100
|
|
233
|
+
end
|
|
234
|
+
* coalesce((
|
|
235
|
+
select sum(tp.factor_percent)
|
|
236
|
+
from tax_postings tp
|
|
237
|
+
where tp.tax_id = t.id
|
|
238
|
+
and tp.posting_type in ('tax', 'tax_on_base')
|
|
239
|
+
and tp.document_kind = case
|
|
240
|
+
when d.doc_type in ('sale_credit_note', 'purchase_credit_note')
|
|
241
|
+
then 'credit_note'::tax_document_kind
|
|
242
|
+
else 'invoice'::tax_document_kind
|
|
243
|
+
end
|
|
244
|
+
), 100) / 100,
|
|
245
|
+
rounding_of(l.company_id, d.currency_code)) as tax_charged
|
|
246
|
+
from document_lines l
|
|
247
|
+
join documents d on d.id = l.document_id
|
|
248
|
+
left join taxes t on t.id = l.tax_id
|
|
249
|
+
where l.line_type = 'product'
|
|
250
|
+
group by l.document_id, l.company_id, d.doc_type, d.currency_code, l.tax_id,
|
|
251
|
+
t.id, t.code, t.name, l.vat_category, l.vat_rate;
|
|
252
|
+
|
|
253
|
+
comment on view document_tax_summary is
|
|
254
|
+
'VAT breakdown of a document, one row per tax, rounded once on the group (EN 16931 BR-CO-14) — on the group''s base, or on the gross it was quoted at where the price includes the tax. The category and the rate are the ones the lines carry: the tax of today for a draft, the tax as it stood for a document that was posted.';
|
|
255
|
+
|
|
256
|
+
grant select on table document_tax_summary to authenticated, service_role;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
-- Ekwo OS — the company that sends an invoice has an address to send it from.
|
|
2
|
+
--
|
|
3
|
+
-- `contacts` has carried `peppol_scheme` and `peppol_identifier` since the
|
|
4
|
+
-- first day: the electronic address of the buyer, BT-49. The seller had
|
|
5
|
+
-- neither. BT-34 is mandatory on the network, so the first brick that wrote a
|
|
6
|
+
-- Peppol invoice had to be *told* the seller's address by its caller, and the
|
|
7
|
+
-- end-to-end test registered the company under its VAT number and said that
|
|
8
|
+
-- it was the test's choice. It is nobody's choice: which identifier a
|
|
9
|
+
-- participant is registered under is a fact of its registration with an access
|
|
10
|
+
-- point, and the only party that knows it is the company.
|
|
11
|
+
--
|
|
12
|
+
-- **The same two columns, under the same two names.** A scheme and a value,
|
|
13
|
+
-- which is the shape `docs/international.md` asks every identifier of this
|
|
14
|
+
-- schema to take. `peppol_` is the name `contacts` already uses and a second
|
|
15
|
+
-- name for the same thing would be a join somebody has to remember; the scheme
|
|
16
|
+
-- is a code of the Electronic Address Scheme list (EAS) that EN 16931 cites
|
|
17
|
+
-- for BT-34-1 and BT-49-1, whichever network delivers.
|
|
18
|
+
--
|
|
19
|
+
-- **No list of schemes.** `contacts` has none and the core gets none here: the
|
|
20
|
+
-- list is published by the Connecting Europe Facility, revised twice a year,
|
|
21
|
+
-- and differs between EN 16931 and Peppol — the brick that writes the file
|
|
22
|
+
-- carries both and names the rule a scheme breaks. A list here would be a
|
|
23
|
+
-- third copy, and the one most likely to be stale.
|
|
24
|
+
--
|
|
25
|
+
-- **One constraint `contacts` does not have: the pair is whole or absent.** A
|
|
26
|
+
-- scheme without an identifier addresses nobody and an identifier without a
|
|
27
|
+
-- scheme cannot be written at all (BR-62). The columns are new on `companies`,
|
|
28
|
+
-- so the check refuses nothing that exists. On `contacts` they are not, and
|
|
29
|
+
-- an installation may hold half a pair somebody typed in 2026; a constraint
|
|
30
|
+
-- added there would either refuse this migration or, `not valid`, refuse the
|
|
31
|
+
-- next unrelated edit of that contact. It is left for the release that can
|
|
32
|
+
-- look at the data first.
|
|
33
|
+
--
|
|
34
|
+
-- **Who may write it** is who may write the company: `companies_update`
|
|
35
|
+
-- judges the row, `company.write`, and there is nothing in an electronic
|
|
36
|
+
-- address that a member allowed to change the VAT number should not change.
|
|
37
|
+
--
|
|
38
|
+
-- The second half of this file corrects two comments. `party_scheme` and
|
|
39
|
+
-- `vat_scheme` were documented as "ISO 6523 ICD", and the packs — rightly —
|
|
40
|
+
-- fill them with codes of the EAS list, which contains the ICD list and more:
|
|
41
|
+
-- the VAT schemes of the 99xx range are not ISO 6523 identifiers at all. Read
|
|
42
|
+
-- as documented, `party_scheme` was taken for the scheme a registration number
|
|
43
|
+
-- is written in (BT-30-1) and produced BR-CL-11 for two packs out of four.
|
|
44
|
+
|
|
45
|
+
alter table companies
|
|
46
|
+
add column if not exists peppol_scheme text,
|
|
47
|
+
add column if not exists peppol_identifier text;
|
|
48
|
+
|
|
49
|
+
alter table companies
|
|
50
|
+
add constraint companies_electronic_address_whole
|
|
51
|
+
check ((peppol_scheme is null) = (peppol_identifier is null));
|
|
52
|
+
|
|
53
|
+
comment on column companies.peppol_scheme is
|
|
54
|
+
'Scheme of the electronic address this company receives and sends under (EN 16931 BT-34-1): a code of the Electronic Address Scheme list, e.g. 0088 for a GLN. A fact of the company''s registration with its access point, never derived from its VAT or registration number. Null together with peppol_identifier.';
|
|
55
|
+
comment on column companies.peppol_identifier is
|
|
56
|
+
'The electronic address itself (EN 16931 BT-34), in the scheme peppol_scheme names.';
|
|
57
|
+
|
|
58
|
+
comment on column contacts.peppol_scheme is
|
|
59
|
+
'Scheme of the electronic address this party is reached at (EN 16931 BT-49-1): a code of the Electronic Address Scheme list. A fact of the party''s registration, never derived from its VAT or registration number.';
|
|
60
|
+
comment on column contacts.peppol_identifier is
|
|
61
|
+
'The electronic address itself (EN 16931 BT-49), in the scheme peppol_scheme names.';
|
|
62
|
+
|
|
63
|
+
comment on column country_defaults.party_scheme is
|
|
64
|
+
'The Electronic Address Scheme (EAS) code under which a party of this country is usually *addressed* on the network — the scheme of an electronic address, BT-34-1 and BT-49-1. Four characters; the pack carries the value and the core never guesses one. It is a default an application may propose, not the address of anybody: that is companies.peppol_scheme and contacts.peppol_scheme. And it is not the scheme a registration number is written in (BT-30-1, BT-47-1), which has to be an ISO 6523 ICD: several EAS codes are not.';
|
|
65
|
+
comment on column country_defaults.vat_scheme is
|
|
66
|
+
'The Electronic Address Scheme (EAS) code under which a party of this country is addressed by its VAT number, where the network allows it. Distinct from party_scheme: a company is addressed by its registration number or by its VAT number, and they are not the same identifier. Not an ISO 6523 ICD in general — the VAT schemes of the EAS list are outside that standard.';
|