Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
302034c
When duplicating a product that has images, ask the admin whether to …
m-michalis Nov 9, 2025
9b040e6
trying not to introduce extra param
m-michalis Nov 9, 2025
8cccafc
Merge remote-tracking branch 'upstream/main' into feature/prod-clone-ask
m-michalis Nov 9, 2025
b93cfb1
Merge branch 'main' into feature/prod-clone-ask
sreichel Nov 14, 2025
9ff82e7
Merge branch 'main' into feature/prod-clone-ask
sreichel Nov 24, 2025
3194bc0
Update app/code/core/Mage/Catalog/Model/Resource/Product.php
m-michalis Dec 12, 2025
be7015c
Update app/code/core/Mage/Catalog/Model/Resource/Product.php
m-michalis Dec 12, 2025
44f2fba
Update app/code/core/Mage/Catalog/Helper/Image.php
m-michalis Dec 12, 2025
f16ad66
Update app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit.php
m-michalis Dec 12, 2025
cfa49be
Update app/code/core/Mage/Catalog/Model/Resource/Product.php
m-michalis Dec 12, 2025
02512cd
Update app/code/core/Mage/Catalog/Model/Resource/Product.php
m-michalis Dec 12, 2025
5c9f625
Update app/code/core/Mage/Catalog/Model/Product.php
m-michalis Dec 12, 2025
be445af
Merge branch 'main' into feature/prod-clone-ask
sreichel Dec 12, 2025
706a0d5
Merge remote-tracking branch 'upstream/main' into feature/prod-clone-ask
m-michalis Dec 13, 2025
7c01a99
- cs fixes
m-michalis Dec 13, 2025
3a90931
in adminhtml (at least) for a product to be duplicable, it should exi…
m-michalis Dec 13, 2025
f8e244f
Merge branch 'main' into feature/prod-clone-ask
addison74 Dec 14, 2025
5de8bd6
Merge branch 'main' into feature/prod-clone-ask
sreichel Dec 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,19 @@ protected function _prepareLayout()
'back_button',
$this->getLayout()->createBlock('adminhtml/widget_button')
->setData([
'label' => Mage::helper('catalog')->__('Back'),
'onclick' => Mage::helper('core/js')->getSetLocationJs($this->getUrl('*/*/', ['store' => $this->getRequest()->getParam('store', 0)])),
'class' => 'back',
'label' => Mage::helper('catalog')->__('Back'),
'onclick' => Mage::helper('core/js')->getSetLocationJs($this->getUrl('*/*/', ['store' => $this->getRequest()->getParam('store', 0)])),
'class' => 'back',
]),
);
} else {
$this->setChild(
'back_button',
$this->getLayout()->createBlock('adminhtml/widget_button')
->setData([
'label' => Mage::helper('catalog')->__('Close Window'),
'onclick' => 'window.close()',
'class' => 'cancel',
'label' => Mage::helper('catalog')->__('Close Window'),
'onclick' => 'window.close()',
'class' => 'cancel',
]),
);
}
Expand All @@ -63,19 +63,19 @@ protected function _prepareLayout()
'reset_button',
$this->getLayout()->createBlock('adminhtml/widget_button')
->setData([
'label' => Mage::helper('catalog')->__('Reset'),
'onclick' => Mage::helper('core/js')->getSetLocationJs($this->getUrl('*/*/*', ['_current' => true])),
'class' => 'reset',
'label' => Mage::helper('catalog')->__('Reset'),
'onclick' => Mage::helper('core/js')->getSetLocationJs($this->getUrl('*/*/*', ['_current' => true])),
'class' => 'reset',
]),
);

