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,649 @@
|
|
|
1
|
+
-- Ekwo OS — a price that already holds its tax.
|
|
2
|
+
--
|
|
3
|
+
-- `taxes.price_include` has been a column since the tax engine landed on
|
|
4
|
+
-- 12 September and has never been read. The note beside it said the
|
|
5
|
+
-- gross-to-net computation "arrives with the country that needs it", and the
|
|
6
|
+
-- United Kingdom is that country: a retail price there is quoted with the VAT
|
|
7
|
+
-- in it as a matter of course, and VAT Notice 700 publishes the fraction that
|
|
8
|
+
-- takes it back out — the rate over one hundred plus the rate, one sixth at
|
|
9
|
+
-- 20 per cent. Australia, Canada and most of what a shop sells to a consumer
|
|
10
|
+
-- anywhere work the same way. Until now a line carrying such a tax was booked
|
|
11
|
+
-- with the tax added *on top* of the price the customer had already paid, so
|
|
12
|
+
-- a till roll of 7 401,32 became an invoice of 8 881,58.
|
|
13
|
+
--
|
|
14
|
+
-- What an invoice has to say is not negotiable. EN 16931 states the line net
|
|
15
|
+
-- amount in BT-131 and the taxable amount of each group in BT-116, both
|
|
16
|
+
-- excluding the tax, and BR-CO-14 requires the tax of a group to be the
|
|
17
|
+
-- rounded figure of that one group and not a sum of line roundings. So the
|
|
18
|
+
-- gross price is an input, never an output: it is converted, once, per tax
|
|
19
|
+
-- group, and what the document stores is the net.
|
|
20
|
+
--
|
|
21
|
+
-- **The rule.** For every tax group of a document whose tax prices with the
|
|
22
|
+
-- tax in it:
|
|
23
|
+
--
|
|
24
|
+
-- gross = sum of the lines, each quantity x unit price less its discount
|
|
25
|
+
-- tax = round(gross - gross / (1 + rate/100)) once, at the currency
|
|
26
|
+
-- base = gross - tax
|
|
27
|
+
--
|
|
28
|
+
-- and `base` is then shared over the lines of the group in proportion to their
|
|
29
|
+
-- gross, the last line of the group taking whatever is left. That is the
|
|
30
|
+
-- technique `post_document` already uses to share a `tax_on_base` over the
|
|
31
|
+
-- accounts it lands on, and it is used here for the same reason: the shares
|
|
32
|
+
-- have to add up to the figure that was rounded once, or the invoice does not
|
|
33
|
+
-- foot.
|
|
34
|
+
--
|
|
35
|
+
-- **Why the base is subtracted rather than computed.** `base = gross x 100 /
|
|
36
|
+
-- (100 + rate)` rounded on its own, with the tax rounded on its own beside it,
|
|
37
|
+
-- gives a pair that misses the gross by a unit often enough to notice: at
|
|
38
|
+
-- 20 per cent, one price in about fifty. Subtracting makes `base + tax =
|
|
39
|
+
-- gross` true by construction, which is the one thing a retail customer can
|
|
40
|
+
-- check by looking at the receipt in their hand. HMRC's VAT Notice 700,
|
|
41
|
+
-- §§ 17.5 and 17.6, admits two rounding methods to a retailer — line by line
|
|
42
|
+
-- or invoice by invoice — and this is the second, which is also the one
|
|
43
|
+
-- BR-CO-14 requires of a European invoice. Nothing here invents a word for the
|
|
44
|
+
-- choice: `rounding_method` is the *arithmetic* of a country, not the unit the
|
|
45
|
+
-- arithmetic is applied to, and a pack that had to declare "per line" would be
|
|
46
|
+
-- declaring an invoice that fails validation. The gap, where a country really
|
|
47
|
+
-- does allow a trader to choose, is written up in `docs/international.md`.
|
|
48
|
+
--
|
|
49
|
+
-- **Why the tax cannot be re-derived from the base.** `document_tax_summary`
|
|
50
|
+
-- computed the tax of every group as `round(base x rate / 100)`. Applied to a
|
|
51
|
+
-- base that came out of the subtraction above, that answer is not always the
|
|
52
|
+
-- tax that produced it: 99,99 at 20 per cent gives a tax of 16,67 and a base of
|
|
53
|
+
-- 83,32, and 83,32 at 20 per cent rounds back to 16,66 — a penny short, and a
|
|
54
|
+
-- total of 99,98 against a price of 99,99. So the view computes the tax of an
|
|
55
|
+
-- inclusive group from the gross it was quoted at, and of every other group
|
|
56
|
+
-- exactly as before. No figure of any existing pack moves by a cent.
|
|
57
|
+
--
|
|
58
|
+
-- **Two columns on the line, and why they are columns.**
|
|
59
|
+
--
|
|
60
|
+
-- * `unit_price_includes_tax` is a snapshot, taken from the tax while the
|
|
61
|
+
-- document is a draft and frozen when it is posted — the rule BT-151 and
|
|
62
|
+
-- BT-152 already follow. A pack upgrade that turns the flag on a tax must
|
|
63
|
+
-- not rewrite an invoice somebody has already sent. It is *derived* and
|
|
64
|
+
-- never keyed, because it is a fact about the tax and not a choice of the
|
|
65
|
+
-- line.
|
|
66
|
+
-- * `amount_incl_tax` keeps the gross the line was quoted at, which is the
|
|
67
|
+
-- only figure a retailer recognises and the only one the conversion can be
|
|
68
|
+
-- redone from. Null on every line whose price excludes the tax, so the
|
|
69
|
+
-- column says which world it is in rather than holding a copy of
|
|
70
|
+
-- `amount_untaxed`.
|
|
71
|
+
--
|
|
72
|
+
-- **Where the arithmetic runs.** `document_lines.amount_untaxed` stays the
|
|
73
|
+
-- truth and stays derived. Its `before` trigger cannot see the other lines of
|
|
74
|
+
-- the group, so it writes what one line on its own comes to — which *is* the
|
|
75
|
+
-- group's answer when the group has one line, the ordinary retail case — and
|
|
76
|
+
-- the `after` trigger that already refreshes the document's totals shares the
|
|
77
|
+
-- group's base over the group first. The share is written only where it
|
|
78
|
+
-- differs, so the write that corrects a line does not start again.
|
|
79
|
+
--
|
|
80
|
+
-- **Three refusals, each by name and each as early as it can be made.**
|
|
81
|
+
--
|
|
82
|
+
-- * A fixed-amount tax cannot price with the tax in it: there is no rate to
|
|
83
|
+
-- divide by, and "the price includes 0,50" is a discount, not a tax. A
|
|
84
|
+
-- check constraint on `taxes` and on `tax_templates` refuses it, and
|
|
85
|
+
-- `ekwo pack check` refuses it before a seed is written.
|
|
86
|
+
-- * A line whose price includes a tax has to have a tax. A check constraint
|
|
87
|
+
-- on `document_lines`.
|
|
88
|
+
-- * A tax group cannot be half inclusive. The only way to build one is to
|
|
89
|
+
-- turn the flag on a tax between two line writes of the same draft;
|
|
90
|
+
-- `mixed_price_include` says so where it happens, rather than letting a
|
|
91
|
+
-- document be posted whose gross and net lines were added together.
|
|
92
|
+
|
|
93
|
+
-- ---------------------------------------------------------------------------
|
|
94
|
+
-- 1. A price that includes a tax needs a rate to take it back out
|
|
95
|
+
-- ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
alter table taxes
|
|
98
|
+
add constraint taxes_price_include_needs_a_rate
|
|
99
|
+
check (not price_include or amount_type = 'percent');
|
|
100
|
+
|
|
101
|
+
alter table tax_templates
|
|
102
|
+
add constraint tax_templates_price_include_needs_a_rate
|
|
103
|
+
check (not price_include or amount_type = 'percent');
|
|
104
|
+
|
|
105
|
+
comment on column taxes.price_include is
|
|
106
|
+
'The unit price of a line carrying this tax is the gross price: the engine takes the tax out of it per group rather than adding it on top. Only a percentage tax may say so.';
|
|
107
|
+
|
|
108
|
+
comment on column tax_templates.price_include is
|
|
109
|
+
'The unit price of a line carrying this tax is the gross price. Compiled from the pack and copied onto the tax a company installs.';
|
|
110
|
+
|
|
111
|
+
-- ---------------------------------------------------------------------------
|
|
112
|
+
-- 2. What the line keeps
|
|
113
|
+
-- ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
alter table document_lines
|
|
116
|
+
add column if not exists unit_price_includes_tax boolean not null default false,
|
|
117
|
+
add column if not exists amount_incl_tax numeric(16, 2);
|
|
118
|
+
|
|
119
|
+
comment on column document_lines.unit_price_includes_tax is
|
|
120
|
+
'The unit price of this line was quoted with the tax in it. Derived from the tax while the document is a draft and frozen when it is posted, like BT-151 and BT-152, so a later change to the tax cannot rewrite an invoice that has been sent.';
|
|
121
|
+
|
|
122
|
+
comment on column document_lines.amount_incl_tax is
|
|
123
|
+
'quantity x unit_price less the discount, tax included, as the line was quoted. Null where the price excludes the tax, which is every line of every pack but a retail one.';
|
|
124
|
+
|
|
125
|
+
alter table document_lines
|
|
126
|
+
add constraint document_lines_included_tax_needs_a_tax
|
|
127
|
+
check (not unit_price_includes_tax or tax_id is not null);
|
|
128
|
+
|
|
129
|
+
-- ---------------------------------------------------------------------------
|
|
130
|
+
-- 3. The line derives its own amounts
|
|
131
|
+
--
|
|
132
|
+
-- Same trigger, same name, one more thing to work out. Three cases:
|
|
133
|
+
--
|
|
134
|
+
-- * not a product line — nothing is billed and nothing is quoted;
|
|
135
|
+
-- * a price that excludes the tax — what the trigger has always done;
|
|
136
|
+
-- * a price that includes it — the gross is kept and the net is what this
|
|
137
|
+
-- line alone comes to. `documents_allocate_included_tax()` then replaces
|
|
138
|
+
-- that provisional with the line's share of a base rounded once on the
|
|
139
|
+
-- group. Where the group has one line the two are the same figure, which
|
|
140
|
+
-- is why the provisional is this and not the gross: a line that never
|
|
141
|
+
-- reached the allocator is out by at most one unit of the currency,
|
|
142
|
+
-- instead of out by the whole tax.
|
|
143
|
+
--
|
|
144
|
+
-- The provisional is recomputed only when the gross moved. That is what lets
|
|
145
|
+
-- the allocator write a share back through this trigger without it being
|
|
146
|
+
-- overwritten on the way in.
|
|
147
|
+
-- ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
create or replace function document_lines_amount_untaxed()
|
|
150
|
+
returns trigger
|
|
151
|
+
language plpgsql
|
|
152
|
+
as $$
|
|
153
|
+
declare
|
|
154
|
+
v_company uuid;
|
|
155
|
+
v_currency char(3);
|
|
156
|
+
v_state doc_state;
|
|
157
|
+
v_round money_rounding;
|
|
158
|
+
v_gross numeric;
|
|
159
|
+
v_rate numeric;
|
|
160
|
+
begin
|
|
161
|
+
if new.line_type <> 'product' then
|
|
162
|
+
new.amount_untaxed := 0;
|
|
163
|
+
new.amount_incl_tax := null;
|
|
164
|
+
new.unit_price_includes_tax := false;
|
|
165
|
+
return new;
|
|
166
|
+
end if;
|
|
167
|
+
|
|
168
|
+
-- Read in two steps: `select f(x) into a_composite` would assign the whole
|
|
169
|
+
-- row to the first field of the target, which is a trap plpgsql lays for
|
|
170
|
+
-- every function that returns a composite.
|
|
171
|
+
select d.company_id, d.currency_code, d.state
|
|
172
|
+
into v_company, v_currency, v_state
|
|
173
|
+
from documents d where d.id = new.document_id;
|
|
174
|
+
v_round := rounding_of(v_company, v_currency);
|
|
175
|
+
|
|
176
|
+
-- The snapshot. Taken from the tax for as long as the document is a draft,
|
|
177
|
+
-- and left alone from the moment it is posted.
|
|
178
|
+
if tg_op = 'INSERT' or v_state = 'draft' then
|
|
179
|
+
new.unit_price_includes_tax :=
|
|
180
|
+
coalesce((select t.price_include from taxes t where t.id = new.tax_id), false);
|
|
181
|
+
end if;
|
|
182
|
+
|
|
183
|
+
v_gross := round_amount(
|
|
184
|
+
new.quantity * new.unit_price * (1 - new.discount_percent / 100), v_round);
|
|
185
|
+
|
|
186
|
+
if not new.unit_price_includes_tax then
|
|
187
|
+
new.amount_incl_tax := null;
|
|
188
|
+
new.amount_untaxed := v_gross;
|
|
189
|
+
return new;
|
|
190
|
+
end if;
|
|
191
|
+
|
|
192
|
+
new.amount_incl_tax := v_gross;
|
|
193
|
+
if tg_op = 'INSERT'
|
|
194
|
+
or not old.unit_price_includes_tax
|
|
195
|
+
or old.amount_incl_tax is distinct from v_gross then
|
|
196
|
+
select t.amount into v_rate from taxes t where t.id = new.tax_id;
|
|
197
|
+
if coalesce(v_rate, 0) <= -100 then
|
|
198
|
+
raise exception 'unsupported_included_rate: a price cannot include a tax of % per cent', v_rate;
|
|
199
|
+
end if;
|
|
200
|
+
new.amount_untaxed :=
|
|
201
|
+
v_gross - round_amount(v_gross - v_gross / (1 + coalesce(v_rate, 0) / 100), v_round);
|
|
202
|
+
end if;
|
|
203
|
+
return new;
|
|
204
|
+
end;
|
|
205
|
+
$$;
|
|
206
|
+
|
|
207
|
+
comment on function document_lines_amount_untaxed() is
|
|
208
|
+
'Derives a line''s amounts from its quantity, price and discount, rounded once at the decimals of the document''s currency: the net, the gross where the price includes the tax, and the snapshot of whether it does.';
|
|
209
|
+
|
|
210
|
+
-- ---------------------------------------------------------------------------
|
|
211
|
+
-- 4. The group is the unit of the rounding
|
|
212
|
+
--
|
|
213
|
+
-- One pass per tax group of the document whose lines are quoted with the tax
|
|
214
|
+
-- in them. The tax is rounded once on the gross of the group (BR-CO-14), the
|
|
215
|
+
-- base is the gross less that tax — so the two add back to the price that was
|
|
216
|
+
-- quoted, always — and the base is shared over the lines in proportion to
|
|
217
|
+
-- their gross, the last of them taking the remainder.
|
|
218
|
+
--
|
|
219
|
+
-- It writes each share only where it differs from what is stored. That is not
|
|
220
|
+
-- on its own enough to stop the loop it would otherwise start — the share goes
|
|
221
|
+
-- back through the `before` trigger and the column's own scale can round it, so
|
|
222
|
+
-- the comparison never settles in a currency with three decimals — which is why
|
|
223
|
+
-- the trigger below it allocates on the outermost write only.
|
|
224
|
+
-- ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
create or replace function documents_allocate_included_tax(p_document_id uuid)
|
|
227
|
+
returns void
|
|
228
|
+
language plpgsql
|
|
229
|
+
as $$
|
|
230
|
+
declare
|
|
231
|
+
v_company uuid;
|
|
232
|
+
v_currency char(3);
|
|
233
|
+
v_round money_rounding;
|
|
234
|
+
v_gross numeric;
|
|
235
|
+
v_tax numeric;
|
|
236
|
+
v_base numeric;
|
|
237
|
+
v_left numeric;
|
|
238
|
+
v_share numeric;
|
|
239
|
+
v_group record;
|
|
240
|
+
v_line record;
|
|
241
|
+
begin
|
|
242
|
+
-- The ordinary case, and the one this has to cost nothing in: a document
|
|
243
|
+
-- with no line quoted with its tax in it.
|
|
244
|
+
if not exists (
|
|
245
|
+
select 1 from document_lines dl
|
|
246
|
+
where dl.document_id = p_document_id
|
|
247
|
+
and dl.line_type = 'product'
|
|
248
|
+
and dl.unit_price_includes_tax
|
|
249
|
+
) then
|
|
250
|
+
return;
|
|
251
|
+
end if;
|
|
252
|
+
|
|
253
|
+
select d.company_id, d.currency_code into v_company, v_currency
|
|
254
|
+
from documents d where d.id = p_document_id;
|
|
255
|
+
if not found then
|
|
256
|
+
return;
|
|
257
|
+
end if;
|
|
258
|
+
v_round := rounding_of(v_company, v_currency);
|
|
259
|
+
|
|
260
|
+
for v_group in
|
|
261
|
+
select dl.tax_id,
|
|
262
|
+
max(t.code) as tax_code,
|
|
263
|
+
max(t.amount) as rate,
|
|
264
|
+
bool_and(dl.unit_price_includes_tax) as all_inclusive,
|
|
265
|
+
coalesce(sum(dl.amount_incl_tax), 0) as gross
|
|
266
|
+
from document_lines dl
|
|
267
|
+
join taxes t on t.id = dl.tax_id
|
|
268
|
+
where dl.document_id = p_document_id
|
|
269
|
+
and dl.line_type = 'product'
|
|
270
|
+
group by dl.tax_id
|
|
271
|
+
having bool_or(dl.unit_price_includes_tax)
|
|
272
|
+
order by 1
|
|
273
|
+
loop
|
|
274
|
+
-- Half a group is not a group. The only way to build one is to change the
|
|
275
|
+
-- tax between two line writes of the same draft, and adding a gross line
|
|
276
|
+
-- to a net one would produce a base nobody can explain.
|
|
277
|
+
if not v_group.all_inclusive then
|
|
278
|
+
raise exception 'mixed_price_include: tax % prices some lines of document % with the tax in them and others without',
|
|
279
|
+
v_group.tax_code, p_document_id;
|
|
280
|
+
end if;
|
|
281
|
+
if coalesce(v_group.rate, 0) <= -100 then
|
|
282
|
+
raise exception 'unsupported_included_rate: a price cannot include a tax of % per cent', v_group.rate;
|
|
283
|
+
end if;
|
|
284
|
+
|
|
285
|
+
v_gross := v_group.gross;
|
|
286
|
+
v_tax := round_amount(v_gross - v_gross / (1 + coalesce(v_group.rate, 0) / 100), v_round);
|
|
287
|
+
v_base := v_gross - v_tax;
|
|
288
|
+
v_left := v_base;
|
|
289
|
+
|
|
290
|
+
for v_line in
|
|
291
|
+
select dl.id,
|
|
292
|
+
coalesce(dl.amount_incl_tax, 0) as gross,
|
|
293
|
+
row_number() over (order by dl.sequence, dl.id)
|
|
294
|
+
= count(*) over () as is_last
|
|
295
|
+
from document_lines dl
|
|
296
|
+
where dl.document_id = p_document_id
|
|
297
|
+
and dl.line_type = 'product'
|
|
298
|
+
and dl.tax_id = v_group.tax_id
|
|
299
|
+
order by dl.sequence, dl.id
|
|
300
|
+
loop
|
|
301
|
+
if v_line.is_last then
|
|
302
|
+
v_share := v_left;
|
|
303
|
+
else
|
|
304
|
+
v_share := case when v_gross = 0 then 0
|
|
305
|
+
else round_amount(v_base * v_line.gross / v_gross, v_round) end;
|
|
306
|
+
v_left := v_left - v_share;
|
|
307
|
+
end if;
|
|
308
|
+
|
|
309
|
+
update document_lines
|
|
310
|
+
set amount_untaxed = v_share
|
|
311
|
+
where id = v_line.id
|
|
312
|
+
and amount_untaxed is distinct from v_share;
|
|
313
|
+
end loop;
|
|
314
|
+
end loop;
|
|
315
|
+
end;
|
|
316
|
+
$$;
|
|
317
|
+
|
|
318
|
+
comment on function documents_allocate_included_tax(uuid) is
|
|
319
|
+
'Turns the gross of every tax group quoted with the tax in it into a base: the tax rounded once on the group (BR-CO-14), the base the gross less that tax, shared over the lines in proportion to their gross with the remainder on the last. Refuses a group that is half inclusive, by name.';
|
|
320
|
+
|
|
321
|
+
revoke execute on function documents_allocate_included_tax(uuid) from public, anon;
|
|
322
|
+
grant execute on function documents_allocate_included_tax(uuid) to authenticated, service_role;
|
|
323
|
+
|
|
324
|
+
-- The allocation runs before the totals, because the totals read the bases it
|
|
325
|
+
-- writes.
|
|
326
|
+
create or replace function document_lines_refresh_totals()
|
|
327
|
+
returns trigger
|
|
328
|
+
language plpgsql
|
|
329
|
+
as $$
|
|
330
|
+
begin
|
|
331
|
+
-- The allocator writes its shares back through this same trigger, and what it
|
|
332
|
+
-- wrote is already the answer. Only the outermost write allocates: a second
|
|
333
|
+
-- pass would be work for nothing, and in a currency whose decimals the column
|
|
334
|
+
-- cannot hold — `amount_untaxed` is `numeric(16, 2)`, which is a gap of its
|
|
335
|
+
-- own — a share that comes back rounded would never compare equal and the
|
|
336
|
+
-- two would call each other for ever.
|
|
337
|
+
if pg_trigger_depth() <= 1 then
|
|
338
|
+
perform documents_allocate_included_tax(coalesce(new.document_id, old.document_id));
|
|
339
|
+
end if;
|
|
340
|
+
perform documents_refresh_totals(coalesce(new.document_id, old.document_id));
|
|
341
|
+
return null;
|
|
342
|
+
end;
|
|
343
|
+
$$;
|
|
344
|
+
|
|
345
|
+
comment on function document_lines_refresh_totals() is
|
|
346
|
+
'After a line moves: shares out the base of every tax group quoted with its tax in it, then refreshes the three totals of the document.';
|
|
347
|
+
|
|
348
|
+
-- ---------------------------------------------------------------------------
|
|
349
|
+
-- 5. The breakdown reads the gross where there is one
|
|
350
|
+
--
|
|
351
|
+
-- `base_amount` is unchanged and needs to be: the bases the allocator wrote
|
|
352
|
+
-- add up to the group's base exactly. What changes is the tax beside it, for
|
|
353
|
+
-- an inclusive group only — computed from the gross the group was quoted at
|
|
354
|
+
-- rather than re-derived from the base, for the reason in the header.
|
|
355
|
+
-- `tax_charged` keeps its shape: the unrounded tax times the share of it that
|
|
356
|
+
-- reaches the other party, rounded once.
|
|
357
|
+
-- ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
create or replace view document_tax_summary
|
|
360
|
+
with (security_invoker = true) as
|
|
361
|
+
select l.document_id,
|
|
362
|
+
l.company_id,
|
|
363
|
+
d.doc_type,
|
|
364
|
+
l.tax_id,
|
|
365
|
+
t.code as tax_code,
|
|
366
|
+
t.name as tax_name,
|
|
367
|
+
t.vat_category,
|
|
368
|
+
t.amount as tax_rate,
|
|
369
|
+
sum(l.amount_untaxed) as base_amount,
|
|
370
|
+
-- Gross tax: what the VAT return reports.
|
|
371
|
+
round_amount(
|
|
372
|
+
case when bool_and(l.unit_price_includes_tax)
|
|
373
|
+
then sum(l.amount_incl_tax)
|
|
374
|
+
- sum(l.amount_incl_tax) / (1 + coalesce(t.amount, 0) / 100)
|
|
375
|
+
else sum(l.amount_untaxed) * coalesce(t.amount, 0) / 100
|
|
376
|
+
end,
|
|
377
|
+
rounding_of(l.company_id, d.currency_code)) as tax_amount,
|
|
378
|
+
-- Charged tax: what the other party actually pays. Zero when the tax
|
|
379
|
+
-- postings net out, which is exactly what self-assessment means.
|
|
380
|
+
round_amount(
|
|
381
|
+
case when bool_and(l.unit_price_includes_tax)
|
|
382
|
+
then sum(l.amount_incl_tax)
|
|
383
|
+
- sum(l.amount_incl_tax) / (1 + coalesce(t.amount, 0) / 100)
|
|
384
|
+
else sum(l.amount_untaxed) * coalesce(t.amount, 0) / 100
|
|
385
|
+
end
|
|
386
|
+
* coalesce((
|
|
387
|
+
select sum(tp.factor_percent)
|
|
388
|
+
from tax_postings tp
|
|
389
|
+
where tp.tax_id = t.id
|
|
390
|
+
and tp.posting_type in ('tax', 'tax_on_base')
|
|
391
|
+
and tp.document_kind = case
|
|
392
|
+
when d.doc_type in ('sale_credit_note', 'purchase_credit_note')
|
|
393
|
+
then 'credit_note'::tax_document_kind
|
|
394
|
+
else 'invoice'::tax_document_kind
|
|
395
|
+
end
|
|
396
|
+
), 100) / 100,
|
|
397
|
+
rounding_of(l.company_id, d.currency_code)) as tax_charged
|
|
398
|
+
from document_lines l
|
|
399
|
+
join documents d on d.id = l.document_id
|
|
400
|
+
left join taxes t on t.id = l.tax_id
|
|
401
|
+
where l.line_type = 'product'
|
|
402
|
+
group by l.document_id, l.company_id, d.doc_type, d.currency_code, l.tax_id,
|
|
403
|
+
t.id, t.code, t.name, t.vat_category, t.amount;
|
|
404
|
+
|
|
405
|
+
comment on view document_tax_summary is
|
|
406
|
+
'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.';
|
|
407
|
+
|
|
408
|
+
-- ---------------------------------------------------------------------------
|
|
409
|
+
-- 6. The line items say which price they are showing
|
|
410
|
+
--
|
|
411
|
+
-- Two columns are appended: whether this line was quoted with its tax in it,
|
|
412
|
+
-- and the gross it was quoted at. A renderer that prints a till receipt needs
|
|
413
|
+
-- both, and so does anybody reading a base back to the price it came from.
|
|
414
|
+
--
|
|
415
|
+
-- `unit_price` keeps its own meaning — the price as it was keyed — and it is
|
|
416
|
+
-- therefore no longer BT-146 on a line quoted gross, because BT-146 is the net
|
|
417
|
+
-- price. Publishing the net one is a column this view does not get today: the
|
|
418
|
+
-- honest definition is the base divided by the quantity, which is the only one
|
|
419
|
+
-- that keeps BR-CO-10 true after the group's remainder has landed on a line,
|
|
420
|
+
-- and writing it needs a precision that is the *price* column's and not the
|
|
421
|
+
-- currency's — which `round_amount` does not express and a cast to
|
|
422
|
+
-- `numeric(16, 6)` would smuggle past the rule that there is one place where
|
|
423
|
+
-- decimals are decided. That is a gap and it is written up in
|
|
424
|
+
-- `docs/international.md` rather than solved by an exception here.
|
|
425
|
+
-- ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
create or replace view document_line_items
|
|
428
|
+
with (security_invoker = true) as
|
|
429
|
+
select l.id as document_line_id,
|
|
430
|
+
l.document_id,
|
|
431
|
+
l.company_id,
|
|
432
|
+
l.sequence,
|
|
433
|
+
l.line_type,
|
|
434
|
+
l.name as item_name, -- BT-153
|
|
435
|
+
l.description as item_description, -- BT-154
|
|
436
|
+
p.code as seller_item_identifier, -- BT-155
|
|
437
|
+
l.product_id,
|
|
438
|
+
p.kind as product_kind,
|
|
439
|
+
l.quantity, -- BT-129
|
|
440
|
+
l.unit_code, -- BT-130
|
|
441
|
+
l.unit_price, -- as keyed; gross where the tax is in it
|
|
442
|
+
l.discount_percent,
|
|
443
|
+
l.amount_untaxed, -- BT-131
|
|
444
|
+
l.tax_id,
|
|
445
|
+
l.vat_category, -- BT-151
|
|
446
|
+
l.vat_rate, -- BT-152
|
|
447
|
+
l.account_id,
|
|
448
|
+
t.treatment as tax_treatment,
|
|
449
|
+
t.exemption_code as tax_exemption_code, -- BT-121
|
|
450
|
+
t.cash_basis as tax_cash_basis,
|
|
451
|
+
l.unit_price_includes_tax,
|
|
452
|
+
l.amount_incl_tax
|
|
453
|
+
from document_lines l
|
|
454
|
+
left join products p on p.id = l.product_id
|
|
455
|
+
left join taxes t on t.id = l.tax_id;
|
|
456
|
+
|
|
457
|
+
comment on view document_line_items is
|
|
458
|
+
'Document lines with the EN 16931 item terms — BT-153 name, BT-154 description, BT-155 the seller identifier — what decides a legal mention on the line, and, where the price was quoted with the tax in it, the gross it was quoted at. unit_price is the price as keyed, which is that gross: BT-146 is the net price and this view does not publish it yet.';
|
|
459
|
+
|
|
460
|
+
grant select on table document_tax_summary, document_line_items
|
|
461
|
+
to authenticated, service_role;
|
|
462
|
+
|
|
463
|
+
-- ---------------------------------------------------------------------------
|
|
464
|
+
-- 7. The invoice behind a link says which price it is showing
|
|
465
|
+
--
|
|
466
|
+
-- `shared_document()` names every field of a line by hand, so a column added
|
|
467
|
+
-- to `document_line_items` does not reach it: a customer following the link to
|
|
468
|
+
-- a retail invoice would be shown a `unit_price` that is gross, beside an
|
|
469
|
+
-- `amount_untaxed` that is net, with nothing saying which is which. The two
|
|
470
|
+
-- fields go into `lines[]` — `unit_price_includes_tax` and `amount_incl_tax`
|
|
471
|
+
-- — and a renderer can print a till receipt from the gross or an EN 16931
|
|
472
|
+
-- invoice from the net without guessing.
|
|
473
|
+
--
|
|
474
|
+
-- Everything else is `20260915191200` unchanged, republished because a
|
|
475
|
+
-- function is replaced whole. Its grants are restated below because a replaced
|
|
476
|
+
-- function keeps the privileges it had and this one is the single door an
|
|
477
|
+
-- anonymous reader comes through: saying so again is cheaper than trusting it.
|
|
478
|
+
--
|
|
479
|
+
-- The order matters and holds: this file recreates `document_line_items`
|
|
480
|
+
-- above, then the function that reads it. Between the two migrations nothing
|
|
481
|
+
-- is broken either — a function records no dependency on a view, so
|
|
482
|
+
-- `20260915191200`'s `shared_document()` kept working against the view as it
|
|
483
|
+
-- stood, and reads two more fields only from here on.
|
|
484
|
+
-- ---------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
create or replace function shared_document(p_token text)
|
|
487
|
+
returns jsonb
|
|
488
|
+
language plpgsql
|
|
489
|
+
security definer
|
|
490
|
+
set search_path = public, pg_temp
|
|
491
|
+
as $$
|
|
492
|
+
declare
|
|
493
|
+
v_share document_shares%rowtype;
|
|
494
|
+
v_doc documents%rowtype;
|
|
495
|
+
v_header document_header%rowtype;
|
|
496
|
+
v_last_payment date;
|
|
497
|
+
v_payload jsonb;
|
|
498
|
+
begin
|
|
499
|
+
select * into v_share
|
|
500
|
+
from document_shares
|
|
501
|
+
where token_hash = encode(sha256(convert_to(coalesce(p_token, ''), 'UTF8')), 'hex');
|
|
502
|
+
|
|
503
|
+
if v_share.id is null
|
|
504
|
+
or v_share.revoked_at is not null
|
|
505
|
+
or (v_share.expires_at is not null and v_share.expires_at <= now())
|
|
506
|
+
or v_share.subject_kind <> 'document' then
|
|
507
|
+
return null;
|
|
508
|
+
end if;
|
|
509
|
+
|
|
510
|
+
select * into v_doc from documents where id = v_share.document_id;
|
|
511
|
+
if v_doc.id is null or document_share_refusal(v_doc) is not null then
|
|
512
|
+
return null;
|
|
513
|
+
end if;
|
|
514
|
+
|
|
515
|
+
-- The link was used. Counted before the payload is built, so a reader who
|
|
516
|
+
-- gives up halfway is still a reader.
|
|
517
|
+
update document_shares
|
|
518
|
+
set view_count = view_count + 1,
|
|
519
|
+
last_viewed_at = now()
|
|
520
|
+
where id = v_share.id;
|
|
521
|
+
|
|
522
|
+
select * into v_header from document_header where document_id = v_doc.id;
|
|
523
|
+
|
|
524
|
+
-- When the last money against it arrived. The other side of every matching
|
|
525
|
+
-- on the document's own third-party lines, where that side is a payment.
|
|
526
|
+
select max(p.payment_date) into v_last_payment
|
|
527
|
+
from entry_lines dl
|
|
528
|
+
join reconciliations r
|
|
529
|
+
on r.debit_line_id = dl.id or r.credit_line_id = dl.id
|
|
530
|
+
join entry_lines ol
|
|
531
|
+
on ol.id = case when r.debit_line_id = dl.id then r.credit_line_id else r.debit_line_id end
|
|
532
|
+
join payments p on p.entry_id = ol.entry_id
|
|
533
|
+
where dl.entry_id = v_doc.entry_id;
|
|
534
|
+
|
|
535
|
+
v_payload := jsonb_build_object(
|
|
536
|
+
'document', jsonb_build_object(
|
|
537
|
+
'type', v_header.doc_type,
|
|
538
|
+
'number', v_header.number,
|
|
539
|
+
'document_date', v_header.document_date,
|
|
540
|
+
'due_date', v_header.due_date,
|
|
541
|
+
'delivery_date', v_header.delivery_date,
|
|
542
|
+
'currency', v_header.currency_code,
|
|
543
|
+
-- The document's own, snapshotted when it was created and frozen when it
|
|
544
|
+
-- was posted. A customer who has since changed preference does not
|
|
545
|
+
-- change what was sent to them.
|
|
546
|
+
'language', v_doc.language,
|
|
547
|
+
'payment_terms', v_header.payment_terms,
|
|
548
|
+
'payment_reference', v_header.payment_reference,
|
|
549
|
+
'buyer_reference', v_header.buyer_reference,
|
|
550
|
+
'order_reference', v_header.order_reference,
|
|
551
|
+
-- BT-22, the note the seller wrote on the invoice itself. An internal
|
|
552
|
+
-- remark about a customer is `contacts.notes` and is not in this object.
|
|
553
|
+
'note', v_header.note
|
|
554
|
+
),
|
|
555
|
+
'seller', jsonb_build_object(
|
|
556
|
+
'name', v_header.seller_name,
|
|
557
|
+
'legal_name', v_header.seller_legal_name,
|
|
558
|
+
'legal_form', v_header.seller_legal_form,
|
|
559
|
+
'vat_number', v_header.seller_vat_number,
|
|
560
|
+
'registration_number', v_header.seller_registration_number,
|
|
561
|
+
'address_line1', v_header.seller_address_line1,
|
|
562
|
+
'address_line2', v_header.seller_address_line2,
|
|
563
|
+
'postal_code', v_header.seller_postal_code,
|
|
564
|
+
'city', v_header.seller_city,
|
|
565
|
+
'country', v_header.seller_country,
|
|
566
|
+
'email', v_header.seller_email,
|
|
567
|
+
'phone', v_header.seller_phone,
|
|
568
|
+
'website', v_header.seller_website,
|
|
569
|
+
'logo_url', v_header.seller_logo_url,
|
|
570
|
+
'iban', v_header.payee_iban,
|
|
571
|
+
'bic', v_header.payee_bic
|
|
572
|
+
),
|
|
573
|
+
'buyer', jsonb_build_object(
|
|
574
|
+
'name', v_header.buyer_name,
|
|
575
|
+
'vat_number', v_header.buyer_vat_number,
|
|
576
|
+
'registration_number', v_header.buyer_registration_number,
|
|
577
|
+
'address_line1', v_header.buyer_address_line1,
|
|
578
|
+
'address_line2', v_header.buyer_address_line2,
|
|
579
|
+
'postal_code', v_header.buyer_postal_code,
|
|
580
|
+
'city', v_header.buyer_city,
|
|
581
|
+
'country', v_header.buyer_country
|
|
582
|
+
),
|
|
583
|
+
'lines', coalesce((
|
|
584
|
+
select jsonb_agg(jsonb_build_object(
|
|
585
|
+
'sequence', li.sequence,
|
|
586
|
+
'type', li.line_type,
|
|
587
|
+
'name', li.item_name,
|
|
588
|
+
'description', li.item_description,
|
|
589
|
+
'quantity', li.quantity::text,
|
|
590
|
+
'unit_code', li.unit_code,
|
|
591
|
+
-- The price as it was keyed. On a line quoted with the tax
|
|
592
|
+
-- in it that is the gross one, and the two fields below say so
|
|
593
|
+
-- rather than leaving a reader to divide `amount_untaxed` by
|
|
594
|
+
-- the quantity and wonder.
|
|
595
|
+
'unit_price', li.unit_price::text,
|
|
596
|
+
'unit_price_includes_tax', li.unit_price_includes_tax,
|
|
597
|
+
'discount_percent', li.discount_percent::text,
|
|
598
|
+
'amount_untaxed', li.amount_untaxed::text,
|
|
599
|
+
'amount_incl_tax', li.amount_incl_tax::text,
|
|
600
|
+
'tax_category', li.vat_category,
|
|
601
|
+
'tax_rate', li.vat_rate::text,
|
|
602
|
+
'tax_exemption_code', li.tax_exemption_code)
|
|
603
|
+
order by li.sequence)
|
|
604
|
+
from document_line_items li
|
|
605
|
+
where li.document_id = v_doc.id), '[]'::jsonb),
|
|
606
|
+
'tax_summary', coalesce((
|
|
607
|
+
select jsonb_agg(jsonb_build_object(
|
|
608
|
+
'name', ts.tax_name,
|
|
609
|
+
'category', ts.vat_category,
|
|
610
|
+
'rate', ts.tax_rate::text,
|
|
611
|
+
'base_amount', ts.base_amount::text,
|
|
612
|
+
-- What the other party actually pays, which is what an invoice
|
|
613
|
+
-- prints: zero where the tax self-assesses, and what the four
|
|
614
|
+
-- totals below add up from.
|
|
615
|
+
'tax_amount', ts.tax_charged::text)
|
|
616
|
+
order by ts.tax_rate, ts.tax_name)
|
|
617
|
+
from document_tax_summary ts
|
|
618
|
+
where ts.document_id = v_doc.id), '[]'::jsonb),
|
|
619
|
+
'totals', jsonb_build_object(
|
|
620
|
+
'amount_untaxed', v_header.amount_untaxed::text,
|
|
621
|
+
'amount_tax', v_header.amount_tax::text,
|
|
622
|
+
'amount_total', v_header.amount_total::text
|
|
623
|
+
),
|
|
624
|
+
-- The one thing that legitimately moves after the document was sent, so a
|
|
625
|
+
-- link a customer keeps stays worth opening.
|
|
626
|
+
'payment', jsonb_build_object(
|
|
627
|
+
'state', v_header.payment_state,
|
|
628
|
+
'amount_paid', v_header.amount_paid::text,
|
|
629
|
+
'amount_residual', v_header.amount_residual::text,
|
|
630
|
+
'last_payment_date', v_last_payment
|
|
631
|
+
),
|
|
632
|
+
'legal_mentions', coalesce((
|
|
633
|
+
select jsonb_agg(jsonb_build_object(
|
|
634
|
+
'code', lm.code,
|
|
635
|
+
'text', lm.text)
|
|
636
|
+
order by lm.sequence, lm.code)
|
|
637
|
+
from document_legal_mentions lm
|
|
638
|
+
where lm.document_id = v_doc.id), '[]'::jsonb)
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
return v_payload;
|
|
642
|
+
end;
|
|
643
|
+
$$;
|
|
644
|
+
|
|
645
|
+
comment on function shared_document(text) is
|
|
646
|
+
'One document, read by whoever holds its link: the header, the lines with the price as it was keyed and whether that price holds the tax, 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.';
|
|
647
|
+
|
|
648
|
+
revoke execute on function shared_document(text) from public;
|
|
649
|
+
grant execute on function shared_document(text) to anon, authenticated, service_role;
|