feat(nexa_coa_setup): 8 fiscal positions + tax substitution maps
XML defines 8 positions with auto-detection by country/state: - CA Ontario (default), CA Atlantic, CA Quebec, CA BC, CA Prairies/Territories - Export US, Export International, Tax Exempt post_init hook _configure_fiscal_position_tax_maps sets up bidirectional tax routing (sale + purchase) from the default '5% GST' to the appropriate provincial tax via Odoo 19's account.fiscal.position.tax_ids / account.tax.original_tax_ids relation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,64 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<data noupdate="0">
|
<data noupdate="0">
|
||||||
|
|
||||||
|
<!-- Tax substitutions on each fiscal position are configured by the
|
||||||
|
post_init_hook (_configure_fiscal_position_tax_maps) because the
|
||||||
|
source/destination tax IDs are resolved at runtime from the
|
||||||
|
curated active tax set. -->
|
||||||
|
|
||||||
|
<record id="fp_ca_ontario" model="account.fiscal.position">
|
||||||
|
<field name="name">CA — Ontario (Default)</field>
|
||||||
|
<field name="auto_apply" eval="True"/>
|
||||||
|
<field name="country_id" ref="base.ca"/>
|
||||||
|
<field name="state_ids" eval="[(6, 0, [ref('base.state_ca_on')])]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="fp_ca_atlantic" model="account.fiscal.position">
|
||||||
|
<field name="name">CA — Atlantic (HST 15%)</field>
|
||||||
|
<field name="auto_apply" eval="True"/>
|
||||||
|
<field name="country_id" ref="base.ca"/>
|
||||||
|
<field name="state_ids" eval="[(6, 0, [ref('base.state_ca_nb'), ref('base.state_ca_ns'), ref('base.state_ca_pe'), ref('base.state_ca_nl')])]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="fp_ca_quebec" model="account.fiscal.position">
|
||||||
|
<field name="name">CA — Quebec (GST + QST)</field>
|
||||||
|
<field name="auto_apply" eval="True"/>
|
||||||
|
<field name="country_id" ref="base.ca"/>
|
||||||
|
<field name="state_ids" eval="[(6, 0, [ref('base.state_ca_qc')])]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="fp_ca_bc" model="account.fiscal.position">
|
||||||
|
<field name="name">CA — British Columbia (GST 5%, PST per-product)</field>
|
||||||
|
<field name="auto_apply" eval="True"/>
|
||||||
|
<field name="country_id" ref="base.ca"/>
|
||||||
|
<field name="state_ids" eval="[(6, 0, [ref('base.state_ca_bc')])]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="fp_ca_prairies_territories" model="account.fiscal.position">
|
||||||
|
<field name="name">CA — Prairies / Territories (GST 5% only)</field>
|
||||||
|
<field name="auto_apply" eval="True"/>
|
||||||
|
<field name="country_id" ref="base.ca"/>
|
||||||
|
<field name="state_ids" eval="[(6, 0, [ref('base.state_ca_ab'), ref('base.state_ca_mb'), ref('base.state_ca_sk'), ref('base.state_ca_yt'), ref('base.state_ca_nt'), ref('base.state_ca_nu')])]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="fp_export_us" model="account.fiscal.position">
|
||||||
|
<field name="name">Export — United States (Zero-rated)</field>
|
||||||
|
<field name="auto_apply" eval="True"/>
|
||||||
|
<field name="country_id" ref="base.us"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="fp_export_intl" model="account.fiscal.position">
|
||||||
|
<field name="name">Export — International (Zero-rated)</field>
|
||||||
|
<field name="auto_apply" eval="False"/>
|
||||||
|
<field name="note">Manually apply for non-CA / non-US customers. Auto-apply by country-group requires a custom rule.</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="fp_tax_exempt" model="account.fiscal.position">
|
||||||
|
<field name="name">Tax Exempt (cert-holder)</field>
|
||||||
|
<field name="auto_apply" eval="False"/>
|
||||||
|
<field name="note">Apply manually to customers with a valid exemption certificate on file. Record certificate details in the partner notes.</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -80,10 +80,89 @@ def post_init_hook(env):
|
|||||||
_archive_unused_l10n_ca_accounts(env)
|
_archive_unused_l10n_ca_accounts(env)
|
||||||
_rename_legacy_accounts(env)
|
_rename_legacy_accounts(env)
|
||||||
_archive_unused_taxes(env)
|
_archive_unused_taxes(env)
|
||||||
|
_configure_fiscal_position_tax_maps(env)
|
||||||
_lock_fiscal_year_2025(env)
|
_lock_fiscal_year_2025(env)
|
||||||
_logger.info("nexa_coa_setup: post_init_hook complete")
|
_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
|
# 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
|
# export + exempt). Everything else gets archived if it has zero usage on
|
||||||
# existing journal entries.
|
# existing journal entries.
|
||||||
|
|||||||
Reference in New Issue
Block a user