[BUG] Child products cannot have individual discount rules - product_discount_id ignored on save + missing UI fieldAffected versions: 4.6.6, 4.6.8, 4.9.3 (confirmed: present in all three, never fixed)
Severity: High - can cause heavily negative calculated prices displayed as "Price on request" on frontend
DescriptionWhen a product is assigned as a child of a parent product, three related bugs prevent the discount rule (`product_discount_id`) from being managed independently for that child.
Bug 1 - Missing UI field in the traditional child list (`views/product/tmpl/product_edit_childs.php`)The child product list displayed in the parent's "Child product" tab has no column or selector for `product_discount_id`.
Bug 2 - Missing UI field in the type C customfield child list (`models/customfields.php`)When a product uses a type C customfield ("Multi Variant" / child combo selector), VirtueMart hides the traditional "Child product" tab and replaces it with the type C customfield's own child list panel. This panel also has no column or selector for `product_discount_id`.
This is the more critical case: the type C customfield child list is the
only child management interface available when type C is active. Saving the parent through this interface submits child prices WITHOUT `product_discount_id`, which leads directly to Bug 3.
Bug 3 - Model resets discount rule to 0 when not submitted (`models/product.php`)In `models/product.php`, the `product_discount_id` line was originally wrapped inside an `if (!$isChild)` block, meaning it was never saved for children saved through the parent form. Even after moving it outside that block, if the form does not submit `product_discount_id` (Bug 1 or Bug 2), the fallback `!empty(...) ? ... : 0` sets it to 0.
VirtueMart interprets `product_discount_id = 0` as "apply all active calculation rules", cascading every active discount rule simultaneously. With many active rules this produces a heavily negative `salesPrice` displayed on the frontend as "Price on request".
ConsequenceChild products end up with `product_discount_id = NULL` or `0` in `#__virtuemart_product_prices`.
With many active discount rules this produces a heavily negative `salesPrice` (observed: -67 000), displayed on the frontend as "Price on request".
Note on `product_discount_id` values:
- `NULL` or `0` : all active calculation rules applied (dangerous)
- `-1` : no discount rule at all (safe default for children with no specific rule)
- `> 0` : specific discount rule ID (intentional)
Steps to reproduce- Create a parent product with a type C customfield ("Multi Variant") and at least one child
- Edit the parent - the type C panel shows the child list but has no discount column
- Edit the child product directly, assign a discount rule, save
- Re-open the parent, re-save from the type C panel: the child's `product_discount_id` is reset to 0
- Check `#__virtuemart_product_prices` for the child: `product_discount_id = 0`
- Frontend: all active discount rules cascade onto the child, price goes negative
Reproduces also without type C customfield via the traditional "Child product" tab (Bug 1 + Bug 3).
Proposed fixFix 1 - `administrator/components/com_virtuemart/views/product/tmpl/product_edit_childs.php`Add a `<th>` header and `<td>` cell with `$this->renderDiscountList()` in the traditional child list table:
// In <thead> - after the price column header:
<th style="text-align: left !important;"><?php echo vmText::_('COM_VIRTUEMART_PRODUCT_DISCOUNT')?></th>
// In each <tr> child row - after the price <td>:
<td><?php echo $this->renderDiscountList(
isset($child->allPrices[$child->selectedPrice]['product_discount_id'])
? (int)$child->allPrices[$child->selectedPrice]['product_discount_id']
: -1,
'childs['.$child->virtuemart_product_id.'][mprices][product_discount_id][]'
) ?></td>Fix 2 - `administrator/components/com_virtuemart/models/customfields.php`In `renderProductChildLine()`, add a `<th>` in the thead and a `<td>` with the discount selector in each child row:
// In thead - after COM_VIRTUEMART_PRODUCT_FORM_PRICE_COST:
$html .= '<th style="text-align: left !important;width:80px;">'.vmText::_('COM_VIRTUEMART_PRODUCT_DISCOUNT').'</th>';
// In each child row - after the product_price / virtuemart_product_price_id inputs:
$selectedDiscount = isset($child->allPrices[$child->selectedPrice]['product_discount_id'])
? (int)$child->allPrices[$child->selectedPrice]['product_discount_id']
: -1;
$discountRates = array();
$discountRates[] = JHtml::_('select.option', '-1', vmText::_('COM_VIRTUEMART_PRODUCT_DISCOUNT_NONE'), 'product_discount_id');
$discountRates[] = JHtml::_('select.option', '0', vmText::_('COM_VIRTUEMART_PRODUCT_DISCOUNT_NO_SPECIAL'), 'product_discount_id');
if (!class_exists('VirtueMartModelCalc')) { VmModel::getModel('calc'); }
foreach (VirtueMartModelCalc::getDiscounts() as $disc) {
$discountRates[] = JHtml::_('select.option', $disc->virtuemart_calc_id, $disc->calc_name, 'product_discount_id');
}
$html .= '<td>'.JHtml::_('select.genericlist', $discountRates,
'childs['.$child->virtuemart_product_id.'][mprices][product_discount_id][]',
'class="vm-chzn-add"', 'product_discount_id', 'text', $selectedDiscount, '[').'</td>';Fix 3 - `administrator/components/com_virtuemart/models/product.php`Move `product_discount_id` outside the `if (!$isChild)` block, AND change the fallback from hard-coding 0 to only writing the value when explicitly submitted:
// BEFORE (buggy - inside if (!$isChild), never saved for children):
if (!$isChild){
// ...
$pricesToStore['product_discount_id'] = !empty($data['mprices']['product_discount_id'][$k])
? (int)$data['mprices']['product_discount_id'][$k] : 0; // resets to 0 if missing
// ...
}
// AFTER (outside the block, preserved when not submitted):
if (!$isChild){
// ... other fields only ...
}
if (isset($data['mprices']['product_discount_id'][$k])) {
$pricesToStore['product_discount_id'] = (int)$data['mprices']['product_discount_id'][$k];
}
// If not in POST, existing DB value is preserved
Immediate workaround (SQL)Set `product_discount_id = -1` (no discount rule) for affected child products:
-- Diagnostic
SELECT p.virtuemart_product_id, p.product_parent_id,
pp.product_price, pp.product_discount_id
FROM #__virtuemart_products p
LEFT JOIN #__virtuemart_product_prices pp USING (virtuemart_product_id)
WHERE p.product_parent_id > 0;
-- Fix: NULL/0 -> -1 for children with no intentional discount
UPDATE #__virtuemart_product_prices
SET product_discount_id = -1
WHERE virtuemart_product_id IN (
SELECT virtuemart_product_id
FROM #__virtuemart_products
WHERE product_parent_id > 0
)
AND (product_discount_id IS NULL OR product_discount_id = 0);