Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
63 changes: 37 additions & 26 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()->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 !== -1) {
$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($skipImages = false)
{
return $this->getUrl('*/*/duplicate', ['_current' => true]);
return $this->getUrl('*/*/duplicate', ['_current' => true, 'skipImages' => $skipImages ? 1 : 0]);
}

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

class Mage_Adminhtml_Model_System_Config_Source_Catalog_ImageDuplicate {


public function toOptionArray()
{
return [
['value' => -1, 'label' => Mage::helper('adminhtml')->__('Always ask')],
['value' => 0, 'label' => Mage::helper('adminhtml')->__('Copy images to the new product')],
['value' => 1, '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
10 changes: 10 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,12 @@ public function validateUploadFile($filePath)

return $mimeType !== null;
}

/**
* @return int
*/
public function skipProductImageOnDuplicate()
{
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 @@ -56,6 +56,8 @@
* @method bool getIsChangedWebsites()
* @method bool getIsCustomOptionChanged()
* @method bool getIsDefault()
* @method bool getSkipImagesOnDuplicate()
* @method $this setSkipImagesOnDuplicate(bool $value)
* @method bool getIsDuplicate()
* @method bool getIsMassupdate()
* @method bool getIsRecurring()
Expand Down Expand Up @@ -1365,6 +1367,12 @@ public function duplicate()
->setId(null)
->setStoreId(Mage::app()->getStore()->getId());

if($newProduct->getSkipImagesOnDuplicate() == null && $this->_getImageHelper()->skipProductImageOnDuplicate() === -1){
$newProduct->setSkipImagesOnDuplicate(false);
}else{
$newProduct->setSkipImagesOnDuplicate((bool) $this->_getImageHelper()->skipProductImageOnDuplicate());
}

Mage::dispatchEvent(
'catalog_model_product_duplicate',
['current_product' => $this, 'new_product' => $newProduct],
Expand Down Expand Up @@ -1437,7 +1445,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
44 changes: 43 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,13 @@
*/
protected $_productCategoryTable;

/**
* Used when duplicating product
*
* @var string
*/
protected $_skipImagesOnDuplicate = false;

Check failure on line 36 in app/code/core/Mage/Catalog/Model/Resource/Product.php

View workflow job for this annotation

GitHub Actions / PHPStan / Analyze

Property Mage_Catalog_Model_Resource_Product::$_skipImagesOnDuplicate (string) does not accept default value of type false.

/**
* Initialize resource
*/
Expand Down Expand Up @@ -565,9 +572,22 @@
{
$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 +603,10 @@
->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 +743,22 @@

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

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

Check failure on line 752 in app/code/core/Mage/Catalog/Model/Resource/Product.php

View workflow job for this annotation

GitHub Actions / PHPStan / Analyze

Property Mage_Catalog_Model_Resource_Product::$_skipImagesOnDuplicate (string) does not accept bool.
return $this;
}

/**
* @return bool|string
*/
public function getSkipImagesOnDuplicate(){
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
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,38 @@
return 1;
}

function openDuplicateDialog(keepImagesUrl,skipImagesUrl) {
var html = '<p><small><?php echo $this->__('You can disable this message on'); ?>:<br/> <i><?php echo $this->__('System'); ?> > <?php echo $this->__('Configuration'); ?> > <?php echo $this->__('Catalog Images'); ?> > <?php echo $this->__('Product Image'); ?></i></small></p><br/>';

function duplicateKeepImages(dialogWindow) {
dialogWindow.close();
setLocation(keepImagesUrl);
}
function duplicateSkipImages(dialogWindow) {
dialogWindow.close();
setLocation(skipImagesUrl);
}

Dialog.confirm(html, {
width: 450,
height: 120,
draggable:true,
closable:true,
className:"magento",
windowClassName:"popup-window",
title:'<?php echo $this->__('Copy the images onto the new product?') ?>',
recenterAuto:false,
hideEffect:Element.hide,
showEffect:Element.show,
id:"duplicate-product",
buttonClass:"form-button",
okLabel:"<?php echo $this->__('Yes'); ?>",
ok: duplicateKeepImages.bind(this),
cancelLabel: "<?php echo $this->__('Duplicate product without images'); ?>",
cancel: duplicateSkipImages.bind(this),
});
}

Event.observe(window, 'load', function() {
var objName = '<?php echo $this->getSelectedTabId() ?>';
if (objName) {
Expand Down
7 changes: 7 additions & 0 deletions app/locale/en_US/Mage_Adminhtml.csv
Original file line number Diff line number Diff line change
Expand Up @@ -1312,3 +1312,10 @@
"{{base_url}} is not recommended to use in a production environment to declare the Base Unsecure URL / Base Secure URL. It is highly recommended to change this value in your Magento <a href=""%s"">configuration</a>.","{{base_url}} is not recommended to use in a production environment to declare the Base Unsecure URL / Base Secure URL. It is highly recommended to change this value in your Magento <a href=""%s"">configuration</a>."
"Powered by OpenMage","Powered by OpenMage"
"At least one currency has to be allowed.","At least one currency has to be allowed."
"You can disable this message on","You can disable this message on"
"Copy the images onto the new product?","Copy the images onto the new product?"
"Copy images to the new product","Copy images to the new product"
"Always ask","Always ask"
"Duplicate product without images","Duplicate product without images"
"Skip Images on Duplicate","Skip Images on Duplicate"
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
"Skip Images on Duplicate","Skip Images on Duplicate"
"Copy Images on Duplicate","Copy Images on Duplicate"

"'Ask' option only affects Admin interface. Default for programmatical duplication is to persist images.","'Ask' option only affects Admin interface. Default for programmatical duplication is to persist images."
Loading