DEV Community

WebKoding
WebKoding

Posted on

WooCommerce Only Allows One Tax Rate Per Line Item — Here's How I Solved It

WooCommerce has a limitation that quietly causes legal compliance issues for thousands of stores across Germany, Austria, France, and other EU countries: you can only assign one tax class per line item.

For most stores this is fine. But if you sell brunch tickets, hotel packages, event passes, or any product that legally requires multiple VAT rates on a single invoice — you have a problem.

The Real-World Problem

Take a brunch ticket priced at €39. In Germany, food is taxed at 7% and beverages at 19%. Since January 1, 2026, the German BMF (Bundesministerium der Finanzen) requires that invoices for such products show the split explicitly:

Item Net VAT Gross
Food portion (80%) €29.16 7% → €2.04 €31.20
Drinks portion (20%) €6.55 19% → €1.24 €7.80
Total €35.71 €3.28 €39.00

Without the split, your invoice is legally non-compliant — the customer overpays VAT by €2.94, and your tax report is wrong. Similar requirements exist in Austria, Switzerland, and France.

Why WooCommerce Can't Do This Natively

WooCommerce's order data model stores tax per line item as a single tax class. There is no native concept of sub-items within a line item — you can't attach two different tax rates to one product without fundamentally changing how the order is stored.

The Approach: Virtual Split Parts in Order Meta

The solution is to store split rules on the product, then at order creation inject virtual line items into order metadata — one per split part — while keeping the customer-facing display as a single product.

Step 1: Product-Level Split Rules

// Stored as product meta: _splitvat_rules
$rules = [
    [
        'label'      => 'Food portion',
        'percentage' => 80,
        'tax_class'  => 'reduced-rate', // 7% in Germany
    ],
    [
        'label'      => 'Drinks portion',
        'percentage' => 20,
        'tax_class'  => 'standard',     // 19% in Germany
    ],
];
Enter fullscreen mode Exit fullscreen mode

Step 2: Hooking Into Order Creation

add_action(
    'woocommerce_checkout_create_order_line_item',
    function( $item, $cart_item_key, $values, $order ) {
        $product = $item->get_product();
        $rules   = get_post_meta( $product->get_id(), '_splitvat_rules', true );

        if ( empty( $rules ) ) {
            return;
        }

        $split_parts = splitvat_calculate_parts( $item->get_total(), $rules );
        $item->add_meta_data( '_splitvat_parts', $split_parts, true );
        $item->add_meta_data( '_splitvat_enabled', true, true );
    },
    10, 4
);
Enter fullscreen mode Exit fullscreen mode

Step 3: Penny-Perfect Rounding

Splitting €39 into 80/20 is clean, but many totals produce floating point rounding errors. The "remainder on last" algorithm fixes this:

function splitvat_calculate_parts( float $total, array $rules ): array {
    $parts      = [];
    $allocated  = 0.0;
    $last_index = count( $rules ) - 1;

    foreach ( $rules as $index => $rule ) {
        if ( $index === $last_index ) {
            $amount = round( $total - $allocated, 2 ); // remainder — no drift
        } else {
            $amount    = round( $total * ( $rule['percentage'] / 100 ), 2 );
            $allocated += $amount;
        }
        $parts[] = array_merge( $rule, [ 'amount' => $amount ] );
    }

    return $parts;
}
Enter fullscreen mode Exit fullscreen mode

This guarantees split parts always sum to exactly the original total.

Step 4: HPOS Compatibility

If you're on WooCommerce High-Performance Order Storage, always use the order abstraction layer — not direct SQL:

// Always use this
$orders = wc_get_orders([
    'status'     => ['completed', 'processing'],
    'meta_query' => [[
        'key'   => '_splitvat_enabled',
        'value' => true,
    ]]
]);
Enter fullscreen mode Exit fullscreen mode

Accounting Export Formats

Different countries need different export formats:

  • DATEV Buchungsstapel — Germany (direct import into tax advisor software)
  • BMD/RZL — Austria (Steuercode + Kontenrahmen mapping)
  • FEC — France (18 mandatory fields, required by tax authorities)
  • CSV — Universal (Excel, Google Sheets, any tool)

The Key Insight

Orders are immutable records. Do the heavy lifting at checkout, store the computed split in order item meta, and read it back whenever you need it (invoices, exports, reports). Trying to recalculate after the fact is unreliable — prices and tax rates can change.


I packaged all of this into SplitVAT — 13 country presets, 4 export formats, and integrations for the major WooCommerce invoice plugins. If you're dealing with this compliance problem: SplitVAT on CodeCanyon

Happy to go deeper on any part of the implementation.

Top comments (0)