$this->setChild(
'save_button',
$this->getLayout()->createBlock('adminhtml/widget_button')
->setData([
'label' => Mage::helper('catalog')->__('Save'),
'onclick' => 'productForm.submit()',
'class' => 'save',
'label' => Mage::helper('catalog')->__('Save'),
'onclick' => 'productForm.submit()',
'class' => 'save',
]),
);
}
Expand All @@ -86,9 +86,9 @@ protected function _prepareLayout()
'save_and_edit_button',
$this->getLayout()->createBlock('adminhtml/widget_button')
->setData([
'label' => Mage::helper('catalog')->__('Save and Continue Edit'),
'onclick' => Mage::helper('core/js')->getSaveAndContinueEditJs($this->getSaveAndContinueUrl()),
'class' => 'save continue',
'label' => Mage::helper('catalog')->__('Save and Continue Edit'),
'onclick' => Mage::helper('core/js')->getSaveAndContinueEditJs($this->getSaveAndContinueUrl()),
'class' => 'save continue',
]),
);
}
Expand All @@ -98,21 +98,32 @@ protected function _prepareLayout()
'delete_button',
$this->getLayout()->createBlock('adminhtml/widget_button')
->setData([
'label' => Mage::helper('catalog')->__('Delete'),
'onclick' => Mage::helper('core/js')->getConfirmSetLocationJs($this->getDeleteUrl()),
'class' => 'delete',
'label' => Mage::helper('catalog')->__('Delete'),
'onclick' => Mage::helper('core/js')->getConfirmSetLocationJs($this->getDeleteUrl()),
'class' => 'delete',
]),
);
}

