News:

Support the VirtueMart project and become a member

Main Menu

Recent posts

#21
General Questions / Google Merchant Center Product...
Last post by hotrod - June 29, 2026, 14:08:08 PM
Since moving my store to J4 and VM 4.2.4  I lost my Data Feed for Google Merchant..  I don't even remember what I had or where I got it.    Most of the answers here on the question show dead links..

Any help would be great..

Rod
#22
Administration & Configuration / Re: blank pdf delivery note an...
Last post by panne003 - June 26, 2026, 16:25:06 PM
### Opgelost: Lege PDF-facturen en afleverbonnen bij PAY.nl Billink (VirtueMart 4 / Joomla 5)

Na bijna twee jaar zoeken heb ik eindelijk de oorzaak gevonden.

**Omgeving**

* Joomla 5.4.6
* VirtueMart 4.6.8
* PAY.nl plugin 4.0.3
* Billink Achteraf Betalen

**Probleem**

Alleen bij Billink werden de factuur-PDF en afleverbon volledig leeg gegenereerd.

De HTML van de factuur was volledig correct, maar de PDF bevatte alleen een lege pagina. Andere betaalmethoden zoals iDEAL werkten probleemloos.

Na uitgebreid debuggen van:

* VirtueMart
* TCPDF
* PAY.nl plugin
* vmpdf.php

bleek uiteindelijk dat de oorzaak helemaal niet in de plugin zat.

### Oorzaak

De naam van de betaalmethode bevatte:

```
Billink Achteraf betalen <€100,-
```

Het `<`-teken werd door TCPDF als het begin van een HTML-tag geïnterpreteerd.

Daardoor kon TCPDF de HTML niet meer correct verwerken en werd een lege PDF aangemaakt.

### Oplossing

Wijzig de naam van de betaalmethode bijvoorbeeld naar:

```
Billink Achteraf betalen tot €100,-
```

of

```
Billink Achteraf betalen (€0 - €100)
```

Na deze wijziging werken direct weer:

* PDF-facturen
* Afleverbonnen
* E-mails met PDF-bijlage

Misschien helpt dit iemand anders die hetzelfde probleem heeft.

### Solved: Empty PDF invoices and packing slips with PAY.nl Billink (VirtueMart 4 / Joomla 5)

After almost two years of troubleshooting I finally found the root cause.

**Environment**

* Joomla 5.4.6
* VirtueMart 4.6.8
* PAY.nl plugin 4.0.3
* Billink Post-Payment

**Problem**

Only Billink generated completely empty invoice PDFs and packing slips.

The invoice HTML was generated correctly, but the PDF itself contained no content.

Other payment methods (such as iDEAL) worked perfectly.

After debugging:

* VirtueMart
* TCPDF
* PAY.nl plugin
* vmpdf.php

it turned out that neither VirtueMart nor the PAY.nl plugin was responsible.

### Root cause

The payment method name contained:

```
Billink Achteraf betalen <€100,-
```

The `<` character was interpreted by TCPDF as the start of an HTML tag.

As a result, TCPDF failed to render the HTML correctly and generated an almost empty PDF.

### Solution

Rename the payment method, for example:

```
Billink Post-Payment up to €100
```

or

```
Billink Post-Payment (€0 - €100)
```

After removing the `<` character:

* Invoice PDFs are generated correctly.
* Packing slips work again.
* PDF attachments in customer emails are generated correctly.

Hopefully this saves someone else many hours of debugging.


#23
And which Captcha solution will work with this code change?
#24
Feature Request - Child product ordering via drag-and-drop in type C (Multi Variant) customfield panel

VirtueMart version: 4.6.8 (also checked on 4.6.6 and 4.9.3)
Joomla version: 5.4.6
PHP: 8.1 - 8.5

Summary

When a product uses a type C "Multi Variant" customfield, the child product table in the backend panel provides no way to reorder variants. The display order is determined by sortChildIds() which sorts by option combinations - not by the merchant's preferred order. Reordering is a common need (e.g. keeping size variants sorted smallest to largest, or putting the most popular variant first).

