From 302034c1163f03f0b551a192d0ac152c99c6679f Mon Sep 17 00:00:00 2001 From: michalis Date: Sun, 9 Nov 2025 07:37:45 +0200 Subject: [PATCH 1/2] When duplicating a product that has images, ask the admin whether to copy the images or not. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit added a configuration option “Skip Images on Duplicate” and a small prompt on Duplicate when the source product has images. --- .../Adminhtml/Block/Catalog/Product/Edit.php | 63 +++++++++++-------- .../Config/Source/Catalog/ImageDuplicate.php | 14 +++++ .../controllers/Catalog/ProductController.php | 6 ++ app/code/core/Mage/Catalog/Helper/Image.php | 10 +++ app/code/core/Mage/Catalog/Model/Product.php | 14 ++++- .../Model/Product/Attribute/Backend/Media.php | 6 +- .../Mage/Catalog/Model/Resource/Product.php | 21 ++++++- app/code/core/Mage/Catalog/etc/config.xml | 1 + app/code/core/Mage/Catalog/etc/system.xml | 10 +++ .../template/catalog/product/edit.phtml | 32 ++++++++++ app/locale/en_US/Mage_Adminhtml.csv | 7 +++ 11 files changed, 150 insertions(+), 34 deletions(-) create mode 100644 app/code/core/Mage/Adminhtml/Model/System/Config/Source/Catalog/ImageDuplicate.php diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit.php index b1f053e3fcd..a7891bb257d 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit.php @@ -41,9 +41,9 @@ 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 { @@ -51,9 +51,9 @@ protected function _prepareLayout() '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', ]), ); } @@ -63,9 +63,9 @@ 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', ]), ); @@ -73,9 +73,9 @@ protected function _prepareLayout() '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', ]), ); } @@ -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', ]), ); } @@ -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', ]), ); } @@ -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, ]); } @@ -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]); } /** diff --git a/app/code/core/Mage/Adminhtml/Model/System/Config/Source/Catalog/ImageDuplicate.php b/app/code/core/Mage/Adminhtml/Model/System/Config/Source/Catalog/ImageDuplicate.php new file mode 100644 index 00000000000..91c33cb006b --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Model/System/Config/Source/Catalog/ImageDuplicate.php @@ -0,0 +1,14 @@ + -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')], + ]; + } +} diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php index b06044c174e..06ed62f6b57 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php @@ -823,6 +823,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()]); diff --git a/app/code/core/Mage/Catalog/Helper/Image.php b/app/code/core/Mage/Catalog/Helper/Image.php index 3327dfae9ea..1a8644f594e 100644 --- a/app/code/core/Mage/Catalog/Helper/Image.php +++ b/app/code/core/Mage/Catalog/Helper/Image.php @@ -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'; /** @@ -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); + } } diff --git a/app/code/core/Mage/Catalog/Model/Product.php b/app/code/core/Mage/Catalog/Model/Product.php index 50726d8d91a..a83b3296b46 100644 --- a/app/code/core/Mage/Catalog/Model/Product.php +++ b/app/code/core/Mage/Catalog/Model/Product.php @@ -105,6 +105,8 @@ * @method bool getIsDefault() * @method bool getIsRelationsChanged() * @method $this setIsRelationsChanged(bool $value) + * @method bool getSkipImagesOnDuplicate() + * @method $this setSkipImagesOnDuplicate(bool $value) * @method bool getIsDuplicate() * @method $this setIsDuplicate(bool $value) * @method $this setIsQtyDecimal(int $value) @@ -854,7 +856,7 @@ protected function _afterDeleteCommit() { parent::_afterDeleteCommit(); - /** @var \Mage_Index_Model_Indexer $indexer */ + /** @var Mage_Index_Model_Indexer $indexer */ $indexer = Mage::getSingleton('index/indexer'); $indexer->processEntityAction($this, self::ENTITY, Mage_Index_Model_Event::TYPE_DELETE); @@ -1364,6 +1366,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], @@ -1436,7 +1444,7 @@ public function duplicate() $newProduct->save(); $this->getOptionInstance()->duplicate($this->getId(), $newProduct->getId()); - $this->getResource()->duplicate($this->getId(), $newProduct->getId()); + $this->getResource()->duplicate($this->getId(), $newProduct->getId(), $newProduct->getSkipImagesOnDuplicate()); // TODO - duplicate product on all stores of the websites it is associated with /*if ($storeIds = $this->getWebsiteIds()) { @@ -2401,7 +2409,7 @@ public function afterCommitCallback() { parent::afterCommitCallback(); - /** @var \Mage_Index_Model_Indexer $indexer */ + /** @var Mage_Index_Model_Indexer $indexer */ $indexer = Mage::getSingleton('index/indexer'); $indexer->processEntityAction($this, self::ENTITY, Mage_Index_Model_Event::TYPE_SAVE); diff --git a/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Media.php b/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Media.php index c73a00d564a..e64b868d1bf 100644 --- a/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Media.php +++ b/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Media.php @@ -105,7 +105,7 @@ public function beforeSave($object) $value['images'] = Mage::helper('core')->jsonDecode($value['images']); } - if (!isset($value['values'])) { + if (!isset($value['values']) || $object->getSkipImagesOnDuplicate()) { $value['values'] = []; } @@ -113,7 +113,7 @@ public function beforeSave($object) $value['values'] = Mage::helper('core')->jsonDecode($value['values']); } - if (!is_array($value['images'])) { + if (!is_array($value['images']) || $object->getSkipImagesOnDuplicate()) { $value['images'] = []; } @@ -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; } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product.php b/app/code/core/Mage/Catalog/Model/Resource/Product.php index 191e00531c0..5cb14acea03 100644 --- a/app/code/core/Mage/Catalog/Model/Resource/Product.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product.php @@ -551,13 +551,26 @@ public function canBeShowInCategory($product, $categoryId) * @param int $newId * @return $this */ - public function duplicate($oldId, $newId) + public function duplicate($oldId, $newId, $skipImagesOnDuplicate = false) { $adapter = $this->_getWriteAdapter(); $eavTables = ['datetime', 'decimal', 'int', 'text', 'varchar']; - + $mediaImageAttributeSkipIds = []; $adapter = $this->_getWriteAdapter(); + if($skipImagesOnDuplicate){ + + /** + * @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]); @@ -573,6 +586,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, diff --git a/app/code/core/Mage/Catalog/etc/config.xml b/app/code/core/Mage/Catalog/etc/config.xml index 07adebaca45..a5a78270ebb 100644 --- a/app/code/core/Mage/Catalog/etc/config.xml +++ b/app/code/core/Mage/Catalog/etc/config.xml @@ -810,6 +810,7 @@ 1800 210 5000 + -1 .html diff --git a/app/code/core/Mage/Catalog/etc/system.xml b/app/code/core/Mage/Catalog/etc/system.xml index add69a01d34..ac143082692 100644 --- a/app/code/core/Mage/Catalog/etc/system.xml +++ b/app/code/core/Mage/Catalog/etc/system.xml @@ -203,6 +203,16 @@ 1 validate-digits validate-greater-than-zero + + + 'Ask' option only affects Admin interface. Default for programmatical duplication is to persist images. + select + adminhtml/system_config_source_catalog_imageDuplicate + 50 + 1 + 1 + 1 + diff --git a/app/design/adminhtml/default/default/template/catalog/product/edit.phtml b/app/design/adminhtml/default/default/template/catalog/product/edit.phtml index 5fb4f6e9490..30a4faf538a 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/edit.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/edit.phtml @@ -92,6 +92,38 @@ return 1; } + function openDuplicateDialog(keepImagesUrl,skipImagesUrl) { + var html = '

