diff --git a/nexa_coa_setup/data/04_account_fiscal_position.xml b/nexa_coa_setup/data/04_account_fiscal_position.xml index 7faba267..c74bc805 100644 --- a/nexa_coa_setup/data/04_account_fiscal_position.xml +++ b/nexa_coa_setup/data/04_account_fiscal_position.xml @@ -1,5 +1,64 @@ + + + + + CA — Ontario (Default) + + + + + + + CA — Atlantic (HST 15%) + + + + + + + CA — Quebec (GST + QST) + + + + + + + CA — British Columbia (GST 5%, PST per-product) + + + + + + + CA — Prairies / Territories (GST 5% only) + + + + + + + Export — United States (Zero-rated) + + + + + + Export — International (Zero-rated) + + Manually apply for non-CA / non-US customers. Auto-apply by country-group requires a custom rule. + + + + Tax Exempt (cert-holder) + + Apply manually to customers with a valid exemption certificate on file. Record certificate details in the partner notes. + + diff --git a/nexa_coa_setup/hooks.py b/nexa_coa_setup/hooks.py index 59fbcd13..3ddad7bc 100644 --- a/nexa_coa_setup/hooks.py +++ b/nexa_coa_setup/hooks.py @@ -80,10 +80,89 @@ def post_init_hook(env): _archive_unused_l10n_ca_accounts(env) _rename_legacy_accounts(env) _archive_unused_taxes(env) + _configure_fiscal_position_tax_maps(env) _lock_fiscal_year_2025(env) _logger.info("nexa_coa_setup: post_init_hook complete") +def _configure_fiscal_position_tax_maps(env): + """For each Nexa fiscal position, set up tax substitution from default + '5% GST' to the appropriate provincial tax. + + Odoo 19 fiscal position model: + - account.fiscal.position has tax_ids (M2M) — destination taxes + - account.tax has original_tax_ids (M2M) — source taxes it replaces + - Effective map is computed on the fly from both sides. + + Idempotent: writes are 'replace' style (6, 0, [ids]) so re-running cleans + up prior runs. + """ + def find_tax(name, use): + return env["account.tax"].search( + [("name", "=", name), ("type_tax_use", "=", use), ("active", "=", True)], + limit=1, + ) + + GST_5_sale = find_tax("5% GST", "sale") + GST_5_purchase = find_tax("5% GST", "purchase") + HST_13_sale = find_tax("13% HST", "sale") + HST_13_purchase = find_tax("13% HST", "purchase") + HST_15_sale = find_tax("15% HST", "sale") + HST_15_purchase = find_tax("15% HST", "purchase") + QST_sale = find_tax("14.975% GST+QST", "sale") + QST_purchase = find_tax("14.975% GST+QST", "purchase") + ZERO_sale = find_tax("0% GST", "sale") + ZERO_purchase = find_tax("0% GST", "purchase") + + # (fp_xmlid, [(source_tax, destination_tax), ...]) + fp_map = [ + ("nexa_coa_setup.fp_ca_ontario", [ + (GST_5_sale, HST_13_sale), + (GST_5_purchase, HST_13_purchase), + ]), + ("nexa_coa_setup.fp_ca_atlantic", [ + (GST_5_sale, HST_15_sale), + (GST_5_purchase, HST_15_purchase), + ]), + ("nexa_coa_setup.fp_ca_quebec", [ + (GST_5_sale, QST_sale), + (GST_5_purchase, QST_purchase), + ]), + # CA-BC and CA-Prairies/Territories: default is already 5% GST, no substitution + ("nexa_coa_setup.fp_ca_bc", []), + ("nexa_coa_setup.fp_ca_prairies_territories", []), + ("nexa_coa_setup.fp_export_us", [ + (GST_5_sale, ZERO_sale), + ]), + ("nexa_coa_setup.fp_export_intl", [ + (GST_5_sale, ZERO_sale), + ]), + ("nexa_coa_setup.fp_tax_exempt", [ + (GST_5_sale, ZERO_sale), + ]), + ] + + configured = 0 + for fp_xmlid, pairs in fp_map: + fp = env.ref(fp_xmlid, raise_if_not_found=False) + if not fp: + _logger.warning("nexa_coa_setup: fiscal position not found: %s", fp_xmlid) + continue + # Clear existing destination taxes on the FP + fp.tax_ids = [(5, 0, 0)] + # For each (src, dst) pair, add dst to FP's tax_ids and src to dst's + # original_tax_ids + for src, dst in pairs: + if not src or not dst or src == dst: + continue + fp.tax_ids = [(4, dst.id, 0)] + existing_sources = dst.original_tax_ids.ids + if src.id not in existing_sources: + dst.original_tax_ids = [(4, src.id, 0)] + configured += 1 + _logger.info("nexa_coa_setup: configured tax maps on %d fiscal positions", configured) + + # Tax names to keep ACTIVE (covers GST/HST/QST/PST across provinces + zero-rated # export + exempt). Everything else gets archived if it has zero usage on # existing journal entries.