Current behaviour

The child table in the type C panel renders with a vmicon-16-move handle in each row and jQuery UI Sortable is already initialised on #syncro (the tbody). Dragging rows works visually but nothing is ever saved - the update callback is absent and no pordering value is submitted with the form.

After a save, sortChildIds() re-sorts children by their option combination order, overriding whatever was in the database.

A second issue: the parent product (which is itself a variant in type C products) is always forced to the first row via array_unshift, regardless of drag order. Its pordering is also never saved because the childs save loop in product.php skips entries where $productId == $data['virtuemart_product_id'].

Expected behaviour

Dragging any row (including the parent variant) should update and persist the display order. The frontend already uses ORDER BY pordering ASC via getAllProductChildIds() - the infrastructure is in place. Only the admin side is missing.

Fix - three changes

1. models/customfields.php - add hidden pordering input in renderProductChildLine() (first <td>, before the link):

$html .= '<td style="white-space:nowrap;">'
. '<span class="vmicon vmicon-16-move"
      style="cursor:move;font-size:20px;color:#666;vertical-align:middle;margin:0 6px 0 5px;"
      title="Reorder">⋮⋮</span>'
. JHTML::_('link', ...)
. '<input type="hidden" class="vmchild-ordering"
      name="childs[' . $child->virtuemart_product_id . '][pordering]"
      value="' . (int)($child->pordering ?? 0) . '" />'
. '</td>';

2. models/customfields.php - replace sortChildIds + array_unshift block with pordering-aware sort:

if (isset($childIds[$product_id])) {
$sorted = self::sortChildIds($product_id, $childIds[$product_id], $field->options);
$allIds = array_merge([$product_id], array_values($childIds[$product_id]));
$db = JFactory::getDBO();
$db->setQuery('SELECT virtuemart_product_id, pordering FROM #__virtuemart_products'
. ' WHERE virtuemart_product_id IN (' . implode(',', array_map('intval', $allIds)) . ')');
$porderingMap = $db->loadAssocList('virtuemart_product_id', 'pordering');
$sorted[] = ['parent_id' => $product_id, 'vm_product_id' => $product_id]; // parent included
usort($sorted, function ($a, $b) use ($porderingMap) {
$posA = isset($porderingMap[$a['vm_product_id']]) ? (int)$porderingMap[$a['vm_product_id']] : 999;
$posB = isset($porderingMap[$b['vm_product_id']]) ? (int)$porderingMap[$b['vm_product_id']] : 999;
return $posA - $posB;
});
} else {
$sorted[] = ['parent_id' => $product_id, 'vm_product_id' => $product_id];
}
// array_unshift(...) removed - parent is now part of the sorted array

3. models/customfields.php - add update callback to the #syncro sortable (existing JS block):

jQuery(document).ready(function ($) {
$('#syncro').sortable({
cursorAt: { top: 0, left: 0 },
handle: '.vmicon-16-move',
update: function (event, ui) {
$(this).find('.vmchild-ordering').each(function (index, el) {
$(el).val(index);
});
}
});
$('#syncro .vmchild-ordering').each(function (index, el) {
$(el).val(index);
});
});

4. models/product.php - save parent's pordering when submitted from the type C panel:

In the foreach ($data['childs'] as $productId => $child) loop, add an else branch for the case where $productId == $data['virtuemart_product_id']:

} else {
// Parent variant: only update pordering if explicitly submitted
if (isset($child['pordering'])) {
$db = JFactory::getDBO();
$db->setQuery('UPDATE #__virtuemart_products SET pordering = '
. (int)$child['pordering']
. ' WHERE virtuemart_product_id = ' . (int)$productId);
$db->execute();
}
}

Why it matters

Merchants managing products with several size or format variants (e.g. 100 cm / 130 cm / 150 cm / 200 cm / 250 cm) have no control over which variant appears first in the selector on the product page. Making the existing drag infrastructure actually save the order would be a low-risk, high-value improvement. The pordering field and the ORDER BY pordering clause are already in place on the frontend side - the admin panel simply never writes to them.