__('You can disable this message on'); ?>:
__('System'); ?> > __('Configuration'); ?> > __('Catalog Images'); ?> > __('Product Image'); ?>


'; + + 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:'__('Copy the images onto the new product?') ?>', + recenterAuto:false, + hideEffect:Element.hide, + showEffect:Element.show, + id:"duplicate-product", + buttonClass:"form-button", + okLabel:"__('Yes'); ?>", + ok: duplicateKeepImages.bind(this), + cancelLabel: "__('Duplicate product without images'); ?>", + cancel: duplicateSkipImages.bind(this), + }); + } + Event.observe(window, 'load', function() { var objName = 'getSelectedTabId() ?>'; if (objName) { diff --git a/app/locale/en_US/Mage_Adminhtml.csv b/app/locale/en_US/Mage_Adminhtml.csv index 0262cbb17b8..022a6b0aa5f 100644 --- a/app/locale/en_US/Mage_Adminhtml.csv +++ b/app/locale/en_US/Mage_Adminhtml.csv @@ -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 configuration.","{{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 configuration." "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" +"'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." From 9b040e682b8737f37e3b2dde0246c3b7bf721e66 Mon Sep 17 00:00:00 2001 From: michalis Date: Sun, 9 Nov 2025 08:40:39 +0200 Subject: [PATCH 2/2] trying not to introduce extra param --- app/code/core/Mage/Catalog/Model/Product.php | 4 ++- .../Mage/Catalog/Model/Resource/Product.php | 29 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/app/code/core/Mage/Catalog/Model/Product.php b/app/code/core/Mage/Catalog/Model/Product.php index a83b3296b46..5d8f3308c89 100644 --- a/app/code/core/Mage/Catalog/Model/Product.php +++ b/app/code/core/Mage/Catalog/Model/Product.php @@ -1444,7 +1444,9 @@ public function duplicate() $newProduct->save(); $this->getOptionInstance()->duplicate($this->getId(), $newProduct->getId()); - $this->getResource()->duplicate($this->getId(), $newProduct->getId(), $newProduct->getSkipImagesOnDuplicate()); + $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()) { diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product.php b/app/code/core/Mage/Catalog/Model/Resource/Product.php index 5cb14acea03..ea25d295769 100644 --- a/app/code/core/Mage/Catalog/Model/Resource/Product.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product.php @@ -28,6 +28,13 @@ class Mage_Catalog_Model_Resource_Product extends Mage_Catalog_Model_Resource_Ab */ protected $_productCategoryTable; + /** + * Used when duplicating product + * + * @var string + */ + protected $_skipImagesOnDuplicate = false; + /** * Initialize resource */ @@ -551,14 +558,14 @@ public function canBeShowInCategory($product, $categoryId) * @param int $newId * @return $this */ - public function duplicate($oldId, $newId, $skipImagesOnDuplicate = false) + public function duplicate($oldId, $newId) { $adapter = $this->_getWriteAdapter(); $eavTables = ['datetime', 'decimal', 'int', 'text', 'varchar']; $mediaImageAttributeSkipIds = []; $adapter = $this->_getWriteAdapter(); - if($skipImagesOnDuplicate){ + if($this->getSkipImagesOnDuplicate()){ /** * @var int $attributeId @@ -726,4 +733,22 @@ public function getCategoryIdsWithAnchors($object) return $this->_getReadAdapter()->fetchCol($select); } + + /** + * @param bool $newProductSkipImages + * @return $this + */ + public function setSkipImagesOnDuplicate(bool $newProductSkipImages){ + $this->_skipImagesOnDuplicate = $newProductSkipImages; + return $this; + } + + /** + * @return bool|string + */ + public function getSkipImagesOnDuplicate(){ + return $this->_skipImagesOnDuplicate; + } + + }