if ($this->getProduct()->isDuplicable()) {
if ($this->getProduct()->isDuplicable() && $this->getProduct()->getId()) {
if ($this->getProduct()->getMediaGalleryImages()->count() === 0) {
$onClickAction = Mage::helper('core/js')->getSetLocationJs($this->getDuplicateUrl(true));
} else {
$skipImgOnDuplicate = $this->helper('catalog/image')->skipProductImageOnDuplicate();
$onClickAction = "openDuplicateDialog('" . $this->getDuplicateUrl(false) . "','" . $this->getDuplicateUrl(true) . "'); return false;";

if ($skipImgOnDuplicate !== Mage_Catalog_Model_Product_Image::ON_DUPLICATE_ASK) {
$onClickAction = Mage::helper('core/js')->getSetLocationJs($this->getDuplicateUrl((bool) $skipImgOnDuplicate));
}
}

$this->setChild(
'duplicate_button',
$this->getLayout()->createBlock('adminhtml/widget_button')
->setData([
'label' => Mage::helper('catalog')->__('Duplicate'),
'onclick' => Mage::helper('core/js')->getSetLocationJs($this->getDuplicateUrl()),
'class' => 'add duplicate',
'label' => Mage::helper('catalog')->__('Duplicate'),
'onclick' => $onClickAction,
'class' => 'add duplicate',
]),
);
}
Expand Down Expand Up @@ -191,9 +202,9 @@ public function getSaveUrl()
public function getSaveAndContinueUrl()
{
return $this->getUrl('*/*/save', [
'_current' => true,
'back' => 'edit',
'tab' => '{{tab_id}}',
'_current' => true,
'back' => 'edit',
'tab' => '{{tab_id}}',
'active_tab' => null,
]);
}
Expand Down Expand Up @@ -229,9 +240,9 @@ public function getDeleteUrl()
/**
* @return string
*/
public function getDuplicateUrl()
public function getDuplicateUrl(bool $skipImages = false)
{
return $this->getUrl('*/*/duplicate', ['_current' => true]);
return $this->getUrl('*/*/duplicate', ['_current' => true, 'skipImages' => $skipImages ? Mage_Catalog_Model_Product_Image::ON_DUPLICATE_SKIP : Mage_Catalog_Model_Product_Image::ON_DUPLICATE_COPY]);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

class Mage_Adminhtml_Model_System_Config_Source_Catalog_ImageDuplicate
{
public function toOptionArray()
{
return [
['value' => Mage_Catalog_Model_Product_Image::ON_DUPLICATE_ASK, 'label' => Mage::helper('adminhtml')->__('Always ask')],
['value' => Mage_Catalog_Model_Product_Image::ON_DUPLICATE_COPY, 'label' => Mage::helper('adminhtml')->__('Copy images to the new product')],
['value' => Mage_Catalog_Model_Product_Image::ON_DUPLICATE_SKIP, 'label' => Mage::helper('adminhtml')->__('Duplicate product without images')],
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,12 @@ public function duplicateAction()
{
$product = $this->_initProduct();
try {
$imgHelper = Mage::helper('catalog/image');

if ($imgHelper->skipProductImageOnDuplicate() === -1) {
$product->setSkipImagesOnDuplicate((bool) $this->getRequest()->getParam('skipImages', true));
}

$newProduct = $product->duplicate();
$this->_getSession()->addSuccess($this->__('The product has been duplicated.'));
$this->_redirect('*/*/edit', ['_current' => true, 'id' => $newProduct->getId()]);
Expand Down
7 changes: 7 additions & 0 deletions app/code/core/Mage/Catalog/Helper/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class Mage_Catalog_Helper_Image extends Mage_Core_Helper_Abstract

public const XML_NODE_PRODUCT_MAX_DIMENSION = 'catalog/product_image/max_dimension';

public const XML_NODE_SKIP_IMAGE_ON_DUPLICATE_ACTION = 'catalog/product_image/images_on_duplicate_action';

protected $_moduleName = 'Mage_Catalog';

/**
Expand Down Expand Up @@ -650,4 +652,9 @@ public function validateUploadFile($filePath)

return $mimeType !== null;
}

public function skipProductImageOnDuplicate(): int
{
return Mage::getStoreConfigAsInt(self::XML_NODE_SKIP_IMAGE_ON_DUPLICATE_ACTION);
}
}
12 changes: 11 additions & 1 deletion app/code/core/Mage/Catalog/Model/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
* @method string getShipmentType()
* @method string getShortDescription()
* @method bool getSkipCheckRequiredOption()
* @method bool getSkipImagesOnDuplicate()
* @method string getSmallImage()
* @method bool getStickWithinParent()
* @method array getStockData()
Expand Down Expand Up @@ -209,6 +210,7 @@
* @method $this setRequiredOptions(bool $value)
* @method $this setShortDescription(string $value)
* @method $this setSkipCheckRequiredOption(bool $value)
* @method $this setSkipImagesOnDuplicate(bool $value)
* @method $this setSku(string $value)
* @method $this setStatus(int $store)
* @method $this setStockData(array $value)
Expand Down Expand Up @@ -1372,6 +1374,12 @@ public function duplicate()
->setId(null)
->setStoreId(Mage::app()->getStore()->getId());

if (is_null($newProduct->getSkipImagesOnDuplicate()) && $this->_getImageHelper()->skipProductImageOnDuplicate() === Mage_Catalog_Model_Product_Image::ON_DUPLICATE_ASK) {
$newProduct->setSkipImagesOnDuplicate(false);
} else {
$newProduct->setSkipImagesOnDuplicate($this->_getImageHelper()->skipProductImageOnDuplicate());
}

Mage::dispatchEvent(
'catalog_model_product_duplicate',
['current_product' => $this, 'new_product' => $newProduct],
Expand Down Expand Up @@ -1444,7 +1452,9 @@ public function duplicate()
$newProduct->save();

$this->getOptionInstance()->duplicate($this->getId(), $newProduct->getId());
$this->getResource()->duplicate($this->getId(), $newProduct->getId());
$this->getResource()
->setSkipImagesOnDuplicate($newProduct->getSkipImagesOnDuplicate())
->duplicate($this->getId(), $newProduct->getId());

// TODO - duplicate product on all stores of the websites it is associated with
/*if ($storeIds = $this->getWebsiteIds()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,15 @@ public function beforeSave($object)
$value['images'] = Mage::helper('core')->jsonDecode($value['images']);
}

if (!isset($value['values'])) {
if (!isset($value['values']) || $object->getSkipImagesOnDuplicate()) {
$value['values'] = [];
}

if (!is_array($value['values']) && (string) $value['values'] !== '') {
$value['values'] = Mage::helper('core')->jsonDecode($value['values']);
}

if (!is_array($value['images'])) {
if (!is_array($value['images']) || $object->getSkipImagesOnDuplicate()) {
$value['images'] = [];
}

Expand Down Expand Up @@ -696,7 +696,7 @@ public function duplicate($object)
$attrCode = $this->getAttribute()->getAttributeCode();
$mediaGalleryData = $object->getData($attrCode);

if (!isset($mediaGalleryData['images']) || !is_array($mediaGalleryData['images'])) {
if (!isset($mediaGalleryData['images']) || !is_array($mediaGalleryData['images']) || $object->getSkipImagesOnDuplicate()) {
return $this;
}

Expand Down
23 changes: 23 additions & 0 deletions app/code/core/Mage/Catalog/Model/Product/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@ class Mage_Catalog_Model_Product_Image extends Mage_Core_Model_Abstract
{
public const DIMENSIONS_SEPARATOR = 'x';


/**
* Always ask to copy images on duplicated product
*
* @var int
*/
public const ON_DUPLICATE_ASK = -1;

/**
* Copy images to the new product
*
* @var int
*/
public const ON_DUPLICATE_COPY = 0;


/**
* Always ask
*
* @var int
*/
public const ON_DUPLICATE_SKIP = 1;

/**
* Requested width for the scaled image
* @var int
Expand Down
38 changes: 37 additions & 1 deletion app/code/core/Mage/Catalog/Model/Resource/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class Mage_Catalog_Model_Resource_Product extends Mage_Catalog_Model_Resource_Ab
*/
protected $_productCategoryTable;

/**
* Used when duplicating product
*/
protected bool $skipImagesOnDuplicate = false;

/**
* Initialize resource
*/
Expand Down Expand Up @@ -565,9 +570,22 @@ public function duplicate($oldId, $newId)
{
$adapter = $this->_getWriteAdapter();
$eavTables = ['datetime', 'decimal', 'int', 'text', 'varchar'];

$mediaImageAttributeSkipIds = [];
$adapter = $this->_getWriteAdapter();

if ($this->getSkipImagesOnDuplicate()) {

/**
* @var int $attributeId
* @var Mage_Eav_Model_Entity_Attribute_Abstract $attribute
*/
foreach ($this->getAttributesById() as $attributeId => $attribute) {
if ($attribute->getFrontendInput() == 'media_image') {
$mediaImageAttributeSkipIds[$attribute->getBackendType()][] = $attributeId;
}
}
}

// duplicate EAV store values
foreach ($eavTables as $suffix) {
$tableName = $this->getTable(['catalog/product', $suffix]);
Expand All @@ -583,6 +601,10 @@ public function duplicate($oldId, $newId)
->where('entity_id = ?', $oldId)
->where('store_id > ?', 0);

if (isset($mediaImageAttributeSkipIds[$suffix])) {
$select->where('attribute_id NOT IN (?)', $mediaImageAttributeSkipIds[$suffix]);
}

$adapter->query($adapter->insertFromSelect(
$select,
$tableName,
Expand Down Expand Up @@ -719,4 +741,18 @@ public function getCategoryIdsWithAnchors($object)

return $this->_getReadAdapter()->fetchCol($select);
}

/**
* @return $this
*/
public function setSkipImagesOnDuplicate(bool $newProductSkipImages)
{
$this->skipImagesOnDuplicate = $newProductSkipImages;
return $this;
}

public function getSkipImagesOnDuplicate(): bool
{
return $this->skipImagesOnDuplicate;
}
}
1 change: 1 addition & 0 deletions app/code/core/Mage/Catalog/etc/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,7 @@
<base_width>1800</base_width>
<small_width>210</small_width>
<max_dimension>5000</max_dimension>
<images_on_duplicate_action>-1</images_on_duplicate_action>
</product_image>
<seo>
<product_url_suffix>.html</product_url_suffix>
Expand Down
10 changes: 10 additions & 0 deletions app/code/core/Mage/Catalog/etc/system.xml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,16 @@
<show_in_default>1</show_in_default>
<validate>validate-digits validate-greater-than-zero</validate>
</progressive_threshold>
<images_on_duplicate_action translate="label comment">
<label>Skip Images on Duplicate</label>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<label>Skip Images on Duplicate</label>
<label>Copy Images on Duplicate</label>

<comment>'Ask' option only affects Admin interface. Default for programmatical duplication is to persist images.</comment>
<frontend_type>select</frontend_type>
<source_model>adminhtml/system_config_source_catalog_imageDuplicate</source_model>
<sort_order>50</sort_order>
<show_in_default>1</show_in_default>
<show_in_website>1</show_in_website>
<show_in_store>1</show_in_store>
</images_on_duplicate_action>
</fields>
</product_image>
<placeholder translate="label">
Expand Down
Loading
Loading