Thanks again for reading this...
#25
[BUG] Product edit - Categories field collapses to width: 0px after tab switch (Chosen.js + UIkit hidden tab race condition)

VirtueMart version: 4.6.8
Joomla version: 5.x
PHP version: 8.x
Template: vmadmin (default backend template)

Description

When editing a product, switching away from the "Product Information" tab and then switching back causes the Categories multi-select field to collapse to zero width and become unusable. The browser inspector shows:

<div class="chosen-container chosen-container-multi" id="categories_chosen" style="width: 0px;">
Steps to reproduce

  • Open any product for editing (the "Product Information" tab is active by default).
  • Click any other tab, e.g. "Product Images".
  • Click back on "Product Information".
  • The Categories field is now crushed to 0px width and invisible.

The bug also reproduces reliably on first load when the product was previously saved while on a tab other than "Product Information" (UIkit stores the active tab in a cookie - on reload, the "Information" tab starts hidden).

Root cause

Virtuemart.loadCategoryTree() (in
components/com_virtuemart/assets/js/ajax_catree.js) fires an AJAX request during
document.ready. On AJAX success, it calls:

jQuery('select#' + id).chosen({ select_some_options_text: Virtuemart.selectSomeCategory });
// no width option passed

Chosen.js computes its container width from the
<select> element at the moment of initialization. If the AJAX response arrives while the "Product Information"
<li> is hidden by UIkit's switcher (
display: none), the measured width is 0. Chosen then stamps
style="width: 0px;" on its container, and nothing corrects it when the tab becomes visible again.

This is a classic AJAX-vs-hidden-element race condition. The AJAX delay is unpredictable, and once the user has switched tabs, the hidden tab has zero dimensions.

A secondary trigger:
Virtuemart.updateChosenDropdownLayout() (generated by
vmjsapi.php) passes
width: '100%' to
.chosen(), which avoids the problem. But the categories field carries the class
vm-chzn-add, which intentionally excludes it from that initializer (
select:not(.vm-chzn-add)). So the categories select is always initialized through the AJAX path, without a width fallback.

Proposed fix - two-part

1. Root fix (in ajax_catree.js): pass
width: '100%' to the
.chosen() call so the container always gets a percentage-based width instead of a pixel-measured one.

// Before (line ~87):
jQuery('select#'+id).chosen({select_some_options_text: Virtuemart.selectSomeCategory});

// After:
jQuery('select#'+id).chosen({select_some_options_text: Virtuemart.selectSomeCategory, width: '100%'});

2. Safety net (UIkit show event): UIkit 3 dispatches
show with
bubbles: true on each
<li> of the switcher when it becomes active (confirmed in uikit.js -
createEvent(e, bubbles = true)). Adding a listener on the tabs container catches any other Chosen widget that may have suffered the same fate:

jQuery(document).ready(function($) {
var tabsContainer = document.getElementById('vmuikit-admin-ui-tabs');
if (!tabsContainer) { return; }
tabsContainer.addEventListener('show', function(e) {
$(e.target).find('.chosen-container').each(function() {
if (this.style.width === '0px') {
$(this).css('width', '100%');
}
});
});
});

This listener is harmless on every other tab switch (no
.chosen-container with
width: 0px means the
.each() body never runs).

Workaround for existing installations

Add the safety-net JS snippet above to a template override of
administrator/templates/vmadmin/html/com_virtuemart/product/product_edit_information.php via
vmJsApi::addJScript().

Thanks for reading all of that
#26
[BUG] Child products cannot have individual discount rules - product_discount_id ignored on save + missing UI field

Affected 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



Description

When 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".



Consequence

Child 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 fix

Fix 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:

Code (php) Select
// 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:

Code (php) Select
// 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:

Code (php) Select
// 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:

Code (sql) Select
-- 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);
#27
Virtuemart Development and bug reports / recaptcha wasnt rendering
Last post by PRO - June 24, 2026, 20:19:56 PM
Captcha field was failing to render , was wrapped in editor html, the onInit, and onDisplay was causing this.

shopfunctionsF


