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,902 @@
|
|
|
1
|
+
-- Ekwo OS — a ledger line says which posting wrote it.
|
|
2
|
+
--
|
|
3
|
+
-- `entry_lines` carries `tax_id` and `tax_line`, and between them they cannot
|
|
4
|
+
-- tell a `base` line from a `tax_on_base` line. Both belong to the same tax,
|
|
5
|
+
-- both are booked on a base account, and both have `tax_line = false` — which
|
|
6
|
+
-- is right, because a non-deductible share of tax is a cost and belongs on the
|
|
7
|
+
-- base side of the declaration, and it is exactly why the Belgian grids 82 and
|
|
8
|
+
-- 83 report it together with the base. What it cost is that anything reading
|
|
9
|
+
-- the ledger *by tax* rather than *by box* had to know that one of the two
|
|
10
|
+
-- could not occur.
|
|
11
|
+
--
|
|
12
|
+
-- `ec_sales_list()` is the reader that found it. It takes the lines of a tax
|
|
13
|
+
-- that are not tax lines, which is exact today because an intra-Community
|
|
14
|
+
-- supply is exempt and has no tax to capitalise — and would stop being exact
|
|
15
|
+
-- the day a pack said otherwise, silently, by adding a supply's `tax_on_base`
|
|
16
|
+
-- amount to what the statement declares. `docs/international.md` recorded it
|
|
17
|
+
-- under *A ledger line does not say which posting wrote it*, and proposed
|
|
18
|
+
-- exactly what is here: the posting's own type, copied onto the line beside the
|
|
19
|
+
-- `declaration_box` that is already copied there.
|
|
20
|
+
--
|
|
21
|
+
-- **The vocabulary is `tax_postings`'s own.** `tax_posting_type` — `base`,
|
|
22
|
+
-- `tax`, `tax_on_base` — because the column answers *which posting wrote this
|
|
23
|
+
-- line*, and a posting's type is the only correct answer. A second enum
|
|
24
|
+
-- spelling the same three words would be the same fact written twice.
|
|
25
|
+
--
|
|
26
|
+
-- **A line that is no posting is null, not a fourth word.** The counterpart of
|
|
27
|
+
-- an invoice, the two legs of a payment, the lines of a closing entry: none of
|
|
28
|
+
-- them was written by a tax posting, and null is what a column says about a
|
|
29
|
+
-- question that does not apply to the row. Adding a `none` to
|
|
30
|
+
-- `tax_posting_type` would put a value meaning *not a posting* inside the
|
|
31
|
+
-- vocabulary of what a posting is, and `tax_postings.posting_type` would then
|
|
32
|
+
-- have to refuse it — a constraint existing only to undo an enum value. A
|
|
33
|
+
-- check constraint says the useful half instead: a line with a posting type
|
|
34
|
+
-- has a tax, so the two are never read apart.
|
|
35
|
+
--
|
|
36
|
+
-- Null therefore carries two readings, and `tax_id` separates them: null with
|
|
37
|
+
-- no tax is *not a tax posting*, and null with a tax is *written before this
|
|
38
|
+
-- migration and not identified*. Which brings us to the backfill.
|
|
39
|
+
--
|
|
40
|
+
-- **The backfill only writes what is certain.** A ledger is not rewritten by
|
|
41
|
+
-- deduction. A line is filled when its tax and its declaration box name one
|
|
42
|
+
-- posting type and no other: `tax_postings` is asked which types write a line
|
|
43
|
+
-- of that tax into that box on that side of `tax_line`, and the line is filled
|
|
44
|
+
-- only where the answer is a single word. Everything else — a line whose
|
|
45
|
+
-- posting names no box, the cash-basis lines that wait with their box held back
|
|
46
|
+
-- until settlement, a box two posting types both write to — stays null, and the
|
|
47
|
+
-- `tax_id` beside it says that null means unknown there rather than
|
|
48
|
+
-- inapplicable. Nothing guesses, and nothing that reads this column afterwards
|
|
49
|
+
-- can mistake a guess for a fact.
|
|
50
|
+
--
|
|
51
|
+
-- **Who fills it from now on.** `post_document()`, on all three of the lines a
|
|
52
|
+
-- tax produces, from the posting row it is already reading; and
|
|
53
|
+
-- `settle_cash_basis_tax()`, on both legs of the transfer that makes a
|
|
54
|
+
-- cash-basis tax due — the leg that carries the box and the leg that empties
|
|
55
|
+
-- the account the amount waited on are one posting falling due, so they carry
|
|
56
|
+
-- one posting type. Both functions are replaced whole, because a function is
|
|
57
|
+
-- replaced whole in PostgreSQL, and nothing else in either of them moves.
|
|
58
|
+
--
|
|
59
|
+
-- Nothing else writes a tax line. `post_payment()` books money against a
|
|
60
|
+
-- third-party account, `close_fiscal_year()` and `reopen_fiscal_year()` move
|
|
61
|
+
-- results between accounts the pack names, and `post_module_entry()` takes the
|
|
62
|
+
-- lines a module hands it and knows no tax at all.
|
|
63
|
+
|
|
64
|
+
-- ---------------------------------------------------------------------------
|
|
65
|
+
-- The column
|
|
66
|
+
-- ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
alter table entry_lines
|
|
69
|
+
add column if not exists posting_type tax_posting_type;
|
|
70
|
+
|
|
71
|
+
comment on column entry_lines.posting_type is
|
|
72
|
+
'Which tax posting wrote this line: base, tax, or tax_on_base, from tax_postings.posting_type. Null on a line no tax posting wrote — a counterpart, a payment, a closing line — and null on a line written before this column existed whose posting could not be identified beyond doubt; tax_id tells the two apart. It is what makes a base line and a tax_on_base line of the same tax on the same account distinguishable, which tax_id and tax_line together cannot do.';
|
|
73
|
+
|
|
74
|
+
alter table entry_lines
|
|
75
|
+
add constraint entry_lines_posting_type_has_a_tax check (
|
|
76
|
+
posting_type is null or tax_id is not null
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
-- ---------------------------------------------------------------------------
|
|
80
|
+
-- What the existing ledger can be told for certain
|
|
81
|
+
-- ---------------------------------------------------------------------------
|
|
82
|
+
--
|
|
83
|
+
-- One posting type, or nothing. `writes_tax_line` is derived from the type
|
|
84
|
+
-- rather than stored, because it is: a `tax` posting is the only one that books
|
|
85
|
+
-- a tax line, and `base` and `tax_on_base` both book a line that is not one.
|
|
86
|
+
-- Two postings of the same tax writing the same box on the same side is an
|
|
87
|
+
-- ambiguity nothing on the line can settle, so those lines stay null.
|
|
88
|
+
|
|
89
|
+
with candidate as (
|
|
90
|
+
select distinct
|
|
91
|
+
tp.tax_id,
|
|
92
|
+
tp.declaration_box,
|
|
93
|
+
(tp.posting_type = 'tax'::tax_posting_type) as writes_tax_line,
|
|
94
|
+
tp.posting_type
|
|
95
|
+
from tax_postings tp
|
|
96
|
+
where tp.declaration_box is not null
|
|
97
|
+
),
|
|
98
|
+
unambiguous as (
|
|
99
|
+
select c.tax_id, c.declaration_box, c.writes_tax_line, min(c.posting_type) as posting_type
|
|
100
|
+
from candidate c
|
|
101
|
+
group by c.tax_id, c.declaration_box, c.writes_tax_line
|
|
102
|
+
having count(*) = 1
|
|
103
|
+
)
|
|
104
|
+
update entry_lines l
|
|
105
|
+
set posting_type = u.posting_type
|
|
106
|
+
from unambiguous u
|
|
107
|
+
where l.tax_id = u.tax_id
|
|
108
|
+
and l.declaration_box = u.declaration_box
|
|
109
|
+
and l.tax_line = u.writes_tax_line
|
|
110
|
+
and l.posting_type is null;
|
|
111
|
+
|
|
112
|
+
-- ---------------------------------------------------------------------------
|
|
113
|
+
-- Everything that writes a tax line, writing it
|
|
114
|
+
-- ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
create or replace function post_document(p_document_id uuid)
|
|
117
|
+
returns entries
|
|
118
|
+
language plpgsql
|
|
119
|
+
as $$
|
|
120
|
+
declare
|
|
121
|
+
v_doc documents%rowtype;
|
|
122
|
+
v_entry entries%rowtype;
|
|
123
|
+
v_journal uuid;
|
|
124
|
+
v_date date;
|
|
125
|
+
v_is_sale boolean;
|
|
126
|
+
v_is_credit boolean;
|
|
127
|
+
v_kind tax_document_kind;
|
|
128
|
+
v_base_credit boolean;
|
|
129
|
+
v_seq integer := 0;
|
|
130
|
+
v_contact uuid;
|
|
131
|
+
v_maturity date;
|
|
132
|
+
v_terms smallint;
|
|
133
|
+
v_counterpart uuid;
|
|
134
|
+
-- Plain `numeric`, not `numeric(16, 2)`: a local that carries a scale is a
|
|
135
|
+
-- second rounding rule hiding in a declaration, and it is not the currency's.
|
|
136
|
+
-- The only thing that rounds here is `round_amount`.
|
|
137
|
+
v_diff numeric;
|
|
138
|
+
v_diff_cur numeric;
|
|
139
|
+
v_amount numeric;
|
|
140
|
+
v_book numeric;
|
|
141
|
+
v_box_amount numeric;
|
|
142
|
+
v_share numeric;
|
|
143
|
+
v_share_book numeric;
|
|
144
|
+
v_share_box numeric;
|
|
145
|
+
v_left numeric;
|
|
146
|
+
v_left_box numeric;
|
|
147
|
+
v_side_left numeric;
|
|
148
|
+
v_side_left_neg numeric;
|
|
149
|
+
v_side_credit boolean;
|
|
150
|
+
v_label text;
|
|
151
|
+
v_rate numeric(18, 8);
|
|
152
|
+
v_home char(3);
|
|
153
|
+
v_foreign boolean;
|
|
154
|
+
v_total_cur numeric;
|
|
155
|
+
-- Two currencies, one method: the document is stated in its own and the
|
|
156
|
+
-- ledger keeps the company's, and a yen invoice paid in euros rounds each
|
|
157
|
+
-- side at the decimals that side has.
|
|
158
|
+
v_round money_rounding;
|
|
159
|
+
v_book_round money_rounding;
|
|
160
|
+
v_cash boolean;
|
|
161
|
+
v_transition uuid;
|
|
162
|
+
v_postings integer;
|
|
163
|
+
r record;
|
|
164
|
+
p record;
|
|
165
|
+
g record;
|
|
166
|
+
begin
|
|
167
|
+
select * into v_doc from documents where id = p_document_id for update;
|
|
168
|
+
if not found then
|
|
169
|
+
raise exception 'unknown_document: document % does not exist', p_document_id;
|
|
170
|
+
end if;
|
|
171
|
+
if v_doc.state = 'posted' then
|
|
172
|
+
raise exception 'document_already_posted: document % is already posted', p_document_id;
|
|
173
|
+
end if;
|
|
174
|
+
if v_doc.state = 'cancelled' then
|
|
175
|
+
raise exception 'document_cancelled: document % cannot be posted', p_document_id;
|
|
176
|
+
end if;
|
|
177
|
+
if v_doc.entry_id is not null then
|
|
178
|
+
raise exception 'document_already_booked: document % already points at entry %',
|
|
179
|
+
p_document_id, v_doc.entry_id;
|
|
180
|
+
end if;
|
|
181
|
+
|
|
182
|
+
if v_doc.doc_type in ('sale_quote', 'purchase_order') then
|
|
183
|
+
raise exception 'document_not_accountable: a % is not booked', v_doc.doc_type;
|
|
184
|
+
end if;
|
|
185
|
+
|
|
186
|
+
v_is_sale := v_doc.doc_type in ('sale_invoice', 'sale_credit_note');
|
|
187
|
+
v_is_credit := v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note');
|
|
188
|
+
v_kind := case when v_is_credit then 'credit_note' else 'invoice' end::tax_document_kind;
|
|
189
|
+
-- Sale invoice and purchase credit note credit the base; the other two debit it.
|
|
190
|
+
v_base_credit := (v_is_sale <> v_is_credit);
|
|
191
|
+
|
|
192
|
+
v_date := coalesce(v_doc.accounting_date, v_doc.document_date);
|
|
193
|
+
|
|
194
|
+
if not exists (
|
|
195
|
+
select 1 from document_lines
|
|
196
|
+
where document_id = p_document_id and line_type = 'product' and amount_untaxed <> 0
|
|
197
|
+
) then
|
|
198
|
+
raise exception 'document_empty: document % has no billable line', p_document_id;
|
|
199
|
+
end if;
|
|
200
|
+
|
|
201
|
+
-- A fixed-amount tax has no basis to spread over lines; refuse rather than
|
|
202
|
+
-- guess.
|
|
203
|
+
if exists (
|
|
204
|
+
select 1 from document_lines l join taxes t on t.id = l.tax_id
|
|
205
|
+
where l.document_id = p_document_id and t.amount_type <> 'percent'
|
|
206
|
+
) then
|
|
207
|
+
raise exception 'unsupported_tax_amount_type: only percentage taxes can be posted';
|
|
208
|
+
end if;
|
|
209
|
+
|
|
210
|
+
-- Every tax used must be in force on the accounting date.
|
|
211
|
+
for r in
|
|
212
|
+
select distinct t.id, t.code, t.valid_from, t.valid_to
|
|
213
|
+
from document_lines l join taxes t on t.id = l.tax_id
|
|
214
|
+
where l.document_id = p_document_id
|
|
215
|
+
loop
|
|
216
|
+
if v_date < r.valid_from or (r.valid_to is not null and v_date > r.valid_to) then
|
|
217
|
+
raise exception 'tax_not_in_force: tax % is not applicable on %', r.code, v_date;
|
|
218
|
+
end if;
|
|
219
|
+
end loop;
|
|
220
|
+
|
|
221
|
+
-- Totals are derived; make sure they reflect the lines as they stand now.
|
|
222
|
+
perform documents_refresh_totals(p_document_id);
|
|
223
|
+
select * into v_doc from documents where id = p_document_id;
|
|
224
|
+
|
|
225
|
+
select c.currency_code into v_home from companies c where c.id = v_doc.company_id;
|
|
226
|
+
v_rate := v_doc.exchange_rate;
|
|
227
|
+
v_foreign := v_doc.currency_code <> v_home;
|
|
228
|
+
v_round := rounding_of(v_doc.company_id, v_doc.currency_code);
|
|
229
|
+
v_book_round := rounding_of(v_doc.company_id);
|
|
230
|
+
|
|
231
|
+
v_journal := coalesce(
|
|
232
|
+
v_doc.journal_id,
|
|
233
|
+
case when v_is_sale
|
|
234
|
+
then (select sales_journal_id from companies where id = v_doc.company_id)
|
|
235
|
+
else (select purchase_journal_id from companies where id = v_doc.company_id)
|
|
236
|
+
end
|
|
237
|
+
);
|
|
238
|
+
if v_journal is null then
|
|
239
|
+
raise exception 'no_journal: set journal_id on the document or a default journal on the company';
|
|
240
|
+
end if;
|
|
241
|
+
|
|
242
|
+
perform assert_period_open(v_doc.company_id, v_date, true);
|
|
243
|
+
|
|
244
|
+
v_label := coalesce(v_doc.number, v_doc.supplier_reference, 'document');
|
|
245
|
+
|
|
246
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
|
|
247
|
+
description, state, document_id, currency_code)
|
|
248
|
+
values (v_doc.company_id, v_journal, fiscal_year_at(v_doc.company_id, v_date), v_date,
|
|
249
|
+
coalesce(v_doc.number, v_doc.supplier_reference),
|
|
250
|
+
v_label || case when v_doc.supplier_reference is not null and v_doc.number is not null
|
|
251
|
+
then ' / ' || v_doc.supplier_reference else '' end,
|
|
252
|
+
'draft', p_document_id, v_doc.currency_code)
|
|
253
|
+
returning * into v_entry;
|
|
254
|
+
|
|
255
|
+
-- ------------------------------------------------------------------ bases
|
|
256
|
+
for r in
|
|
257
|
+
select l.account_id,
|
|
258
|
+
l.tax_id,
|
|
259
|
+
sum(l.amount_untaxed) as base_amount,
|
|
260
|
+
min(l.sequence) as seq,
|
|
261
|
+
string_agg(distinct l.name, ', ') as label
|
|
262
|
+
from document_lines l
|
|
263
|
+
where l.document_id = p_document_id
|
|
264
|
+
and l.line_type = 'product'
|
|
265
|
+
group by l.account_id, l.tax_id
|
|
266
|
+
having sum(l.amount_untaxed) <> 0
|
|
267
|
+
order by 4
|
|
268
|
+
loop
|
|
269
|
+
select tp.posting_type, tp.declaration_box, tp.factor_percent, tp.box_factor_percent,
|
|
270
|
+
coalesce(t.cash_basis, false) as cash_basis
|
|
271
|
+
into p
|
|
272
|
+
from tax_postings tp
|
|
273
|
+
join taxes t on t.id = tp.tax_id
|
|
274
|
+
where tp.tax_id = r.tax_id
|
|
275
|
+
and tp.document_kind = v_kind
|
|
276
|
+
and tp.posting_type = 'base'
|
|
277
|
+
limit 1;
|
|
278
|
+
|
|
279
|
+
v_amount := round_amount(r.base_amount * coalesce(p.factor_percent, 100) / 100, v_round);
|
|
280
|
+
v_book := round_amount(v_amount / v_rate, v_book_round);
|
|
281
|
+
v_seq := v_seq + 10;
|
|
282
|
+
|
|
283
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
284
|
+
debit, credit, tax_id, tax_line, posting_type,
|
|
285
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
286
|
+
values (v_entry.id, v_doc.company_id, r.account_id, v_seq, left(r.label, 200),
|
|
287
|
+
case when v_base_credit then 0 else v_book end,
|
|
288
|
+
case when v_base_credit then v_book else 0 end,
|
|
289
|
+
r.tax_id, false, p.posting_type,
|
|
290
|
+
case when coalesce(p.cash_basis, false) then null else p.declaration_box end,
|
|
291
|
+
case when p.declaration_box is null then null
|
|
292
|
+
else round_amount(r.base_amount * coalesce(p.box_factor_percent, 100) / 100 / v_rate,
|
|
293
|
+
v_book_round) end,
|
|
294
|
+
v_doc.currency_code,
|
|
295
|
+
case when v_foreign then v_amount end);
|
|
296
|
+
end loop;
|
|
297
|
+
|
|
298
|
+
-- ------------------------------------------------------------------ taxes
|
|
299
|
+
for r in
|
|
300
|
+
select s.tax_id, s.tax_code, s.tax_name, s.tax_amount
|
|
301
|
+
from document_tax_summary s
|
|
302
|
+
where s.document_id = p_document_id
|
|
303
|
+
and s.tax_id is not null
|
|
304
|
+
and s.tax_amount <> 0
|
|
305
|
+
order by s.tax_code
|
|
306
|
+
loop
|
|
307
|
+
select t.cash_basis, t.cash_basis_transition_account_id
|
|
308
|
+
into v_cash, v_transition
|
|
309
|
+
from taxes t where t.id = r.tax_id;
|
|
310
|
+
|
|
311
|
+
if v_cash then
|
|
312
|
+
-- A tax that waits needs somewhere to wait. Refuse by name rather than
|
|
313
|
+
-- book it on the account it is due on, which would make it due.
|
|
314
|
+
if v_transition is null then
|
|
315
|
+
raise exception 'no_cash_basis_account: tax % falls due on collection and names no transition account',
|
|
316
|
+
r.tax_code;
|
|
317
|
+
end if;
|
|
318
|
+
-- One posting per side, or the transition lines of a document cannot be
|
|
319
|
+
-- told apart when the matching sends each of them on. A tax whose
|
|
320
|
+
-- postings net out has nothing waiting to collect anyway.
|
|
321
|
+
select count(*) into v_postings
|
|
322
|
+
from tax_postings tp
|
|
323
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
324
|
+
and tp.posting_type = 'tax';
|
|
325
|
+
if v_postings > 1 then
|
|
326
|
+
raise exception 'cash_basis_split_tax: tax % falls due on collection and has % tax postings; it takes one',
|
|
327
|
+
r.tax_code, v_postings;
|
|
328
|
+
end if;
|
|
329
|
+
if exists (select 1 from tax_postings tp
|
|
330
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
331
|
+
and tp.posting_type = 'tax_on_base') then
|
|
332
|
+
raise exception 'cash_basis_tax_on_base: tax % falls due on collection and carries a non-deductible share; a cost is not deferred',
|
|
333
|
+
r.tax_code;
|
|
334
|
+
end if;
|
|
335
|
+
-- And it needs a box to fall due *into*. `settle_cash_basis_tax()` only
|
|
336
|
+
-- ever looks at lines that carry a `box_amount`, and a posting with no
|
|
337
|
+
-- `declaration_box` produces none — so the amount would sit on the
|
|
338
|
+
-- transition account for ever, settled by nothing and reported by
|
|
339
|
+
-- nothing, with no error anywhere. Refuse it here, where the pack can
|
|
340
|
+
-- still be corrected, rather than discover it in a balance years later.
|
|
341
|
+
if not exists (select 1 from tax_postings tp
|
|
342
|
+
where tp.tax_id = r.tax_id and tp.document_kind = v_kind
|
|
343
|
+
and tp.posting_type = 'tax'
|
|
344
|
+
and tp.declaration_box is not null) then
|
|
345
|
+
raise exception 'no_cash_basis_box: tax % falls due on collection and its posting names no declaration box; the amount would wait on the transition account and never settle',
|
|
346
|
+
r.tax_code;
|
|
347
|
+
end if;
|
|
348
|
+
end if;
|
|
349
|
+
|
|
350
|
+
-- The postings of one side share out the tax of the group; the last of
|
|
351
|
+
-- each side takes what is left. Until `tax_on_base` there was never more
|
|
352
|
+
-- than one posting per side, so this changes no existing tax by a cent —
|
|
353
|
+
-- and it is what keeps a 50/50 split honest: 0.63 becomes 0.32 and 0.31,
|
|
354
|
+
-- where rounding each half on its own would book 0.64 against a document
|
|
355
|
+
-- that totals 0.63.
|
|
356
|
+
v_side_left := null;
|
|
357
|
+
v_side_left_neg := null;
|
|
358
|
+
|
|
359
|
+
for p in
|
|
360
|
+
select tp.posting_type, tp.factor_percent, tp.account_id,
|
|
361
|
+
tp.declaration_box, tp.box_factor_percent,
|
|
362
|
+
case when tp.factor_percent >= 0 then 1 else -1 end as side,
|
|
363
|
+
sum(abs(tp.factor_percent))
|
|
364
|
+
over (partition by case when tp.factor_percent >= 0 then 1 else -1 end)
|
|
365
|
+
as side_factor,
|
|
366
|
+
row_number() over (
|
|
367
|
+
partition by case when tp.factor_percent >= 0 then 1 else -1 end
|
|
368
|
+
order by tp.sequence, tp.id)
|
|
369
|
+
= count(*) over (
|
|
370
|
+
partition by case when tp.factor_percent >= 0 then 1 else -1 end)
|
|
371
|
+
as is_last_of_side
|
|
372
|
+
from tax_postings tp
|
|
373
|
+
where tp.tax_id = r.tax_id
|
|
374
|
+
and tp.document_kind = v_kind
|
|
375
|
+
and tp.posting_type in ('tax', 'tax_on_base')
|
|
376
|
+
order by tp.sequence, tp.id
|
|
377
|
+
loop
|
|
378
|
+
-- The tax of the group was rounded once, in the view. Every posting is
|
|
379
|
+
-- a share of that one figure, never of a re-derived one.
|
|
380
|
+
if p.side >= 0 then
|
|
381
|
+
if v_side_left is null then
|
|
382
|
+
v_side_left := round_amount(r.tax_amount * p.side_factor / 100, v_round);
|
|
383
|
+
end if;
|
|
384
|
+
if p.is_last_of_side then
|
|
385
|
+
v_amount := v_side_left;
|
|
386
|
+
else
|
|
387
|
+
v_amount := round_amount(r.tax_amount * abs(p.factor_percent) / 100, v_round);
|
|
388
|
+
v_side_left := v_side_left - v_amount;
|
|
389
|
+
end if;
|
|
390
|
+
else
|
|
391
|
+
if v_side_left_neg is null then
|
|
392
|
+
v_side_left_neg := round_amount(r.tax_amount * p.side_factor / 100, v_round);
|
|
393
|
+
end if;
|
|
394
|
+
if p.is_last_of_side then
|
|
395
|
+
v_amount := v_side_left_neg;
|
|
396
|
+
else
|
|
397
|
+
v_amount := round_amount(r.tax_amount * abs(p.factor_percent) / 100, v_round);
|
|
398
|
+
v_side_left_neg := v_side_left_neg - v_amount;
|
|
399
|
+
end if;
|
|
400
|
+
end if;
|
|
401
|
+
|
|
402
|
+
if v_amount = 0 then
|
|
403
|
+
continue;
|
|
404
|
+
end if;
|
|
405
|
+
-- A positive factor keeps the side of the base, a negative one flips it.
|
|
406
|
+
v_side_credit := case when p.factor_percent >= 0 then v_base_credit else not v_base_credit end;
|
|
407
|
+
-- The box keeps its own rounding: `box_factor_percent` was always
|
|
408
|
+
-- independent from `factor_percent`, because a declaration figure is
|
|
409
|
+
-- not a ledger figure and only the ledger has to balance.
|
|
410
|
+
v_box_amount := case when p.declaration_box is null then null
|
|
411
|
+
else round_amount(r.tax_amount * p.box_factor_percent / 100 / v_rate,
|
|
412
|
+
v_book_round) end;
|
|
413
|
+
v_book := round_amount(v_amount / v_rate, v_book_round);
|
|
414
|
+
|
|
415
|
+
if p.posting_type = 'tax' then
|
|
416
|
+
v_seq := v_seq + 10;
|
|
417
|
+
|
|
418
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
419
|
+
debit, credit, tax_id, tax_line, posting_type,
|
|
420
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
421
|
+
values (v_entry.id, v_doc.company_id,
|
|
422
|
+
case when v_cash then v_transition else p.account_id end,
|
|
423
|
+
v_seq, r.tax_name,
|
|
424
|
+
case when v_side_credit then 0 else v_book end,
|
|
425
|
+
case when v_side_credit then v_book else 0 end,
|
|
426
|
+
r.tax_id, true, p.posting_type,
|
|
427
|
+
case when v_cash then null else p.declaration_box end,
|
|
428
|
+
v_box_amount, v_doc.currency_code,
|
|
429
|
+
case when v_foreign then v_amount end);
|
|
430
|
+
continue;
|
|
431
|
+
end if;
|
|
432
|
+
|
|
433
|
+
-- `tax_on_base`: the tax is a cost, so it lands on the accounts of the
|
|
434
|
+
-- lines it taxes, split in proportion to their base. The last share
|
|
435
|
+
-- takes whatever is left, so the shares add up to the amount that was
|
|
436
|
+
-- rounded once on the group and the entry still balances to the cent.
|
|
437
|
+
v_left := v_amount;
|
|
438
|
+
v_left_box := v_box_amount;
|
|
439
|
+
|
|
440
|
+
for g in
|
|
441
|
+
select account_id,
|
|
442
|
+
base_amount,
|
|
443
|
+
seq,
|
|
444
|
+
sum(base_amount) over () as total_base,
|
|
445
|
+
row_number() over (order by seq) = count(*) over () as is_last
|
|
446
|
+
from (
|
|
447
|
+
select l.account_id,
|
|
448
|
+
sum(l.amount_untaxed) as base_amount,
|
|
449
|
+
min(l.sequence) as seq
|
|
450
|
+
from document_lines l
|
|
451
|
+
where l.document_id = p_document_id
|
|
452
|
+
and l.line_type = 'product'
|
|
453
|
+
and l.tax_id = r.tax_id
|
|
454
|
+
group by l.account_id
|
|
455
|
+
having sum(l.amount_untaxed) <> 0
|
|
456
|
+
) as groups
|
|
457
|
+
order by seq
|
|
458
|
+
loop
|
|
459
|
+
if g.is_last then
|
|
460
|
+
v_share := v_left;
|
|
461
|
+
v_share_box := v_left_box;
|
|
462
|
+
else
|
|
463
|
+
v_share := round_amount(v_amount * g.base_amount / g.total_base, v_round);
|
|
464
|
+
v_share_box := case when v_box_amount is null then null
|
|
465
|
+
else round_amount(v_box_amount * g.base_amount / g.total_base,
|
|
466
|
+
v_book_round) end;
|
|
467
|
+
v_left := v_left - v_share;
|
|
468
|
+
v_left_box := v_left_box - v_share_box;
|
|
469
|
+
end if;
|
|
470
|
+
|
|
471
|
+
if v_share = 0 then
|
|
472
|
+
continue;
|
|
473
|
+
end if;
|
|
474
|
+
|
|
475
|
+
v_seq := v_seq + 10;
|
|
476
|
+
v_share_book := round_amount(v_share / v_rate, v_book_round);
|
|
477
|
+
|
|
478
|
+
-- `tax_line` stays false: the amount is on a base account and belongs
|
|
479
|
+
-- to the base side of the declaration, which is why the Belgian grids
|
|
480
|
+
-- 82 and 83 report it together with the base.
|
|
481
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
482
|
+
debit, credit, tax_id, tax_line, posting_type,
|
|
483
|
+
declaration_box, box_amount, currency_code, amount_currency)
|
|
484
|
+
values (v_entry.id, v_doc.company_id, g.account_id, v_seq, r.tax_name,
|
|
485
|
+
case when v_side_credit then 0 else v_share_book end,
|
|
486
|
+
case when v_side_credit then v_share_book else 0 end,
|
|
487
|
+
r.tax_id, false, p.posting_type,
|
|
488
|
+
p.declaration_box, v_share_box, v_doc.currency_code,
|
|
489
|
+
case when v_foreign then v_share end);
|
|
490
|
+
end loop;
|
|
491
|
+
end loop;
|
|
492
|
+
end loop;
|
|
493
|
+
|
|
494
|
+
-- ------------------------------------------------------------ counterpart
|
|
495
|
+
select total_debit - total_credit into v_diff from entries where id = v_entry.id;
|
|
496
|
+
select coalesce(sum(case when l.debit > 0 then l.amount_currency else -l.amount_currency end), 0)
|
|
497
|
+
into v_diff_cur
|
|
498
|
+
from entry_lines l where l.entry_id = v_entry.id;
|
|
499
|
+
|
|
500
|
+
if v_diff = 0 then
|
|
501
|
+
raise exception 'document_counterpart_zero: document % produced a nil counterpart', p_document_id;
|
|
502
|
+
end if;
|
|
503
|
+
|
|
504
|
+
v_contact := commercial_entity(v_doc.contact_id);
|
|
505
|
+
v_counterpart := resolve_counterpart_account(v_doc.company_id, v_contact, v_is_sale);
|
|
506
|
+
|
|
507
|
+
select payment_terms_days into v_terms from contacts where id = v_contact;
|
|
508
|
+
v_maturity := coalesce(v_doc.due_date, v_doc.document_date + coalesce(v_terms, 30));
|
|
509
|
+
|
|
510
|
+
v_amount := abs(v_diff);
|
|
511
|
+
-- The counterpart balances the entry in both currencies. The total it is
|
|
512
|
+
-- checked against is the document's own, which is the currency
|
|
513
|
+
-- `amount_total` is stated in.
|
|
514
|
+
v_total_cur := case when v_foreign then abs(v_diff_cur) else v_amount end;
|
|
515
|
+
v_seq := v_seq + 10;
|
|
516
|
+
|
|
517
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
518
|
+
debit, credit, contact_id, date_maturity, currency_code,
|
|
519
|
+
amount_currency)
|
|
520
|
+
values (v_entry.id, v_doc.company_id, v_counterpart, v_seq, v_label,
|
|
521
|
+
case when v_diff > 0 then 0 else v_amount end,
|
|
522
|
+
case when v_diff > 0 then v_amount else 0 end,
|
|
523
|
+
v_contact, v_maturity, v_doc.currency_code,
|
|
524
|
+
case when v_foreign then v_total_cur end);
|
|
525
|
+
|
|
526
|
+
-- The ledger is right by construction. If the header disagrees, the header
|
|
527
|
+
-- is what is wrong, and we say so instead of quietly patching a line.
|
|
528
|
+
-- Half a unit of the document's own currency, which is what `0.005` used to
|
|
529
|
+
-- mean when every currency was assumed to have cents.
|
|
530
|
+
if abs(v_total_cur - abs(v_doc.amount_total)) > currency_unit(v_round) / 2 then
|
|
531
|
+
raise exception 'document_total_mismatch: document % totals % but its lines book %',
|
|
532
|
+
p_document_id, v_doc.amount_total, v_total_cur;
|
|
533
|
+
end if;
|
|
534
|
+
|
|
535
|
+
-- --------------------------------------------------------------- posting
|
|
536
|
+
v_entry := post_entry(v_entry.id);
|
|
537
|
+
|
|
538
|
+
update documents
|
|
539
|
+
set state = 'posted',
|
|
540
|
+
number = coalesce(number, v_entry.number),
|
|
541
|
+
entry_id = v_entry.id,
|
|
542
|
+
accounting_date = v_date
|
|
543
|
+
where id = p_document_id;
|
|
544
|
+
|
|
545
|
+
return v_entry;
|
|
546
|
+
end;
|
|
547
|
+
$$;
|
|
548
|
+
|
|
549
|
+
-- The transfer that makes a cash-basis tax due. Both legs carry the posting
|
|
550
|
+
-- that falls due: the one that lands on the account and the box it is declared
|
|
551
|
+
-- on, and the one that empties the transition account it waited on.
|
|
552
|
+
|
|
553
|
+
create or replace function settle_cash_basis_tax(
|
|
554
|
+
p_document_id uuid,
|
|
555
|
+
p_date date
|
|
556
|
+
)
|
|
557
|
+
returns uuid
|
|
558
|
+
language plpgsql
|
|
559
|
+
as $$
|
|
560
|
+
declare
|
|
561
|
+
v_doc documents%rowtype;
|
|
562
|
+
v_company companies%rowtype;
|
|
563
|
+
v_entry entries%rowtype;
|
|
564
|
+
v_kind tax_document_kind;
|
|
565
|
+
v_total numeric;
|
|
566
|
+
v_paid numeric;
|
|
567
|
+
v_ratio numeric;
|
|
568
|
+
v_seq integer := 0;
|
|
569
|
+
v_final uuid;
|
|
570
|
+
v_box text;
|
|
571
|
+
v_target uuid;
|
|
572
|
+
v_due numeric;
|
|
573
|
+
v_done numeric;
|
|
574
|
+
v_delta numeric;
|
|
575
|
+
v_box_due numeric;
|
|
576
|
+
v_box_done numeric;
|
|
577
|
+
v_box_delta numeric;
|
|
578
|
+
-- The transfer is written in the ledger's currency on both sides, which is
|
|
579
|
+
-- the company's own: the entry it produces says so.
|
|
580
|
+
v_round money_rounding;
|
|
581
|
+
v_credit boolean;
|
|
582
|
+
v_type tax_posting_type;
|
|
583
|
+
w record;
|
|
584
|
+
begin
|
|
585
|
+
select * into v_doc from documents where id = p_document_id;
|
|
586
|
+
if not found or v_doc.entry_id is null then
|
|
587
|
+
return null;
|
|
588
|
+
end if;
|
|
589
|
+
|
|
590
|
+
-- Nothing is waiting on this document: the answer for every tax that falls
|
|
591
|
+
-- due when it is invoiced, which is most of them.
|
|
592
|
+
if not exists (
|
|
593
|
+
select 1
|
|
594
|
+
from entry_lines q join taxes t on t.id = q.tax_id
|
|
595
|
+
where q.entry_id = v_doc.entry_id
|
|
596
|
+
and t.cash_basis
|
|
597
|
+
and q.declaration_box is null
|
|
598
|
+
and q.box_amount is not null
|
|
599
|
+
) then
|
|
600
|
+
return null;
|
|
601
|
+
end if;
|
|
602
|
+
|
|
603
|
+
select * into v_company from companies where id = v_doc.company_id;
|
|
604
|
+
v_round := rounding_of(v_doc.company_id);
|
|
605
|
+
if v_company.miscellaneous_journal_id is null then
|
|
606
|
+
raise exception 'no_miscellaneous_journal: company % has no journal for the transfer of a cash-basis tax',
|
|
607
|
+
v_doc.company_id;
|
|
608
|
+
end if;
|
|
609
|
+
|
|
610
|
+
v_kind := case when v_doc.doc_type in ('sale_credit_note', 'purchase_credit_note')
|
|
611
|
+
then 'credit_note' else 'invoice' end::tax_document_kind;
|
|
612
|
+
|
|
613
|
+
-- What share of this document has been settled, read on its own third-party
|
|
614
|
+
-- lines and in the ledger's currency on both sides of the division.
|
|
615
|
+
select coalesce(sum(abs(tl.debit - tl.credit)), 0), coalesce(sum(tl.matched_amount), 0)
|
|
616
|
+
into v_total, v_paid
|
|
617
|
+
from entry_lines tl join accounts a on a.id = tl.account_id
|
|
618
|
+
where tl.entry_id = v_doc.entry_id
|
|
619
|
+
and a.reconcilable
|
|
620
|
+
and a.account_type in ('asset_receivable', 'liability_payable');
|
|
621
|
+
|
|
622
|
+
if v_total = 0 then
|
|
623
|
+
return null;
|
|
624
|
+
end if;
|
|
625
|
+
-- An overpayment settles the document, and no more: a tax is due on what
|
|
626
|
+
-- was invoiced.
|
|
627
|
+
v_ratio := least(1, greatest(0, v_paid / v_total));
|
|
628
|
+
|
|
629
|
+
for w in
|
|
630
|
+
select l2.id, l2.account_id, l2.tax_id, l2.tax_line, l2.box_amount,
|
|
631
|
+
l2.debit, l2.credit, l2.name, l2.sequence
|
|
632
|
+
from entry_lines l2 join taxes t on t.id = l2.tax_id
|
|
633
|
+
where l2.entry_id = v_doc.entry_id
|
|
634
|
+
and t.cash_basis
|
|
635
|
+
and l2.declaration_box is null
|
|
636
|
+
and l2.box_amount is not null
|
|
637
|
+
order by l2.sequence
|
|
638
|
+
loop
|
|
639
|
+
select tp.account_id, tp.declaration_box, tp.posting_type
|
|
640
|
+
into v_final, v_box, v_type
|
|
641
|
+
from tax_postings tp
|
|
642
|
+
where tp.tax_id = w.tax_id
|
|
643
|
+
and tp.document_kind = v_kind
|
|
644
|
+
and tp.posting_type = (case when w.tax_line then 'tax' else 'base' end)::tax_posting_type
|
|
645
|
+
limit 1;
|
|
646
|
+
|
|
647
|
+
-- The account the transfer of this line lands on, which is also how an
|
|
648
|
+
-- earlier transfer of the same line is recognised.
|
|
649
|
+
v_target := case when w.tax_line then v_final else w.account_id end;
|
|
650
|
+
if v_target is null then
|
|
651
|
+
raise exception 'no_cash_basis_target: the tax of line % names no account to fall due on', w.id;
|
|
652
|
+
end if;
|
|
653
|
+
-- A line only waits because a box was worked out for it, and that box came
|
|
654
|
+
-- from this very posting. If it has none, the amount would wait for ever.
|
|
655
|
+
if v_box is null then
|
|
656
|
+
raise exception 'no_cash_basis_box: the tax of line % holds an amount for a box the posting does not name', w.id;
|
|
657
|
+
end if;
|
|
658
|
+
|
|
659
|
+
if w.tax_line then
|
|
660
|
+
v_due := round_amount((w.debit + w.credit) * v_ratio, v_round);
|
|
661
|
+
select coalesce(sum(case when w.credit > 0 then x.credit - x.debit
|
|
662
|
+
else x.debit - x.credit end), 0)
|
|
663
|
+
into v_done
|
|
664
|
+
from entry_lines x join entries e on e.id = x.entry_id
|
|
665
|
+
where e.document_id = p_document_id
|
|
666
|
+
and e.id <> v_doc.entry_id
|
|
667
|
+
and x.tax_id = w.tax_id
|
|
668
|
+
and x.tax_line
|
|
669
|
+
and x.account_id = v_target;
|
|
670
|
+
else
|
|
671
|
+
v_due := 0;
|
|
672
|
+
v_done := 0;
|
|
673
|
+
end if;
|
|
674
|
+
v_delta := v_due - v_done;
|
|
675
|
+
|
|
676
|
+
v_box_due := round_amount(w.box_amount * v_ratio, v_round);
|
|
677
|
+
select coalesce(sum(x.box_amount), 0)
|
|
678
|
+
into v_box_done
|
|
679
|
+
from entry_lines x join entries e on e.id = x.entry_id
|
|
680
|
+
where e.document_id = p_document_id
|
|
681
|
+
and e.id <> v_doc.entry_id
|
|
682
|
+
and x.tax_id = w.tax_id
|
|
683
|
+
and x.tax_line = w.tax_line
|
|
684
|
+
and x.account_id = v_target;
|
|
685
|
+
v_box_delta := v_box_due - v_box_done;
|
|
686
|
+
|
|
687
|
+
if v_delta = 0 and v_box_delta = 0 then
|
|
688
|
+
continue;
|
|
689
|
+
end if;
|
|
690
|
+
|
|
691
|
+
if v_entry.id is null then
|
|
692
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
|
|
693
|
+
description, state, document_id, currency_code)
|
|
694
|
+
values (v_doc.company_id, v_company.miscellaneous_journal_id,
|
|
695
|
+
fiscal_year_at(v_doc.company_id, p_date), p_date,
|
|
696
|
+
v_doc.number,
|
|
697
|
+
coalesce(v_doc.number, 'document') || ' — tax due on settlement',
|
|
698
|
+
'draft', p_document_id, v_company.currency_code)
|
|
699
|
+
returning * into v_entry;
|
|
700
|
+
end if;
|
|
701
|
+
|
|
702
|
+
-- The side the document put the tax on, kept when the share grows and
|
|
703
|
+
-- flipped when it shrinks.
|
|
704
|
+
v_credit := (w.credit > 0) = (v_delta > 0);
|
|
705
|
+
|
|
706
|
+
v_seq := v_seq + 10;
|
|
707
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
708
|
+
debit, credit, tax_id, tax_line, posting_type,
|
|
709
|
+
declaration_box, box_amount, currency_code)
|
|
710
|
+
values (v_entry.id, v_doc.company_id, v_target, v_seq, w.name,
|
|
711
|
+
case when v_credit then 0 else abs(v_delta) end,
|
|
712
|
+
case when v_credit then abs(v_delta) else 0 end,
|
|
713
|
+
w.tax_id, w.tax_line, v_type,
|
|
714
|
+
v_box, v_box_delta,
|
|
715
|
+
v_company.currency_code);
|
|
716
|
+
|
|
717
|
+
if v_delta <> 0 then
|
|
718
|
+
v_seq := v_seq + 10;
|
|
719
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
720
|
+
debit, credit, tax_id, tax_line, posting_type, currency_code)
|
|
721
|
+
values (v_entry.id, v_doc.company_id, w.account_id, v_seq, w.name,
|
|
722
|
+
case when v_credit then abs(v_delta) else 0 end,
|
|
723
|
+
case when v_credit then 0 else abs(v_delta) end,
|
|
724
|
+
w.tax_id, true, v_type, v_company.currency_code);
|
|
725
|
+
end if;
|
|
726
|
+
end loop;
|
|
727
|
+
|
|
728
|
+
if v_entry.id is null then
|
|
729
|
+
return null;
|
|
730
|
+
end if;
|
|
731
|
+
|
|
732
|
+
v_entry := post_entry(v_entry.id);
|
|
733
|
+
return v_entry.id;
|
|
734
|
+
end;
|
|
735
|
+
$$;
|
|
736
|
+
|
|
737
|
+
-- ---------------------------------------------------------------------------
|
|
738
|
+
-- The reader that asked for the column
|
|
739
|
+
-- ---------------------------------------------------------------------------
|
|
740
|
+
--
|
|
741
|
+
-- `ec_sales_list()` took every line of an intra-Community tax that is not a tax
|
|
742
|
+
-- line, and a `tax_on_base` line is one of those. It is exact on every pack of
|
|
743
|
+
-- this repository — an exempt supply has no tax to capitalise — and it was
|
|
744
|
+
-- exact by luck. Now it says what it means, and it says it in a way that is
|
|
745
|
+
-- also right about a ledger written before the column existed: a line is
|
|
746
|
+
-- excluded when it is **known** to be a `tax_on_base` line, and a null keeps
|
|
747
|
+
-- the reading it had, which is the one the backfill above refused to improve
|
|
748
|
+
-- on. The rest of the function is `20260916094500` unchanged, period guard
|
|
749
|
+
-- included.
|
|
750
|
+
|
|
751
|
+
create or replace function ec_sales_list(
|
|
752
|
+
p_company_id uuid,
|
|
753
|
+
p_from date,
|
|
754
|
+
p_to date,
|
|
755
|
+
p_report_code text default null
|
|
756
|
+
)
|
|
757
|
+
returns table (
|
|
758
|
+
vat_country char(2),
|
|
759
|
+
vat_number text,
|
|
760
|
+
nature text,
|
|
761
|
+
amount numeric,
|
|
762
|
+
currency_code char(3),
|
|
763
|
+
documents integer,
|
|
764
|
+
contact_ids uuid[],
|
|
765
|
+
contact_names text[],
|
|
766
|
+
issue text
|
|
767
|
+
)
|
|
768
|
+
language plpgsql
|
|
769
|
+
stable
|
|
770
|
+
as $$
|
|
771
|
+
declare
|
|
772
|
+
v_country char(2);
|
|
773
|
+
v_prefix char(2);
|
|
774
|
+
v_currency char(3);
|
|
775
|
+
v_round money_rounding;
|
|
776
|
+
v_files declaration_period;
|
|
777
|
+
v_accepts declaration_period[];
|
|
778
|
+
v_asked declaration_period;
|
|
779
|
+
begin
|
|
780
|
+
select c.fiscal_country, c.currency_code into v_country, v_currency
|
|
781
|
+
from companies c where c.id = p_company_id;
|
|
782
|
+
if not found then
|
|
783
|
+
raise exception 'unknown_company: %', p_company_id;
|
|
784
|
+
end if;
|
|
785
|
+
if not exists (select 1 from territories) then
|
|
786
|
+
raise exception 'no_territories: apply supabase/seed/00_territories.sql';
|
|
787
|
+
end if;
|
|
788
|
+
|
|
789
|
+
-- Which period this is, when the caller said which statement they are
|
|
790
|
+
-- filing. Five things have to hold: a form was named, it exists, the company
|
|
791
|
+
-- has recorded a cadence for it, the dates are themselves a whole cadence the
|
|
792
|
+
-- form is filed on, and the two differ. A monthly statement from a quarterly
|
|
793
|
+
-- filer is the ordinary case in three of the four countries read while this
|
|
794
|
+
-- was written, and nothing here touches it: the cadence read is the
|
|
795
|
+
-- statement's own.
|
|
796
|
+
if p_report_code is not null then
|
|
797
|
+
select t.periods into v_accepts
|
|
798
|
+
from tax_report_templates t
|
|
799
|
+
where t.code = p_report_code
|
|
800
|
+
and t.valid_from <= p_to
|
|
801
|
+
and (t.valid_to is null or t.valid_to >= p_to)
|
|
802
|
+
order by t.country
|
|
803
|
+
limit 1;
|
|
804
|
+
if v_accepts is null then
|
|
805
|
+
raise exception 'unknown_tax_report: % is not a declaration form in force on %',
|
|
806
|
+
p_report_code, p_to;
|
|
807
|
+
end if;
|
|
808
|
+
v_files := filing_period(p_company_id, p_report_code);
|
|
809
|
+
v_asked := declaration_period_of(p_from, p_to);
|
|
810
|
+
if v_files is not null
|
|
811
|
+
and v_asked is not null
|
|
812
|
+
and v_files = any(v_accepts)
|
|
813
|
+
and v_asked = any(v_accepts)
|
|
814
|
+
and v_asked <> v_files then
|
|
815
|
+
raise exception
|
|
816
|
+
'wrong_declaration_period: this company files % on %; % to % is a %',
|
|
817
|
+
v_files, p_report_code, p_from, p_to, v_asked;
|
|
818
|
+
end if;
|
|
819
|
+
end if;
|
|
820
|
+
|
|
821
|
+
v_prefix := vat_prefix_of(v_country);
|
|
822
|
+
v_round := rounding_of(p_company_id);
|
|
823
|
+
|
|
824
|
+
return query
|
|
825
|
+
with supply as (
|
|
826
|
+
select d.contact_id,
|
|
827
|
+
ct.name as contact_name,
|
|
828
|
+
regexp_replace(t.treatment::text, '^intracom_', '') as nature,
|
|
829
|
+
upper(regexp_replace(coalesce(ct.vat_number, ''), '[^A-Za-z0-9]', '', 'g')) as vat_raw,
|
|
830
|
+
ct.country as contact_country,
|
|
831
|
+
e.entry_date,
|
|
832
|
+
e.document_id,
|
|
833
|
+
l.credit - l.debit as amount
|
|
834
|
+
from entry_lines l
|
|
835
|
+
join entries e on e.id = l.entry_id
|
|
836
|
+
join taxes t on t.id = l.tax_id
|
|
837
|
+
left join documents d on d.id = e.document_id
|
|
838
|
+
left join contacts ct on ct.id = d.contact_id
|
|
839
|
+
where l.company_id = p_company_id
|
|
840
|
+
and e.state = 'posted'
|
|
841
|
+
and e.entry_date between p_from and p_to
|
|
842
|
+
and not l.tax_line
|
|
843
|
+
-- A line known to have been written by a `tax_on_base` posting is a
|
|
844
|
+
-- cost and not a supply. Null is left in, because a ledger written
|
|
845
|
+
-- before `posting_type` existed reads exactly as it did.
|
|
846
|
+
and l.posting_type is distinct from 'tax_on_base'::tax_posting_type
|
|
847
|
+
and t.treatment::text like 'intracom!_%' escape '!'
|
|
848
|
+
and t.treatment::text not like 'intracom!_acquisition!_%' escape '!'
|
|
849
|
+
),
|
|
850
|
+
keyed as (
|
|
851
|
+
select s.*,
|
|
852
|
+
vat_prefix_of(
|
|
853
|
+
case when s.vat_raw ~ '^[A-Z]{2}' then substring(s.vat_raw from 1 for 2)
|
|
854
|
+
when s.vat_raw = '' then null
|
|
855
|
+
else s.contact_country
|
|
856
|
+
end
|
|
857
|
+
) as vat_country,
|
|
858
|
+
case when s.vat_raw = '' then null
|
|
859
|
+
when s.vat_raw ~ '^[A-Z]{2}' then substring(s.vat_raw from 3)
|
|
860
|
+
else s.vat_raw
|
|
861
|
+
end as vat_number
|
|
862
|
+
from supply s
|
|
863
|
+
),
|
|
864
|
+
scoped as (
|
|
865
|
+
select k.*, eu_vat_scope_of(k.vat_country, k.entry_date) as scope
|
|
866
|
+
from keyed k
|
|
867
|
+
),
|
|
868
|
+
judged as (
|
|
869
|
+
select s.*,
|
|
870
|
+
case
|
|
871
|
+
when s.contact_id is null then 'no_customer'
|
|
872
|
+
when s.vat_number is null or s.vat_number = '' then 'no_vat_number'
|
|
873
|
+
when s.vat_country is null then 'no_vat_country'
|
|
874
|
+
when s.vat_country = v_prefix then 'vat_country_is_the_company_country'
|
|
875
|
+
when s.scope = 'none' then 'vat_country_outside_the_union'
|
|
876
|
+
when s.scope <> 'full' and s.nature = 'services'
|
|
877
|
+
then 'vat_country_outside_the_union_for_this_supply'
|
|
878
|
+
end as issue
|
|
879
|
+
from scoped s
|
|
880
|
+
)
|
|
881
|
+
select j.vat_country::char(2),
|
|
882
|
+
j.vat_number,
|
|
883
|
+
j.nature,
|
|
884
|
+
round_amount(sum(j.amount), v_round),
|
|
885
|
+
v_currency,
|
|
886
|
+
count(distinct j.document_id)::integer,
|
|
887
|
+
array_agg(distinct j.contact_id) filter (where j.contact_id is not null),
|
|
888
|
+
array_agg(distinct j.contact_name) filter (where j.contact_name is not null),
|
|
889
|
+
j.issue
|
|
890
|
+
from judged j
|
|
891
|
+
group by j.vat_country, j.vat_number, j.nature, j.issue,
|
|
892
|
+
case when j.issue is not null then j.contact_id end
|
|
893
|
+
having round_amount(sum(j.amount), v_round) <> 0
|
|
894
|
+
order by (j.issue is not null), j.vat_country, j.vat_number, j.nature;
|
|
895
|
+
end;
|
|
896
|
+
$$;
|
|
897
|
+
|
|
898
|
+
comment on function ec_sales_list(uuid, date, date, text) is
|
|
899
|
+
'The recapitulative statement of intra-Community supplies for a period: one line per customer VAT identification number and per nature — goods, services, and whatever the treatment vocabulary gains next — summed from the posted ledger in the company''s currency, credit notes deducted. The country of a line is the prefix the customer''s numbers carry, read from `territories`, so a Greek customer is listed under EL. A supply that cannot be declared comes back with the reason in `issue` rather than being left out. Name the form in p_report_code to have the period checked against the cadence this company files **that statement** on, which is not the cadence of its periodic return in any country read so far; name none and nothing is refused. A supply is a base line of the tax: a line a `tax_on_base` posting wrote is a cost and is left out. No country rule lives in this function.';
|
|
900
|
+
|
|
901
|
+
revoke execute on function ec_sales_list(uuid, date, date, text) from public, anon;
|
|
902
|
+
grant execute on function ec_sales_list(uuid, date, date, text) to authenticated, service_role;
|