Product Icon

WooCommerce

Sell online with the flexible, customizable ecommerce platform designed to grow with your business. From your first sale to millions in revenue, Woo is with you. See why merchants trust us to power 4+ million online stores.

Allow tax_status to be set independently per product variation

I sell products where some variations are physical versions requiring sales tax and some are digital versions requiring no sales tax.

Summary

WooCommerce supports per-variation tax_class but hardcodes tax_status to always inherit from the parent variable product. There is no way to mark one variation as taxable and another as non-taxable within the same product. The fix
requires the same pattern already used for tax_class.


Use Case

A store sells a product with both a physical and a digital variation (e.g. printed manual + downloadable PDF, physical event ticket + digital streaming pass, hardware part + software licence). Physical goods are taxable; digital goods are not. Because they share a parent variable product, both inherit the parent’s tax_status — there is no way to exempt the digital variation from tax without splitting them into entirely separate products.


Root Cause — Exact Code Location

File: includes/data-stores/class-wc-product-variation-data-store-cpt.php

The variation data store reads _tax_class from the variation’s own postmeta (it is in $meta_key_to_props, line ~392):

// Line ~392 — variation’s OWN postmeta is read:
‘_tax_class’ => ‘tax_class’,

But _tax_status is only in $parent_meta_key_to_props (~line 459) and then explicitly forced onto the variation from the parent at line ~481:

// Lines ~478-481 — comment is WooCommerce’s own:
// Pull data from the parent when there is no user-facing way to set props.
$product->set_sold_individually( $parent_data[‘sold_individually’] );
$product->set_tax_status( $parent_data[‘tax_status’] ); // ← always parent’s value

This call to set_tax_status() overwrites anything previously set on the variation object, making it impossible to
store or read a variation-level tax status.

File: includes/class-wc-product-variation.php

get_tax_class() (~line 299) has a full per-variation override with parent fallback:

// Line ~299 — tax_class correctly falls back to parent:
public function get_tax_class( $context = ‘view’ ) {

if ( array_key_exists( ‘tax_class’, $this->data ) ) {
$value = …
if ( ‘parent’ === $value ) {
$value = $this->parent_data[‘tax_class’]; // inherit when set to ‘parent’
}
}
}

There is no equivalent get_tax_status() override in this class. tax_status is not in $this->data at all (line ~44 —
only tax_class is declared), so the value is always whatever the data store last set via set_tax_status() — which is always the parent’s.


Requested Change

Apply the exact same pattern used for tax_class to tax_status:

1. class-wc-product-variation-data-store-cpt.php

Move _tax_status from $parent_meta_key_to_props into $meta_key_to_props so it is read from the variation’s own
postmeta. Add a sentinel fallback identical to the one for tax_class at line ~405:

// After reading variation meta:
$variation_data[‘tax_status’] = ! metadata_exists( ‘post’, $id, ‘_tax_status’ )
? ‘parent’ // new sentinel: inherit from parent when not explicitly set
: $variation_data[‘tax_status’];

Remove line ~481 ($product->set_tax_status( $parent_data[‘tax_status’] )) or guard it with the sentinel check.

2. class-wc-product-variation.php

Add tax_status to $this->data (line ~44) defaulting to ‘parent’ (matching tax_class’s default), and add a
get_tax_status() override mirroring the existing get_tax_class():

public function get_tax_status( $context = ‘view’ ) {
$value = array_key_exists( ‘tax_status’, $this->changes )
? $this->changes[‘tax_status’]
: $this->data[‘tax_status’];

if ( ‘parent’ === $value || ” === $value ) {
$value = $this->parent_data[‘tax_status’];
}

return apply_filters( $this->get_hook_prefix() . ‘tax_status’, $value, $this );
}

3. Admin UI

Add a Tax Status dropdown to the variation edit panel (the same panel that already shows the Tax Class dropdown),
defaulting to — Same as parent —. No variation-level UI currently exists for this field.


Why This Is Consistent With Existing Behaviour

tax_class and tax_status are conceptually paired — both control how tax is applied. WooCommerce already decided that tax_class should be overridable per variation. The only reason tax_status is not is the comment in the data store:
“when there is no user-facing way to set props” — which is a UI gap, not a design principle. The plumbing change is
minimal and follows an already-established pattern in the same file.


What Does Not Work As A Workaround

Hooking woocommerce_product_get_tax_status to return variation-level meta works at the PHP level but conflicts with tax calculation extensions that bypass the filter or cache the value, and is not a pattern WooCommerce documents or supports. It is a hack that should not be necessary.

Author

dataforge

Current Status

Open

Last updated: May 8, 2026

0 comments

Log in to comment on this feature request.

Use of your personal data
We and our partners process your personal data (such as browsing data, IP Addresses, cookie information, and other unique identifiers) based on your consent and/or our legitimate interest to optimize our website, marketing activities, and your user experience.