comment these out
            //   $app = JFactory::getApplication();
            //   $results = $app->triggerEvent('onInit', [$id]);
            //   $output  = $app->triggerEvent('onDisplay', [$reCaptchaName, $id, 'g-recaptcha required']);



and added
   $captcha = \Joomla\CMS\Captcha\Captcha::getInstance($reCaptchaName);
    $output = [$captcha->display($id, $id, 'g-recaptcha required')];



so fixed as so

   } else {
            //   $app = JFactory::getApplication();
            //   $results = $app->triggerEvent('onInit', [$id]);
            //   $output  = $app->triggerEvent('onDisplay', [$reCaptchaName, $id, 'g-recaptcha required']);
            $captcha = \Joomla\CMS\Captcha\Captcha::getInstance($reCaptchaName);
    $output = [$captcha->display($id, $id, 'g-recaptcha required')];
            }


#28
3rd party extension / USPS Shipping for VirtueMart 4...
Last post by Jumbo! - June 24, 2026, 00:57:01 AM
Real-time USPS shipping rates in VirtueMart, on the new USPS REST API

The old USPS Web Tools XML API was retired in January 2026. That change broke a lot of older USPS shipping plugins, and many stores suddenly stopped showing live postage at checkout.

VP USPS Shipping is built on the new USPS REST API, the official replacement that uses OAuth 2.0 and JSON. Your store keeps quoting accurate USPS rates, fetched live at checkout from the real cart weight, the package dimensions and the customer's destination.

You can use retail or commercial base rates, and commercial rates do not need a USPS account number. Built-in rate caching keeps you within the USPS limit of 60 requests per hour, and you can point the plugin at the production or the staging environment while you test.

Domestic services
  • Priority Mail and Priority Mail Express
  • USPS Ground Advantage
  • First-Class Package Service
  • Media Mail and Library Mail
  • Small, Medium and Large Flat Rate Boxes
  • Flat Rate, Legal and Padded Envelopes, for Priority Mail and Priority Mail Express

International services
  • Priority Mail International and Priority Mail Express International
  • First-Class Package International Service
  • Global Express Guaranteed
  • International flat rate boxes and envelopes

A FEW THINGS THAT SET IT APART
  • Smart Flat Rate. A 3D box-packing algorithm checks that the products physically fit inside a flat rate container before that container is offered, so you never quote a box the order cannot go in.
  • Multi-box shipping for heavy orders, with configurable outer box dimensions per method, a cart weight padding option and a machinable toggle.
  • Show Cheapest Option Only. Collapse the results to the single lowest-priced method that qualifies when you want a simpler checkout.
  • Handling fees as a flat amount, a percentage of the shipping cost, or a percentage of the cart total. Per-method tax rules and logos are supported too.
  • Restrictions by destination country, ZIP or postal range, order amount, product category and a blocking category. On VirtueMart 4.6 and later it uses the VirtueMart core restrictions, which also add blocking countries, and it falls back to its own restrictions on older versions.

One thing to be clear about. Rates come back in US Dollars with no currency conversion, and the plugin is built for stores that ship from a US address, since USPS only collects packages inside the US.

No hacks, no manual file edits. Install the plugin with the normal Joomla Extensions Installer, enter your USPS Client ID and Secret, enable it, and you are ready to go.



System Requirement: VirtueMart 3.8+, 4.0, 4.2, 4.4 or 4.6 | Joomla 3, 4 or 5 | PHP 7.1+ (PHP 8.1+ recommended for Joomla 5) | cURL extension enabled

Product Details: https://www.virtueplanet.com/extensions/usps-shipping
Documentation: User Guide
Support: https://www.virtueplanet.com/forum/vp-usps-shipping-plugin
Pre-sale Queries: https://www.virtueplanet.com/contact-us

VirtuePlanet - https://www.virtueplanet.com

#29
Unfortunately, there has been no response from the Virtuemart developers so far. There's no news on this matter. If anyone knows of an update, please let me know. This is very important for Virtuemart stores users.
#30
I repeat question, will there be an update? It's very important.