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
],
];
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
);
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;
}
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,
]]
]);
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)