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,633 @@
|
|
|
1
|
+
-- Ekwo OS — a document records the language it was written in.
|
|
2
|
+
--
|
|
3
|
+
-- Until here it did not. The language of an invoice was re-derived on every
|
|
4
|
+
-- read, from the customer's preference, then the company's, then the one the
|
|
5
|
+
-- country pack declares — and re-derived is the whole problem. A customer who
|
|
6
|
+
-- switches to another language rewrites every invoice ever sent to them: the
|
|
7
|
+
-- legal mentions at the foot of a document somebody filed last year come back
|
|
8
|
+
-- in a language that document was never written in. On the one part of an
|
|
9
|
+
-- invoice a country actually legislates, that is not a display preference, it
|
|
10
|
+
-- is a different document.
|
|
11
|
+
--
|
|
12
|
+
-- `documents.language` is the fix, and it is the same fix the lines already
|
|
13
|
+
-- have. `document_lines.vat_category` and `vat_rate` are snapshots — the value
|
|
14
|
+
-- the tax had when the line was written — so that a rate change cannot rewrite
|
|
15
|
+
-- history. The language is that, for the sentences.
|
|
16
|
+
--
|
|
17
|
+
-- Four decisions.
|
|
18
|
+
--
|
|
19
|
+
-- **The column is written from the chain that already existed, not from a
|
|
20
|
+
-- literal.** The customer's language, else the company's, else the one the
|
|
21
|
+
-- pack declares. No country and no language is named anywhere in this file:
|
|
22
|
+
-- `companies.language` is not null — it is filled from the pack when the
|
|
23
|
+
-- company is created — so the chain always ends somewhere, and it ends in
|
|
24
|
+
-- data.
|
|
25
|
+
--
|
|
26
|
+
-- **It stops moving when the document is posted**, and not before. A draft is
|
|
27
|
+
-- not a document anybody has seen: it carries no number, no entry, and the
|
|
28
|
+
-- customer it is addressed to may still change. So while a document is a
|
|
29
|
+
-- draft its language is the chain's answer, kept in step when the chain moves
|
|
30
|
+
-- under it; the moment it is posted it is the document's own, and the guard
|
|
31
|
+
-- refuses to move it. That is the shape `accounts_guard_frozen` already has
|
|
32
|
+
-- for the code of an account, and the reason is the same: a policy judges
|
|
33
|
+
-- which company a row belongs to and has nothing to say about which column
|
|
34
|
+
-- changes.
|
|
35
|
+
--
|
|
36
|
+
-- **`preferred_languages()` gains the overload it was missing.** The published
|
|
37
|
+
-- way of choosing a language starts at `user_preferences` for `auth.uid()`,
|
|
38
|
+
-- which is null for the one reader a shared link exists for, so
|
|
39
|
+
-- `shared_document()` wrote the chain out a second time inside itself. The
|
|
40
|
+
-- chain is not about a *user*, it is about a *starting point*: a person's
|
|
41
|
+
-- preference, or a document's own language. `preferred_languages(language,
|
|
42
|
+
-- company)` takes that starting point explicitly, and the function that takes
|
|
43
|
+
-- a company alone is now one line on top of it — the signed-in reader's
|
|
44
|
+
-- preference is simply the starting point a session supplies.
|
|
45
|
+
--
|
|
46
|
+
-- **The reader of the sentences reads the document.**
|
|
47
|
+
-- `document_legal_mentions` published the pack's own text and the object of
|
|
48
|
+
-- translations beside it, and left every caller to pick a language; the only
|
|
49
|
+
-- caller in this repository picked it from the contact of the day. The view
|
|
50
|
+
-- now answers in the document's language, because it is a view *about a
|
|
51
|
+
-- document*, and keeps `text_i18n` for a renderer that wants a second one.
|
|
52
|
+
--
|
|
53
|
+
-- Nothing in a pack moves: this is a column, a function and two views. The
|
|
54
|
+
-- golden files are identical to the byte.
|
|
55
|
+
|
|
56
|
+
-- ---------------------------------------------------------------------------
|
|
57
|
+
-- 1. The chain, from a starting point that is not a session
|
|
58
|
+
--
|
|
59
|
+
-- `stable security definer`, like the function it now carries: it answers
|
|
60
|
+
-- about a company the caller is not necessarily a member of — which is exactly
|
|
61
|
+
-- the case of somebody holding a link to one invoice — and what comes out is
|
|
62
|
+
-- two-letter language codes and nothing else.
|
|
63
|
+
--
|
|
64
|
+
-- Neither argument carries a default. A one-argument call would then be
|
|
65
|
+
-- ambiguous between this function and the one below it, and an ambiguity that
|
|
66
|
+
-- resolves on the type of an untyped literal is a call that works until
|
|
67
|
+
-- somebody writes it differently.
|
|
68
|
+
-- ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
create or replace function preferred_languages(p_language text, p_company_id uuid)
|
|
71
|
+
returns text[]
|
|
72
|
+
language sql
|
|
73
|
+
stable
|
|
74
|
+
security definer
|
|
75
|
+
set search_path = public, pg_temp
|
|
76
|
+
as $$
|
|
77
|
+
select array_remove(array[
|
|
78
|
+
p_language,
|
|
79
|
+
(select c.language from companies c where c.id = p_company_id),
|
|
80
|
+
(select d.language_default
|
|
81
|
+
from companies c join country_defaults d on d.country = c.country
|
|
82
|
+
where c.id = p_company_id)
|
|
83
|
+
], null);
|
|
84
|
+
$$;
|
|
85
|
+
|
|
86
|
+
comment on function preferred_languages(text, uuid) is
|
|
87
|
+
'The languages to try, in order, from a starting point the caller names: that one, then the company''s, then the one the country pack declares. Feed it to label_for(). The starting point is a person''s preference for a reader who is signed in, and documents.language for a document being rendered.';
|
|
88
|
+
|
|
89
|
+
-- The published form, now one line on top of the one above. A signed-in
|
|
90
|
+
-- reader's preference is a starting point like any other; what was specific to
|
|
91
|
+
-- it was never the chain, only where the chain began.
|
|
92
|
+
create or replace function preferred_languages(p_company_id uuid default null)
|
|
93
|
+
returns text[]
|
|
94
|
+
language sql
|
|
95
|
+
stable
|
|
96
|
+
security definer
|
|
97
|
+
set search_path = public, pg_temp
|
|
98
|
+
as $$
|
|
99
|
+
select preferred_languages(
|
|
100
|
+
(select p.language from user_preferences p where p.user_id = auth.uid()),
|
|
101
|
+
p_company_id);
|
|
102
|
+
$$;
|
|
103
|
+
|
|
104
|
+
comment on function preferred_languages(uuid) is
|
|
105
|
+
'The languages to try, in order: the signed-in user''s own, then the company''s, then the one the country pack declares. The particular case of preferred_languages(language, company) where the starting point comes from the session.';
|
|
106
|
+
|
|
107
|
+
-- ---------------------------------------------------------------------------
|
|
108
|
+
-- 2. The column
|
|
109
|
+
--
|
|
110
|
+
-- `char(2)`, which is what `contacts.language` and `companies.language` are: a
|
|
111
|
+
-- language here is ISO 639-1, two characters by definition, and the padding
|
|
112
|
+
-- that made `char(2)` wrong for a VAT category cannot happen to a value that
|
|
113
|
+
-- is always exactly two long.
|
|
114
|
+
--
|
|
115
|
+
-- Not null, once the backfill below has filled what exists. A document that
|
|
116
|
+
-- did not know its own language is the thing this migration exists to remove,
|
|
117
|
+
-- and a nullable column would leave every reader with a coalesce to write —
|
|
118
|
+
-- which is the chain, written a third time, in as many places as there are
|
|
119
|
+
-- renderers.
|
|
120
|
+
-- ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
alter table documents
|
|
123
|
+
add column if not exists language char(2);
|
|
124
|
+
|
|
125
|
+
comment on column documents.language is
|
|
126
|
+
'The language this document is written in: its legal mentions, and whatever a renderer prints from the labels of the schema. Taken from the customer, else the company, else the country pack when the document is created, kept in step while it is a draft, and frozen the moment it is posted — a document already sent is not rewritten because its customer later changed preference.';
|
|
127
|
+
|
|
128
|
+
-- ---------------------------------------------------------------------------
|
|
129
|
+
-- 3. What the documents already here were written in
|
|
130
|
+
--
|
|
131
|
+
-- The same chain, applied once. It is a guess and it is worth saying so: for a
|
|
132
|
+
-- document already sent, nobody knows what language the PDF in the customer's
|
|
133
|
+
-- inbox was in — the only evidence the installation holds is the chain, which
|
|
134
|
+
-- is what produced that PDF at the time unless somebody has since changed a
|
|
135
|
+
-- preference. This is the best thing that can be said about a past document,
|
|
136
|
+
-- and from here on nothing is guessed.
|
|
137
|
+
-- ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
update documents d
|
|
140
|
+
set language = (preferred_languages(ct.language, d.company_id))[1]
|
|
141
|
+
from contacts ct
|
|
142
|
+
where ct.id = d.contact_id
|
|
143
|
+
and d.language is null;
|
|
144
|
+
|
|
145
|
+
alter table documents
|
|
146
|
+
alter column language set not null;
|
|
147
|
+
|
|
148
|
+
-- ---------------------------------------------------------------------------
|
|
149
|
+
-- 4. Writing it, and refusing to rewrite it
|
|
150
|
+
--
|
|
151
|
+
-- One function on both events, because it is one rule read twice: the language
|
|
152
|
+
-- of a document is the chain's answer until the document is posted, and the
|
|
153
|
+
-- document's own afterwards.
|
|
154
|
+
--
|
|
155
|
+
-- on insert the caller's value stands; where there is none, the chain
|
|
156
|
+
-- answers. `contact_id` is not null on this table, so there is
|
|
157
|
+
-- always a customer to start from.
|
|
158
|
+
-- on update a draft may be given another language by hand, and follows the
|
|
159
|
+
-- chain again when the customer under it changes. A document
|
|
160
|
+
-- that is no longer a draft keeps the language it was sent in,
|
|
161
|
+
-- and says so by name.
|
|
162
|
+
--
|
|
163
|
+
-- `cancelled` is not a draft either. A cancelled invoice was sent before it
|
|
164
|
+
-- was cancelled, and the credit note that undoes it quotes it.
|
|
165
|
+
-- ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
create or replace function documents_guard_language()
|
|
168
|
+
returns trigger
|
|
169
|
+
language plpgsql
|
|
170
|
+
as $$
|
|
171
|
+
declare
|
|
172
|
+
v_contact_language text;
|
|
173
|
+
begin
|
|
174
|
+
if tg_op = 'INSERT' then
|
|
175
|
+
if new.language is null then
|
|
176
|
+
select ct.language into v_contact_language
|
|
177
|
+
from contacts ct where ct.id = new.contact_id;
|
|
178
|
+
new.language := (preferred_languages(v_contact_language, new.company_id))[1];
|
|
179
|
+
end if;
|
|
180
|
+
return new;
|
|
181
|
+
end if;
|
|
182
|
+
|
|
183
|
+
if old.state <> 'draft' then
|
|
184
|
+
if new.language is distinct from old.language then
|
|
185
|
+
raise exception 'document_language_frozen: % was sent in %, so it stays in %. A document already issued is not rewritten because its customer changed preference; issue the new one in the new language.',
|
|
186
|
+
coalesce(new.number, old.number, new.id::text), old.language, old.language
|
|
187
|
+
using errcode = '55006';
|
|
188
|
+
end if;
|
|
189
|
+
return new;
|
|
190
|
+
end if;
|
|
191
|
+
|
|
192
|
+
-- A draft whose customer changes follows the new customer, unless the same
|
|
193
|
+
-- statement names a language itself.
|
|
194
|
+
if new.contact_id is distinct from old.contact_id
|
|
195
|
+
and new.language is not distinct from old.language then
|
|
196
|
+
select ct.language into v_contact_language
|
|
197
|
+
from contacts ct where ct.id = new.contact_id;
|
|
198
|
+
new.language := (preferred_languages(v_contact_language, new.company_id))[1];
|
|
199
|
+
end if;
|
|
200
|
+
|
|
201
|
+
return new;
|
|
202
|
+
end;
|
|
203
|
+
$$;
|
|
204
|
+
|
|
205
|
+
comment on function documents_guard_language() is
|
|
206
|
+
'Fills documents.language from the customer, then the company, then the country pack when a document is created, keeps a draft in step with the customer it is addressed to, and refuses document_language_frozen on anything that is no longer a draft.';
|
|
207
|
+
|
|
208
|
+
create trigger documents_guard_language
|
|
209
|
+
before insert or update on documents
|
|
210
|
+
for each row execute function documents_guard_language();
|
|
211
|
+
|
|
212
|
+
-- A trigger body is invoked by its table, never called.
|
|
213
|
+
revoke execute on function documents_guard_language() from public, anon, authenticated, service_role;
|
|
214
|
+
|
|
215
|
+
-- ---------------------------------------------------------------------------
|
|
216
|
+
-- 5. A customer who changes language, and the drafts addressed to them
|
|
217
|
+
--
|
|
218
|
+
-- The other half of "a draft follows the chain": the chain moves when the
|
|
219
|
+
-- customer moves, and a draft that is not touched again would keep an answer
|
|
220
|
+
-- that is no longer the chain's. Bounded to drafts of that one customer, and
|
|
221
|
+
-- to the one column — `documents_contact_idx` is the index it reads.
|
|
222
|
+
--
|
|
223
|
+
-- `security definer`, and the reason is worth writing down: the caller is
|
|
224
|
+
-- somebody editing a contact, and editing a contact is `contacts.write`, which
|
|
225
|
+
-- is not `documents.write`. A member allowed to correct a customer's record
|
|
226
|
+
-- should not be refused because a draft of theirs sits behind a policy the
|
|
227
|
+
-- member does not satisfy, and should not thereby gain the right to write a
|
|
228
|
+
-- document either. What this function writes is derived data, in one column,
|
|
229
|
+
-- on rows nothing has issued yet.
|
|
230
|
+
-- ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
create or replace function contacts_language_reaches_drafts()
|
|
233
|
+
returns trigger
|
|
234
|
+
language plpgsql
|
|
235
|
+
security definer
|
|
236
|
+
set search_path = public, pg_temp
|
|
237
|
+
as $$
|
|
238
|
+
begin
|
|
239
|
+
update documents d
|
|
240
|
+
set language = (preferred_languages(new.language, d.company_id))[1]
|
|
241
|
+
where d.contact_id = new.id
|
|
242
|
+
and d.state = 'draft'
|
|
243
|
+
and d.language is distinct from (preferred_languages(new.language, d.company_id))[1];
|
|
244
|
+
return null;
|
|
245
|
+
end;
|
|
246
|
+
$$;
|
|
247
|
+
|
|
248
|
+
comment on function contacts_language_reaches_drafts() is
|
|
249
|
+
'A customer who changes language changes the drafts addressed to them, and nothing else: a posted document keeps the language it was sent in.';
|
|
250
|
+
|
|
251
|
+
create trigger contacts_language_reaches_drafts
|
|
252
|
+
after update of language on contacts
|
|
253
|
+
for each row
|
|
254
|
+
when (new.language is distinct from old.language)
|
|
255
|
+
execute function contacts_language_reaches_drafts();
|
|
256
|
+
|
|
257
|
+
revoke execute on function contacts_language_reaches_drafts() from public, anon, authenticated, service_role;
|
|
258
|
+
|
|
259
|
+
-- ---------------------------------------------------------------------------
|
|
260
|
+
-- 6. The sentences, in the language of the document they are on
|
|
261
|
+
--
|
|
262
|
+
-- `document_legal_mentions` handed back `text` — the pack's own wording — and
|
|
263
|
+
-- `text_i18n` beside it, and left the choice of a language to whoever read it.
|
|
264
|
+
-- There was one reader, and it chose from the contact of the day.
|
|
265
|
+
--
|
|
266
|
+
-- The view is about a document, so it answers in the document's language, and
|
|
267
|
+
-- `language` is published beside the sentence so that a reader can see which
|
|
268
|
+
-- one it picked. `label_for` is fed the whole chain rather than the one
|
|
269
|
+
-- language, so a pack that has not translated a mention into the customer's
|
|
270
|
+
-- language falls back to the company's and then to its own wording instead of
|
|
271
|
+
-- printing nothing — which is what `label_for` was built to do.
|
|
272
|
+
--
|
|
273
|
+
-- `text_i18n` stays. A renderer printing a bilingual invoice — which several
|
|
274
|
+
-- countries' customers ask for — has every language of the pack in it and does
|
|
275
|
+
-- not have to go back to the templates for the second one.
|
|
276
|
+
--
|
|
277
|
+
-- Dropped and recreated rather than replaced: a column moves, and `create or
|
|
278
|
+
-- replace view` may only add at the end. The grants the drop takes away are
|
|
279
|
+
-- given back by name in section 8, which is the arrangement
|
|
280
|
+
-- `20260915174500` used for the same reason.
|
|
281
|
+
-- ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
drop view if exists document_legal_mentions;
|
|
284
|
+
|
|
285
|
+
create view document_legal_mentions
|
|
286
|
+
with (security_invoker = true) as
|
|
287
|
+
with document_taxes as (
|
|
288
|
+
select d.id as document_id,
|
|
289
|
+
d.company_id,
|
|
290
|
+
c.fiscal_country as country,
|
|
291
|
+
d.document_date,
|
|
292
|
+
d.doc_type,
|
|
293
|
+
d.language,
|
|
294
|
+
coalesce(
|
|
295
|
+
array_agg(distinct t.treatment::text) filter (where t.treatment is not null),
|
|
296
|
+
array[]::text[]
|
|
297
|
+
) as treatments,
|
|
298
|
+
coalesce(bool_or(t.cash_basis), false) as on_cash_basis
|
|
299
|
+
from documents d
|
|
300
|
+
join companies c on c.id = d.company_id
|
|
301
|
+
left join document_lines l on l.document_id = d.id
|
|
302
|
+
left join taxes t on t.id = l.tax_id
|
|
303
|
+
group by d.id, d.company_id, c.fiscal_country, d.document_date, d.doc_type, d.language
|
|
304
|
+
)
|
|
305
|
+
select dt.document_id,
|
|
306
|
+
dt.company_id,
|
|
307
|
+
m.country,
|
|
308
|
+
m.code,
|
|
309
|
+
m.applies_when,
|
|
310
|
+
dt.language,
|
|
311
|
+
label_for(m.text, m.text_i18n,
|
|
312
|
+
preferred_languages(dt.language, dt.company_id)) as text,
|
|
313
|
+
m.text_i18n,
|
|
314
|
+
m.sequence,
|
|
315
|
+
m.legal_reference
|
|
316
|
+
from document_taxes dt
|
|
317
|
+
join legal_mention_templates m
|
|
318
|
+
on m.country = dt.country
|
|
319
|
+
and m.valid_from <= dt.document_date
|
|
320
|
+
and (m.valid_to is null or m.valid_to >= dt.document_date)
|
|
321
|
+
where case m.applies_when
|
|
322
|
+
when 'always' then true
|
|
323
|
+
when 'reverse_charge' then dt.treatments
|
|
324
|
+
&& array['domestic_reverse_charge',
|
|
325
|
+
'foreign_services_received',
|
|
326
|
+
'intracom_triangular']
|
|
327
|
+
when 'intra_eu_goods' then dt.treatments && array['intracom_goods', 'intracom_acquisition_goods']
|
|
328
|
+
when 'intra_eu_services' then dt.treatments && array['intracom_services', 'intracom_acquisition_services']
|
|
329
|
+
when 'export' then dt.treatments && array['export']
|
|
330
|
+
when 'exempt' then dt.treatments && array['exempt']
|
|
331
|
+
when 'cash_basis' then dt.on_cash_basis
|
|
332
|
+
when 'late_payment' then dt.doc_type in ('sale_invoice', 'sale_credit_note')
|
|
333
|
+
else false
|
|
334
|
+
end;
|
|
335
|
+
|
|
336
|
+
comment on view document_legal_mentions is
|
|
337
|
+
'The legal mentions that apply to a document, each in the language the document was written in: its country, the validity of the mention on its date, and the treatments of the taxes its lines carry. A reverse-charge mention comes out for a domestic reverse charge, for a service received from a supplier who is not established here, and for the middle supply of a triangular arrangement — three articles behind one sentence, which is that the customer owes the tax. A small_business mention never comes out, because no column records the regime.';
|
|
338
|
+
|
|
339
|
+
-- ---------------------------------------------------------------------------
|
|
340
|
+
-- 7. The header a renderer reads once
|
|
341
|
+
--
|
|
342
|
+
-- It already carried everything printed above the lines except the one thing
|
|
343
|
+
-- that decides how the rest of it is worded. Appended at the end, which is
|
|
344
|
+
-- what `create or replace view` allows and what keeps the grants and the
|
|
345
|
+
-- comment of the object it replaces.
|
|
346
|
+
-- ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
create or replace view document_header
|
|
349
|
+
with (security_invoker = true) as
|
|
350
|
+
select d.id as document_id,
|
|
351
|
+
d.company_id,
|
|
352
|
+
d.doc_type,
|
|
353
|
+
d.state,
|
|
354
|
+
d.payment_state,
|
|
355
|
+
d.number,
|
|
356
|
+
d.supplier_reference,
|
|
357
|
+
d.document_date,
|
|
358
|
+
d.accounting_date,
|
|
359
|
+
d.due_date,
|
|
360
|
+
d.delivery_date,
|
|
361
|
+
d.currency_code,
|
|
362
|
+
d.amount_untaxed,
|
|
363
|
+
d.amount_tax,
|
|
364
|
+
d.amount_total,
|
|
365
|
+
d.amount_paid,
|
|
366
|
+
d.amount_residual,
|
|
367
|
+
d.payment_terms,
|
|
368
|
+
d.payment_means_code,
|
|
369
|
+
d.payment_reference,
|
|
370
|
+
d.buyer_reference,
|
|
371
|
+
d.order_reference,
|
|
372
|
+
d.contract_reference,
|
|
373
|
+
d.project_reference,
|
|
374
|
+
d.note,
|
|
375
|
+
coalesce(d.payee_iban, seller_bank.iban) as payee_iban,
|
|
376
|
+
seller_bank.bic as payee_bic,
|
|
377
|
+
|
|
378
|
+
-- The seller: this company.
|
|
379
|
+
coalesce(c.trade_name, c.name) as seller_name,
|
|
380
|
+
c.legal_name as seller_legal_name,
|
|
381
|
+
c.legal_form as seller_legal_form,
|
|
382
|
+
c.vat_number as seller_vat_number,
|
|
383
|
+
c.registration_number as seller_registration_number,
|
|
384
|
+
c.address_line1 as seller_address_line1,
|
|
385
|
+
c.address_line2 as seller_address_line2,
|
|
386
|
+
c.postal_code as seller_postal_code,
|
|
387
|
+
c.city as seller_city,
|
|
388
|
+
c.country as seller_country,
|
|
389
|
+
c.region as seller_region,
|
|
390
|
+
c.email as seller_email,
|
|
391
|
+
c.phone as seller_phone,
|
|
392
|
+
c.website as seller_website,
|
|
393
|
+
c.logo_url as seller_logo_url,
|
|
394
|
+
c.share_capital as seller_share_capital,
|
|
395
|
+
c.share_capital_currency as seller_share_capital_currency,
|
|
396
|
+
c.activity_code as seller_activity_code,
|
|
397
|
+
c.activity_scheme as seller_activity_scheme,
|
|
398
|
+
c.document_template,
|
|
399
|
+
|
|
400
|
+
-- The buyer: the contact the document is written against.
|
|
401
|
+
ct.id as buyer_id,
|
|
402
|
+
ct.name as buyer_name,
|
|
403
|
+
ct.vat_number as buyer_vat_number,
|
|
404
|
+
ct.registration_number as buyer_registration_number,
|
|
405
|
+
ct.address_line1 as buyer_address_line1,
|
|
406
|
+
ct.address_line2 as buyer_address_line2,
|
|
407
|
+
ct.postal_code as buyer_postal_code,
|
|
408
|
+
ct.city as buyer_city,
|
|
409
|
+
ct.country as buyer_country,
|
|
410
|
+
ct.region as buyer_region,
|
|
411
|
+
ct.email as buyer_email,
|
|
412
|
+
|
|
413
|
+
-- What the country of the document requires. Its country is the
|
|
414
|
+
-- company's fiscal_country — the country whose VAT rules apply — and
|
|
415
|
+
-- not its address, which is the same distinction
|
|
416
|
+
-- document_legal_mentions makes.
|
|
417
|
+
c.fiscal_country as country,
|
|
418
|
+
cd.number_format,
|
|
419
|
+
cd.numbering_gapless,
|
|
420
|
+
cd.legal_payment_days,
|
|
421
|
+
cd.late_payment_reference,
|
|
422
|
+
cd.tax_point_rule,
|
|
423
|
+
cd.einvoice_profile,
|
|
424
|
+
cd.einvoice_mandatory_from,
|
|
425
|
+
cd.party_scheme,
|
|
426
|
+
cd.vat_scheme,
|
|
427
|
+
|
|
428
|
+
-- What the document is written in. Not the reader's preference and
|
|
429
|
+
-- not the customer's of today: the document's own, as of the day it
|
|
430
|
+
-- was issued.
|
|
431
|
+
d.language
|
|
432
|
+
from documents d
|
|
433
|
+
join companies c on c.id = d.company_id
|
|
434
|
+
join contacts ct on ct.id = d.contact_id
|
|
435
|
+
left join bank_accounts seller_bank on seller_bank.id = c.default_bank_account_id
|
|
436
|
+
left join country_defaults cd on cd.country = c.fiscal_country;
|
|
437
|
+
|
|
438
|
+
comment on view document_header is
|
|
439
|
+
'One document with everything printed above its lines: the seller, the buyer, the amounts, where it is paid, what its country requires and the language it is written in. The lines are document_line_items and the sentences are document_legal_mentions.';
|
|
440
|
+
|
|
441
|
+
-- ---------------------------------------------------------------------------
|
|
442
|
+
-- 8. The document behind a link, in the language it was written in
|
|
443
|
+
--
|
|
444
|
+
-- `shared_document()` as `20260915153000` published it, with one paragraph
|
|
445
|
+
-- gone: the two-column chain it resolved on every read. The document carries
|
|
446
|
+
-- its language now, and the sentences come out of the view already in it, so
|
|
447
|
+
-- what is left is a read of a column and a read of a view — and the second
|
|
448
|
+
-- place a language was chosen in this schema is no longer there.
|
|
449
|
+
--
|
|
450
|
+
-- Everything else is that function unchanged, republished because a function
|
|
451
|
+
-- is replaced whole.
|
|
452
|
+
-- ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
create or replace function shared_document(p_token text)
|
|
455
|
+
returns jsonb
|
|
456
|
+
language plpgsql
|
|
457
|
+
security definer
|
|
458
|
+
set search_path = public, pg_temp
|
|
459
|
+
as $$
|
|
460
|
+
declare
|
|
461
|
+
v_share document_shares%rowtype;
|
|
462
|
+
v_doc documents%rowtype;
|
|
463
|
+
v_header document_header%rowtype;
|
|
464
|
+
v_last_payment date;
|
|
465
|
+
v_payload jsonb;
|
|
466
|
+
begin
|
|
467
|
+
select * into v_share
|
|
468
|
+
from document_shares
|
|
469
|
+
where token_hash = encode(sha256(convert_to(coalesce(p_token, ''), 'UTF8')), 'hex');
|
|
470
|
+
|
|
471
|
+
if v_share.id is null
|
|
472
|
+
or v_share.revoked_at is not null
|
|
473
|
+
or (v_share.expires_at is not null and v_share.expires_at <= now())
|
|
474
|
+
or v_share.subject_kind <> 'document' then
|
|
475
|
+
return null;
|
|
476
|
+
end if;
|
|
477
|
+
|
|
478
|
+
select * into v_doc from documents where id = v_share.document_id;
|
|
479
|
+
if v_doc.id is null or document_share_refusal(v_doc) is not null then
|
|
480
|
+
return null;
|
|
481
|
+
end if;
|
|
482
|
+
|
|
483
|
+
-- The link was used. Counted before the payload is built, so a reader who
|
|
484
|
+
-- gives up halfway is still a reader.
|
|
485
|
+
update document_shares
|
|
486
|
+
set view_count = view_count + 1,
|
|
487
|
+
last_viewed_at = now()
|
|
488
|
+
where id = v_share.id;
|
|
489
|
+
|
|
490
|
+
select * into v_header from document_header where document_id = v_doc.id;
|
|
491
|
+
|
|
492
|
+
-- When the last money against it arrived. The other side of every matching
|
|
493
|
+
-- on the document's own third-party lines, where that side is a payment.
|
|
494
|
+
select max(p.payment_date) into v_last_payment
|
|
495
|
+
from entry_lines dl
|
|
496
|
+
join reconciliations r
|
|
497
|
+
on r.debit_line_id = dl.id or r.credit_line_id = dl.id
|
|
498
|
+
join entry_lines ol
|
|
499
|
+
on ol.id = case when r.debit_line_id = dl.id then r.credit_line_id else r.debit_line_id end
|
|
500
|
+
join payments p on p.entry_id = ol.entry_id
|
|
501
|
+
where dl.entry_id = v_doc.entry_id;
|
|
502
|
+
|
|
503
|
+
v_payload := jsonb_build_object(
|
|
504
|
+
'document', jsonb_build_object(
|
|
505
|
+
'type', v_header.doc_type,
|
|
506
|
+
'number', v_header.number,
|
|
507
|
+
'document_date', v_header.document_date,
|
|
508
|
+
'due_date', v_header.due_date,
|
|
509
|
+
'delivery_date', v_header.delivery_date,
|
|
510
|
+
'currency', v_header.currency_code,
|
|
511
|
+
-- The document's own, snapshotted when it was created and frozen when it
|
|
512
|
+
-- was posted. A customer who has since changed preference does not
|
|
513
|
+
-- change what was sent to them.
|
|
514
|
+
'language', v_doc.language,
|
|
515
|
+
'payment_terms', v_header.payment_terms,
|
|
516
|
+
'payment_reference', v_header.payment_reference,
|
|
517
|
+
'buyer_reference', v_header.buyer_reference,
|
|
518
|
+
'order_reference', v_header.order_reference,
|
|
519
|
+
-- BT-22, the note the seller wrote on the invoice itself. An internal
|
|
520
|
+
-- remark about a customer is `contacts.notes` and is not in this object.
|
|
521
|
+
'note', v_header.note
|
|
522
|
+
),
|
|
523
|
+
'seller', jsonb_build_object(
|
|
524
|
+
'name', v_header.seller_name,
|
|
525
|
+
'legal_name', v_header.seller_legal_name,
|
|
526
|
+
'legal_form', v_header.seller_legal_form,
|
|
527
|
+
'vat_number', v_header.seller_vat_number,
|
|
528
|
+
'registration_number', v_header.seller_registration_number,
|
|
529
|
+
'address_line1', v_header.seller_address_line1,
|
|
530
|
+
'address_line2', v_header.seller_address_line2,
|
|
531
|
+
'postal_code', v_header.seller_postal_code,
|
|
532
|
+
'city', v_header.seller_city,
|
|
533
|
+
'country', v_header.seller_country,
|
|
534
|
+
'email', v_header.seller_email,
|
|
535
|
+
'phone', v_header.seller_phone,
|
|
536
|
+
'website', v_header.seller_website,
|
|
537
|
+
'logo_url', v_header.seller_logo_url,
|
|
538
|
+
'iban', v_header.payee_iban,
|
|
539
|
+
'bic', v_header.payee_bic
|
|
540
|
+
),
|
|
541
|
+
'buyer', jsonb_build_object(
|
|
542
|
+
'name', v_header.buyer_name,
|
|
543
|
+
'vat_number', v_header.buyer_vat_number,
|
|
544
|
+
'registration_number', v_header.buyer_registration_number,
|
|
545
|
+
'address_line1', v_header.buyer_address_line1,
|
|
546
|
+
'address_line2', v_header.buyer_address_line2,
|
|
547
|
+
'postal_code', v_header.buyer_postal_code,
|
|
548
|
+
'city', v_header.buyer_city,
|
|
549
|
+
'country', v_header.buyer_country
|
|
550
|
+
),
|
|
551
|
+
'lines', coalesce((
|
|
552
|
+
select jsonb_agg(jsonb_build_object(
|
|
553
|
+
'sequence', li.sequence,
|
|
554
|
+
'type', li.line_type,
|
|
555
|
+
'name', li.item_name,
|
|
556
|
+
'description', li.item_description,
|
|
557
|
+
'quantity', li.quantity::text,
|
|
558
|
+
'unit_code', li.unit_code,
|
|
559
|
+
'unit_price', li.unit_price::text,
|
|
560
|
+
'discount_percent', li.discount_percent::text,
|
|
561
|
+
'amount_untaxed', li.amount_untaxed::text,
|
|
562
|
+
'tax_category', li.vat_category,
|
|
563
|
+
'tax_rate', li.vat_rate::text,
|
|
564
|
+
'tax_exemption_code', li.tax_exemption_code)
|
|
565
|
+
order by li.sequence)
|
|
566
|
+
from document_line_items li
|
|
567
|
+
where li.document_id = v_doc.id), '[]'::jsonb),
|
|
568
|
+
'tax_summary', coalesce((
|
|
569
|
+
select jsonb_agg(jsonb_build_object(
|
|
570
|
+
'name', ts.tax_name,
|
|
571
|
+
'category', ts.vat_category,
|
|
572
|
+
'rate', ts.tax_rate::text,
|
|
573
|
+
'base_amount', ts.base_amount::text,
|
|
574
|
+
-- What the other party actually pays, which is what an invoice
|
|
575
|
+
-- prints: zero where the tax self-assesses, and what the four
|
|
576
|
+
-- totals below add up from.
|
|
577
|
+
'tax_amount', ts.tax_charged::text)
|
|
578
|
+
order by ts.tax_rate, ts.tax_name)
|
|
579
|
+
from document_tax_summary ts
|
|
580
|
+
where ts.document_id = v_doc.id), '[]'::jsonb),
|
|
581
|
+
'totals', jsonb_build_object(
|
|
582
|
+
'amount_untaxed', v_header.amount_untaxed::text,
|
|
583
|
+
'amount_tax', v_header.amount_tax::text,
|
|
584
|
+
'amount_total', v_header.amount_total::text
|
|
585
|
+
),
|
|
586
|
+
-- The one thing that legitimately moves after the document was sent, so a
|
|
587
|
+
-- link a customer keeps stays worth opening.
|
|
588
|
+
'payment', jsonb_build_object(
|
|
589
|
+
'state', v_header.payment_state,
|
|
590
|
+
'amount_paid', v_header.amount_paid::text,
|
|
591
|
+
'amount_residual', v_header.amount_residual::text,
|
|
592
|
+
'last_payment_date', v_last_payment
|
|
593
|
+
),
|
|
594
|
+
'legal_mentions', coalesce((
|
|
595
|
+
select jsonb_agg(jsonb_build_object(
|
|
596
|
+
'code', lm.code,
|
|
597
|
+
'text', lm.text)
|
|
598
|
+
order by lm.sequence, lm.code)
|
|
599
|
+
from document_legal_mentions lm
|
|
600
|
+
where lm.document_id = v_doc.id), '[]'::jsonb)
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
return v_payload;
|
|
604
|
+
end;
|
|
605
|
+
$$;
|
|
606
|
+
|
|
607
|
+
comment on function shared_document(text) is
|
|
608
|
+
'One document, read by whoever holds its link: the header, the lines, the tax breakdown, the totals, the legal mentions in the language the document was written in, and what is still owed today. Returns null — the same null, in the same shape — for a token that is unknown, withdrawn, expired, or onto a document that may no longer be shared.';
|
|
609
|
+
|
|
610
|
+
-- ---------------------------------------------------------------------------
|
|
611
|
+
-- 9. Grants
|
|
612
|
+
--
|
|
613
|
+
-- The rule of `supabase/migrations/README.md`: the schema closes behind itself
|
|
614
|
+
-- and then says, by name, who may reach what. The dropped view takes its
|
|
615
|
+
-- privileges with it and gets them back here, to exactly who held them before.
|
|
616
|
+
-- `anon` gains nothing: not the view, not the overload. What an anonymous
|
|
617
|
+
-- reader reaches is still one `security definer` function, which reads both as
|
|
618
|
+
-- its owner.
|
|
619
|
+
-- ---------------------------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
revoke execute on all functions in schema public from public;
|
|
622
|
+
|
|
623
|
+
revoke all on table document_legal_mentions from public, anon;
|
|
624
|
+
grant select on table document_legal_mentions to authenticated, service_role;
|
|
625
|
+
|
|
626
|
+
revoke execute on function preferred_languages(text, uuid) from public, anon;
|
|
627
|
+
grant execute on function preferred_languages(text, uuid) to authenticated, service_role;
|
|
628
|
+
|
|
629
|
+
revoke execute on function preferred_languages(uuid) from public, anon;
|
|
630
|
+
grant execute on function preferred_languages(uuid) to authenticated, service_role;
|
|
631
|
+
|
|
632
|
+
revoke execute on function shared_document(text) from public;
|
|
633
|
+
grant execute on function shared_document(text) to anon, authenticated, service_role;
|