Commit afbd6f4f by lmf

增加后台产品批量操作插件

parent e84a492f
<?php
declare(strict_types=1);
namespace Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab;
use Amasty\Base\Model\MagentoVersion;
use Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice\Checkbox;
use Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice\Content;
use Magento\Backend\Block\Template\Context;
use Magento\Backend\Block\Widget\Tab\TabInterface;
use Magento\Catalog\Block\Adminhtml\Form;
use Magento\Framework\Data\FormFactory;
use Magento\Framework\Phrase;
use Magento\Framework\Registry;
class TierPrice extends Form implements TabInterface
{
const TIER_PRICE_CHANGE_CHECKBOX_NAME = 'tier_price_checkbox';
const TIER_PRICE_CHECKBOX_ID = 'id';
/**
* @var MagentoVersion
*/
private $magentoVersion;
public function __construct(
Context $context,
Registry $registry,
FormFactory $formFactory,
MagentoVersion $magentoVersion,
array $data = []
) {
parent::__construct($context, $registry, $formFactory, $data);
$this->magentoVersion = $magentoVersion;
}
/**
* Tab settings
*
* @return Phrase
*/
public function getTabLabel()
{
return $this->getTitleDependFromVersion();
}
/**
* @return Phrase
*/
public function getTabTitle()
{
return $this->getTitleDependFromVersion();
}
/**
* @return bool
*/
public function canShowTab()
{
return true;
}
/**
* @return bool
*/
public function isHidden()
{
return false;
}
/**
* @return Phrase
*/
private function getLegend()
{
return $this->getTitleDependFromVersion();
}
protected function _prepareForm()
{
/** @var \Magento\Framework\Data\Form $form */
$form = $this->_formFactory->create();
$form->setFieldNameSuffix('attributes');
$fieldset = $form->addFieldset('tiered_price', ['legend' => $this->getLegend()]);
$fieldset->addField(
'tier_price',
'text',
[
'name' => 'tier_price',
'class' => 'requried-entry',
'label' => $this->getTabLabel(),
'title' => $this->getTabTitle()
]
);
$form->getElement(
'tier_price'
)->setRenderer(
$this->getLayout()->createBlock(Content::class)
);
$this->setForm($form);
return parent::_prepareForm();
}
/**
* @param string $html
* @return string
*/
protected function _afterToHtml($html)
{
$tierPriceCheckboxHtml = $this->getLayout()->createBlock(
Checkbox::class,
'',
[
'data' => [
self::TIER_PRICE_CHECKBOX_ID => $this->getId()
]
]
)->toHtml();
return parent::_afterToHtml($html) . $tierPriceCheckboxHtml;
}
private function getTitleDependFromVersion(): Phrase
{
return version_compare($this->magentoVersion->get(), '2.2', '>=')
? __('Advanced Pricing')
: __('Tier Prices');
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice;
use Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice;
use Magento\Framework\View\Element\Template;
class Checkbox extends Template
{
/**
* @var string
*/
protected $_template = 'Amasty_Paction::tier_prices_checkbox.phtml';
public function getCheckboxElementName(): string
{
return TierPrice::TIER_PRICE_CHANGE_CHECKBOX_NAME;
}
public function getCheckboxElementId(): string
{
return 'toggle_' . $this->getData(TierPrice::TIER_PRICE_CHECKBOX_ID);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice;
class Content extends Group
{
/**
* @var string
*/
protected $_template = 'tier_prices.phtml';
public function getAllGroupsId(): array
{
return [$this->_groupManagement->getAllCustomersGroup()->getId() => __('ALL GROUPS')];
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice;
use Magento\Framework\Serialize\Serializer;
use Amasty\Paction\Model\Source\TierPrice as AmTierPrice;
use Magento\Backend\Block\Template\Context;
use Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Price\Group\AbstractGroup;
use Magento\Catalog\Model\Config\Source\Product\Options\TierPrice;
use Magento\Customer\Api\GroupManagementInterface;
use Magento\Customer\Api\GroupRepositoryInterface;
use Magento\Directory\Helper\Data;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Locale\CurrencyInterface;
use Magento\Framework\Module\Manager;
use Magento\Framework\Registry;
class Group extends AbstractGroup
{
/**
* @var TierPrice
*/
private $tierPriceValueType;
/**
* @var Serializer\Json
*/
private $serializer;
public function __construct(
Context $context,
GroupRepositoryInterface $groupRepository,
Data $directoryHelper,
Manager $moduleManager,
Registry $registry,
Serializer\Json $serializer,
GroupManagementInterface $groupManagement,
SearchCriteriaBuilder $searchCriteriaBuilder,
CurrencyInterface $localeCurrency,
AmTierPrice $tierPriceValueType,
array $data = []
) {
parent::__construct(
$context,
$groupRepository,
$directoryHelper,
$moduleManager,
$registry,
$groupManagement,
$searchCriteriaBuilder,
$localeCurrency,
$data
);
$this->tierPriceValueType = $tierPriceValueType;
$this->serializer = $serializer;
}
public function isScopeGlobal(): bool
{
return true;
}
public function getPriceValueTypesJson(): string
{
return $this->serializer->serialize($this->tierPriceValueType->toOptionArray());
}
public function getGroupsJson(): string
{
$allGroupId = $this->getAllGroupsId();
$groups = array_replace_recursive($allGroupId, $this->getCustomerGroups());
return $this->serializer->serialize($groups);
}
public function getWebsitesJson(): string
{
return $this->serializer->serialize($this::getWebsites());
}
public function getApplyToJson(): string
{
$element = $this->getElement();
$applyTo = $element->hasEntityAttribute()
? $element->getEntityAttribute()->getApplyTo()
: [];
return $this->serializer->serialize($applyTo);
}
}
<?php
namespace Amasty\Paction\Block\Adminhtml\System\Config;
use Magento\Framework\Data\Form\Element\AbstractElement;
class Information extends \Magento\Config\Block\System\Config\Form\Fieldset
{
/**
* @var string
*/
private $userGuide = 'https://amasty.com/docs/doku.php?id=magento_2:mass_product_actions';
/**
* @var array
*/
private $enemyExtensions = [];
/**
* @var string
*/
private $content;
/**
* Render fieldset html
*
* @param AbstractElement $element
* @return string
*/
public function render(AbstractElement $element)
{
$html = $this->_getHeaderHtml($element);
$this->setContent(__('Please update Amasty Base module. Re-upload it and replace all the files.'));
$this->_eventManager->dispatch(
'amasty_base_add_information_content',
['block' => $this]
);
$html .= $this->getContent();
$html .= $this->_getFooterHtml($element);
$html = str_replace(
'amasty_information]" type="hidden" value="0"',
'amasty_information]" type="hidden" value="1"',
$html
);
$html = preg_replace('(onclick=\"Fieldset.toggleCollapse.*?\")', '', $html);
return $html;
}
/**
* @return string
*/
public function getUserGuide()
{
return $this->userGuide;
}
/**
* @param string $userGuide
*/
public function setUserGuide($userGuide)
{
$this->userGuide = $userGuide;
}
/**
* @return array
*/
public function getEnemyExtensions()
{
return $this->enemyExtensions;
}
/**
* @param array $enemyExtensions
*/
public function setEnemyExtensions($enemyExtensions)
{
$this->enemyExtensions = $enemyExtensions;
}
/**
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* @param string $content
*/
public function setContent($content)
{
$this->content = $content;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Controller\Adminhtml\Massaction;
use Amasty\Paction\Model\CommandResolver;
use Amasty\Paction\Model\EntityResolver;
use Magento\Backend\App\Action\Context;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Controller\Adminhtml\Product;
use Magento\Catalog\Model\Indexer\Product\Price\Processor;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Ui\Component\MassAction\Filter;
class Index extends Product
{
const ADMIN_RESOURCE = 'Amasty_Paction::paction';
/**
* @var string[]
*/
private $entityIdRequiredActions = [
'addcategory',
'removecategory',
'replacecategory'
];
/**
* @var Processor
*/
protected $productPriceIndexerProcessor;
/**
* @var Filter
*/
protected $filter;
/**
* @var CollectionFactory
*/
protected $collectionFactory;
/**
* @var CommandResolver
*/
private $commandResolver;
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var StoreManagerInterface
*/
private $storeManager;
public function __construct(
Context $context,
Product\Builder $productBuilder,
Processor $productPriceIndexerProcessor,
Filter $filter,
CollectionFactory $collectionFactory,
CommandResolver $commandResolver,
EntityResolver $entityResolver,
StoreManagerInterface $storeManager
) {
parent::__construct($context, $productBuilder);
$this->filter = $filter;
$this->collectionFactory = $collectionFactory;
$this->productPriceIndexerProcessor = $productPriceIndexerProcessor;
$this->commandResolver = $commandResolver;
$this->entityResolver = $entityResolver;
$this->storeManager = $storeManager;
}
public function execute()
{
try {
list($actionValue, $action) = $this->prepareActionData();
if ($command = $this->commandResolver->getCommand($action)) {
$productIds = $this->getProductIds($this->isEntityIdRequiredAction($action));
$storeId = (int)$this->getRequest()->getParam('store', 0);
$this->storeManager->setCurrentStore($storeId);
$result = $command->execute($productIds, $storeId, $actionValue);
if ($result instanceof ResponseInterface) {
return $result;
}
$this->messageManager->addSuccessMessage($result);
// show non critical errors to the user
foreach ($command->getErrors() as $err) {
$this->messageManager->addErrorMessage($err);
}
$this->productPriceIndexerProcessor->reindexList($productIds);
}
} catch (LocalizedException $e) {
$this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
$this->messageManager->addExceptionMessage(
$e,
__('Something went wrong while updating the product(s) data.')
);
}
return $this->_redirect($this->_redirect->getRefererUrl());
}
protected function prepareActionData(): array
{
$action = $this->getRequest()->getParam('action');
$actionValue = $this->getRequest()->getParam('amasty_paction_field')
?? $this->getRequest()->getParam('amasty_file_field', '');
if (strpos($action, 'amasty_') === 0) {
$action = str_replace("amasty_", "", $action);
} else {
throw new LocalizedException(__('Something was wrong. Please try again.'));
}
return [trim($actionValue), $action];
}
protected function getProductIds($isEntityAction = false): array
{
$collection = $this->filter->getCollection($this->collectionFactory->create());
if ($idsFromRequest = $this->getRequest()->getParam('selected', 0)) {
$collection->addFieldToFilter('entity_id', ['IN' => $idsFromRequest]);
}
$entityLinkField = $isEntityAction
? 'entity_id'
: $this->entityResolver->getEntityLinkField(ProductInterface::class);
return $collection->getColumnValues($entityLinkField);
}
private function isEntityIdRequiredAction(string $action): bool
{
return in_array($action, $this->entityIdRequiredActions);
}
}
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* Created By : Rohan Hapani
*/
namespace Amasty\Paction\Model\Category;
use Magento\Catalog\Model\Category as CategoryModel;
class CategoryList implements \Magento\Framework\Option\ArrayInterface
{
/**
* @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory
*/
private $categoryCollectionFactory;
/**
* @param \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $collectionFactory
*/
public function __construct(
\Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $collectionFactory
) {
$this->categoryCollectionFactory = $collectionFactory;
}
/**
* Get list of categories
* @return array
*/
public function toOptionArray()
{
$collection = $this->categoryCollectionFactory->create();
$collection->addAttributeToSelect(['name', 'is_active', 'parent_id']);
$categoryById = [
CategoryModel::TREE_ROOT_ID => [
'value' => CategoryModel::TREE_ROOT_ID,
'optgroup' => null,
],
];
foreach ($collection as $category) {
foreach ([$category->getId(), $category->getParentId()] as $categoryId) {
if (!isset($categoryById[$categoryId])) {
$categoryById[$categoryId] = ['value' => $categoryId];
}
}
$categoryById[$category->getId()]['is_active'] = $category->getIsActive();
$categoryById[$category->getId()]['label'] = $category->getName();
$categoryById[$category->getParentId()]['optgroup'][] = &$categoryById[$category->getId()];
}
return $categoryById[CategoryModel::TREE_ROOT_ID]['optgroup'];
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model;
abstract class Command
{
/**
* @var string
*/
protected $type = '';
/**
* @var array
*/
protected $info = [];
/**
* @var array
*/
protected $errors = [];
abstract public function execute(array $ids, int $storeId, string $val);
public function getCreationData(): array
{
return $this->info;
}
public function getErrors(): array
{
return $this->errors;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Catalog\Model\ResourceModel\Category;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Phrase;
class Addcategory extends Command
{
const TYPE = 'addcategory';
/**
* @var ResourceConnection
*/
protected $resource;
/**
* @var Category\CollectionFactory
*/
private $categoryCollectionFactory;
/**
* @var CategoryRepositoryInterface
*/
private $categoryRepository;
/**
* @var EntityResolver
*/
private $entityResolver;
public function __construct(
ResourceConnection $resource,
Category\CollectionFactory $categoryCollectionFactory,
CategoryRepositoryInterface $categoryRepository,
EntityResolver $entityResolver
) {
$this->resource = $resource;
$this->categoryCollectionFactory = $categoryCollectionFactory;
$this->categoryRepository = $categoryRepository;
$this->entityResolver = $entityResolver;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Assign Category')->render(),
'confirm_message' => __('Are you sure you want to assign category?')->render(),
'type' => $this->type,
'label' => __('Assign Category')->render(),
'fieldLabel' => __('Category IDs')->render(),
'placeholder' => __('id1,id2,id3')->render()
];
}
public function execute(array $ids, int $storeId, string $categoryIds): Phrase
{
$categoryIds = $this->entityResolver->getEntityLinkIds(
CategoryInterface::class,
$this->prepareCategoryIds($categoryIds)
);
if ($this->type === 'replacecategory') { // remove product(s) from all categories
$table = $this->resource->getTableName('catalog_category_product');
$this->resource->getConnection()->delete($table, ['product_id IN(?)' => $ids]);
$this->type = 'addcategory';
}
$numAffectedCats = 0;
$allAffectedProducts = [];
$categoryIdField = $this->entityResolver->getEntityLinkField(CategoryInterface::class);
/** @var Category\Collection $categoriesCollection */
$categoriesCollection = $this->categoryCollectionFactory->create();
$categoriesCollection->addFieldToFilter($categoryIdField, ['in' => $categoryIds]);
$categoriesCollection->addNameToResult();
$categoriesCollection->addAttributeToSelect('*');
$categoriesCollection->setStoreId($storeId);
/** @var CategoryInterface $category */
foreach ($categoriesCollection as $category) {
$positions = $category->getProductsPosition();
$currentAffectedProducts = 0;
foreach ($ids as $productId) {
if ($this->type === 'addcategory' && !isset($positions[$productId])) { // add only new
$positions[$productId] = 0;
$allAffectedProducts[] = $productId;
$currentAffectedProducts++;
} elseif ($this->type === 'removecategory' && isset($positions[$productId])) { //remove only existing
unset($positions[$productId]);
$allAffectedProducts[] = $productId;
$currentAffectedProducts++;
}
}
if ($currentAffectedProducts) {
$category->setPostedProducts($positions);
try {
$category->save(); //category is reloaded in repository, loosing posted products
++$numAffectedCats;
$allAffectedProducts = array_unique($allAffectedProducts);
} catch (\Exception $e) {
$this->errors[] = __(
'Can not handle the category ID=%1, the error is: %2',
$category->getId(),
$e->getMessage()
);
}
}
}
return __(
'Total of %1 category(ies) and %2 product(s) have been successfully updated.',
$numAffectedCats,
count($allAffectedProducts)
);
}
protected function prepareCategoryIds(string $categoryIds): array
{
if (!$categoryIds) {
throw new LocalizedException(__('Please provide comma separated category IDs'));
}
$validCategoryIds = $validationErrors = [];
array_map(function ($categoryId) use (&$validCategoryIds, &$validationErrors) {
if ((int)$categoryId <= 1) {
$validationErrors[] = __('Magento2 does not allow to save the category ID=%1', $categoryId);
} else {
$validCategoryIds[] = $categoryId;
}
}, explode(',', $categoryIds));
$this->errors = array_merge($this->errors, $validationErrors);
return $validCategoryIds;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Store\Model\StoreManagerInterface;
class Addprice extends Modifyprice
{
const TYPE = 'addprice';
/**
* @var string
*/
protected $sourceAttributeCode = 'cost';
/**
* @var string
*/
protected $attributeCodeToModify = 'price';
public function __construct(
Config $eavConfig,
StoreManagerInterface $storeManager,
ResourceConnection $resource,
EntityResolver $entityResolver,
ConfigProvider $configProvider
) {
parent::__construct($eavConfig, $storeManager, $resource, $entityResolver, $configProvider);
$this->type = self::TYPE;
$this->info = array_merge($this->info, [
'confirm_title' => __('Modify Price using Cost')->render(),
'confirm_message' => __('Are you sure you want to modify price using cost?')->render(),
'type' => $this->type,
'label' => __('Modify Price using Cost')->render()
]);
}
protected function prepareQuery(string $table, string $value, array $where): string
{
$attributeId = $this->eavConfig
->getAttribute(Product::ENTITY, $this->attributeCodeToModify)
->getAttributeId();
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
$value = str_replace('`value`', 't.`value`', $value);
$fields = ['attribute_id', 'store_id', $entityIdName, 'value'];
$select = $this->connection->select()
->from(['t' => $table])
->reset('columns')
->columns([new \Zend_Db_expr((int)$attributeId), 'store_id', $entityIdName, new \Zend_Db_expr($value)])
->where('t.value > 0 ');
foreach ($where as $part) {
$select->where($part);
}
return $this->connection->insertFromSelect(
$select,
$table,
$fields,
AdapterInterface::INSERT_ON_DUPLICATE
);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Store\Model\StoreManagerInterface;
class Addspecial extends Addprice
{
const TYPE = 'addspecial';
/**
* @var string
*/
protected $sourceAttributeCode = 'price';
/**
* @var string
*/
protected $attributeCodeToModify = 'special_price';
public function __construct(
Config $eavConfig,
StoreManagerInterface $storeManager,
ResourceConnection $resource,
EntityResolver $entityResolver,
ConfigProvider $configProvider
) {
parent::__construct($eavConfig, $storeManager, $resource, $entityResolver, $configProvider);
$this->type = self::TYPE;
$this->info = array_merge($this->info, [
'confirm_title' => __('Modify Special Price using Price')->render(),
'confirm_message' => __('Are you sure you want to modify special price using price?')->render(),
'type' => $this->type,
'label' => __('Modify Special Price using Price')->render()
]);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Store\Model\StoreManagerInterface;
class Addspecialbycost extends Addspecial
{
const TYPE = 'addspecialbycost';
/**
* @var string
*/
protected $sourceAttributeCode = 'cost';
/**
* @var string
*/
protected $attributeCodeToModify = 'special_price';
public function __construct(
Config $eavConfig,
StoreManagerInterface $storeManager,
ResourceConnection $resource,
EntityResolver $entityResolver,
ConfigProvider $configProvider
) {
parent::__construct($eavConfig, $storeManager, $resource, $entityResolver, $configProvider);
$this->type = self::TYPE;
$this->info = array_merge($this->info, [
'confirm_title' => __('Modify Special Price using Cost')->render(),
'confirm_message' => __('Are you sure you want to modify special price using cost?')->render(),
'type' => $this->type,
'label' => __('Modify Special Price using Cost')->render()
]);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Phrase;
class Amdelete extends Command
{
const TYPE = 'amdelete';
/**
* @var AdapterInterface
*/
protected $connection;
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var ResourceConnection
*/
private $resource;
public function __construct(
ResourceConnection $resource,
EntityResolver $entityResolver
) {
$this->connection = $resource->getConnection();
$this->resource = $resource;
$this->entityResolver = $entityResolver;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Fast Delete')->render(),
'confirm_message' => __('Are you sure you want to apply Fast Delete?')->render(),
'type' => $this->type,
'label' => __('Fast Delete')->render(),
'fieldLabel' => ''
];
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
if (!$ids) {
throw new LocalizedException(__('Please select product(s)'));
}
// do the bulk delete skiping all _before/_after delete observers
// and indexing, as it cause thousands of queries in the
// getProductParentsByChild function
$table = $this->resource->getTableName('catalog_product_entity');
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
// foreign keys delete the rest
$this->connection->delete($table, $this->connection->quoteInto($entityIdName . ' IN(?)', $ids));
return __(
'Products have been successfully deleted. '
. 'We recommend to refresh indexes at the System > Index Management page.'
);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\Source\Append;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product\Attribute\Repository as ProductAttributeRepository;
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Phrase;
class Appendtext extends Command
{
const TYPE = 'appendtext';
const MODIFICATOR = '->';
const FIELD = 'value';
/**
* @var ProductAttributeRepository
*/
protected $productAttributeRepository;
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var ConfigProvider
*/
private $configProvider;
/**
* @var AdapterInterface
*/
private $connection;
/**
* @var ResourceConnection
*/
private $resource;
public function __construct(
ResourceConnection $resource,
ProductAttributeRepository $productAttributeRepository,
EntityResolver $entityResolver,
ConfigProvider $configProvider
) {
$this->resource = $resource;
$this->connection = $resource->getConnection();
$this->productAttributeRepository = $productAttributeRepository;
$this->entityResolver = $entityResolver;
$this->configProvider = $configProvider;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Append Text')->render(),
'confirm_message' => __('Are you sure you want to append text?')->render(),
'type' => $this->type,
'label' => __('Append Text')->render(),
'fieldLabel' => __('Append')->render(),
'placeholder' => __('attribute_code->text')->render()
];
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
$row = $this->generateAppend($val);
$this->appendText($row, $ids, $storeId);
return __('Total of %1 products(s) have been successfully updated.', count($ids));
}
protected function generateAppend(string $inputText): array
{
$modificatorPosition = stripos($inputText, self::MODIFICATOR);
if ($modificatorPosition === false) {
throw new LocalizedException(__('Field must contain "' . self::MODIFICATOR . '"'));
}
$value = trim(substr($inputText, 0, $modificatorPosition));
$text = substr(
$inputText,
(strlen($value) + strlen(self::MODIFICATOR)),
strlen($inputText)
);
return [$value, $text];
}
protected function appendText(array $searchReplace, array $ids, int $storeId): void
{
list($attributeCode, $appendText) = $searchReplace;
try {
$attribute = $this->productAttributeRepository->get($attributeCode);
} catch (NoSuchEntityException $e) {
throw new LocalizedException(__('There is no product attribute with code `%1`, ', $attributeCode));
}
$set = $this->addSetSql($this->connection->quote($appendText), $storeId, $attributeCode);
$table = $this->resource->getTableName('catalog_product_entity');
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
$conditions[$entityIdName . ' IN (?)'] = $ids;
if ($attribute->getBackendType() !== AbstractAttribute::TYPE_STATIC) {
$table = $this->resource->getTableName('catalog_product_entity_' . $attribute->getBackendType());
$conditions['store_id = ?'] = $storeId;
$conditions['attribute_id = ?'] = $attribute->getAttributeId();
}
$this->connection->update(
$table,
$set,
$conditions
);
}
protected function addSetSql(string $appendText, int $storeId, string $attributeCode): array
{
$field = $attributeCode == 'sku' ? 'sku' : self::FIELD;
$position = $this->configProvider->getAppendTextPosition($storeId);
if ($position == Append::POSITION_BEFORE) {
$firstPart = $appendText;
$secondPart = $field;
} else {
$firstPart = $field;
$secondPart = $appendText;
}
return [$field => new \Zend_Db_Expr(sprintf(' CONCAT(%s, %s)', $firstPart, $secondPart))];
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Eav\Api\AttributeSetRepositoryInterface;
use Magento\Eav\Model\Config;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Phrase;
class Changeattributeset extends Command
{
const TYPE = 'changeattributeset';
/**
* @var Config
*/
protected $eavConfig;
/**
* @var ProductRepositoryInterface
*/
private $productRepository;
/**
* @var AttributeSetRepositoryInterface
*/
private $attributeSetRepository;
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var GetProductCollectionByIds
*/
private $getProductCollectionByIds;
public function __construct(
Config $eavConfig,
ProductRepositoryInterface $productRepository,
AttributeSetRepositoryInterface $attributeSetRepository,
EntityResolver $entityResolver,
GetProductCollectionByIds $getProductCollectionByIds
) {
$this->eavConfig = $eavConfig;
$this->productRepository = $productRepository;
$this->attributeSetRepository = $attributeSetRepository;
$this->entityResolver = $entityResolver;
$this->getProductCollectionByIds = $getProductCollectionByIds;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Change Attribute Set')->render(),
'confirm_message' => __('Are you sure you want to change attribute set?')->render(),
'type' => $this->type,
'label' => __('Change Attribute Set')->render(),
'fieldLabel' => __('To')->render(),
'placeholder' => __('Attribute Set Id')->render()
];
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
$fromId = (int)trim($val);
if (!$fromId) {
throw new LocalizedException(__('Please provide a valid Attribute Group ID'));
}
$attributeSet = $this->attributeSetRepository->get($fromId);
$productEntityId = $this->eavConfig->getEntityType(Product::ENTITY)->getId();
if ($attributeSet->getEntityTypeId() != $productEntityId) {
throw new LocalizedException(__('Provided Attribute set non product Attribute set.'));
}
$num = $configurable = 0;
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
foreach ($this->getProductCollectionByIds->get($ids, $entityIdName) as $product) {
try {
if ($product->getTypeId() == Configurable::TYPE_CODE) {
$configurable++;
} else {
$product->setAttributeSetId($fromId)
->setStoreId($storeId)
->setIsMassupdate(true);
$this->productRepository->save($product);
++$num;
}
} catch (\Exception $e) {
$this->errors[] = __(
'Can not change the attribute set for product ID %1, error is: %2',
$product->getId(),
$e->getMessage()
);
}
}
if ($configurable) {
$this->errors[] = __(
'Total of %1 products(s) have not been updated, the reason: '
. 'impossibility to change attribute set for configurable product',
$configurable
);
}
return __('Total of %1 products(s) have been successfully updated.', $num);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\ProductRepository;
use Magento\Framework\Phrase;
class Changevisibility extends Command
{
const TYPE = 'changevisibility';
/**
* @var ProductRepository
*/
private $productRepository;
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var GetProductCollectionByIds
*/
private $getProductCollectionByIds;
public function __construct(
ProductRepository $productRepository,
EntityResolver $entityResolver,
GetProductCollectionByIds $getProductCollectionByIds
) {
$this->productRepository = $productRepository;
$this->entityResolver = $entityResolver;
$this->getProductCollectionByIds = $getProductCollectionByIds;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Change Visibility')->render(),
'confirm_message' => __('Are you sure you want to change visibility?')->render(),
'type' => $this->type,
'label' => __('Change Visibility')->render(),
'fieldLabel' => __('To')->render()
];
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
$visibility = (int)trim($val);
$num = 0;
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
foreach ($this->getProductCollectionByIds->get($ids, $entityIdName) as $product) {
try {
$product->setStoreId($storeId)->setVisibility($visibility);
$product->save();
++$num;
} catch (\Exception $e) {
$this->errors[] = __(
'Can not change visibility for product ID %1, error is: %2',
$product->getId(),
$e->getMessage()
);
}
}
return __('Total of %1 products(s) have been successfully updated.', $num);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Phrase;
class Copyattr extends Command
{
const TYPE = 'copyattr';
/**
* @var AdapterInterface
*/
protected $connection;
/**
* @var ProductRepositoryInterface
*/
private $productRepository;
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var ConfigProvider
*/
private $configProvider;
/**
* @var ResourceConnection
*/
private $resource;
public function __construct(
ResourceConnection $resource,
ProductRepositoryInterface $productRepository,
EntityResolver $entityResolver,
ConfigProvider $configProvider
) {
$this->connection = $resource->getConnection();
$this->resource = $resource;
$this->productRepository = $productRepository;
$this->entityResolver = $entityResolver;
$this->configProvider = $configProvider;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Copy Attributes')->render(),
'confirm_message' => __('Are you sure you want to copy attributes?')->render(),
'type' => $this->type,
'label' => __('Copy Attributes')->render(),
'fieldLabel' => __('From')->render(),
'placeholder' => __('ID of product')->render()
];
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
$fromId = (int)trim($val);
try {
$fromProduct = $this->productRepository->getById($fromId, false, $storeId);
} catch (NoSuchEntityException $e) {
throw new LocalizedException(__('Please provide a valid product ID'));
}
if (!$fromProduct->getId()) {
throw new LocalizedException(__('Please provide a valid product ID'));
}
$fromId = $this->entityResolver->getEntityLinkIds(ProductInterface::class, [$fromId]);
if (isset($fromId[0])) {
$fromId = (int)$fromId[0];
}
if (!$fromId) {
throw new LocalizedException(__('Please provide a valid product ID'));
}
if (in_array($fromId, $ids)) {
throw new LocalizedException(__('Please remove source product from the selected products'));
}
$codes = $this->configProvider->getCopyAttributes($storeId);
if (!$codes) {
throw new LocalizedException(__('Please set attribute codes in the module configuration'));
}
$config = [];
foreach ($codes as $code) {
$code = trim($code);
/** @var \Magento\Eav\Model\Entity\Attribute $attribute */
$attribute = $fromProduct->getResource()->getAttribute($code);
if (!$attribute || !$attribute->getId()) {
$message = __(
'There is no product attribute with code `%1`, '
. 'please compare values in the module configuration with stores > attributes > product.',
$code
);
throw new LocalizedException($message);
}
if ($attribute->getIsUnique()) {
$message = __(
'Attribute `%1` is unique and can not be copied. '
. 'Please remove the code in the module configuration.',
$code
);
throw new LocalizedException($message);
}
$type = $attribute->getBackendType();
if ($type === AbstractAttribute::TYPE_STATIC) {
$message = __(
'Attribute `%1` is static and can not be copied. '
. 'Please remove the code in the module configuration.',
$code
);
throw new LocalizedException($message);
}
if (!isset($config[$type])) {
$config[$type] = [];
}
$config[$type][] = $attribute->getId();
}
// we do not use store id as it is global action
$this->copyData($fromId, $ids, $config);
return __('Attributes have been successfully copied.');
}
protected function copyData(int $fromId, array $ids, array $config): void
{
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
foreach ($config as $type => $attributes) {
if (!$attributes) {
continue;
}
$table = $this->resource->getTableName('catalog_product_entity_' . $type);
$fields = ['attribute_id', 'store_id', $entityIdName, 'value'];
foreach ($ids as $id) {
$data = $this->connection->select()
->from(['t' => $table])
->reset('columns')
->columns(['attribute_id', 'store_id', new \Zend_Db_Expr((int)$id), 'value'])
->where("t.$entityIdName = ?", $fromId)
->where('t.attribute_id IN(?)', $attributes);
$this->connection->query(
$this->connection->insertFromSelect(
$data,
$table,
$fields,
AdapterInterface::INSERT_ON_DUPLICATE
)
);
}
}
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Amasty\Paction\Model\LinkActionsManagement;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\ProductRepository;
class Copycrosssell extends Copyrelate
{
const TYPE = 'copycrosssell';
public function __construct(
ProductRepository $productRepository,
LinkActionsManagement $linkActionsManagement,
GetProductCollectionByIds $getProductCollectionByIds,
EntityResolver $entityResolver
) {
parent::__construct(
$productRepository,
$linkActionsManagement,
$getProductCollectionByIds,
$entityResolver
);
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Copy Cross-sells')->render(),
'confirm_message' => __('Are you sure you want to copy cross-sells?')->render(),
'type' => $this->type,
'label' => __('Copy Cross-sells')->render(),
'fieldLabel' => __('From')->render()
];
}
protected function getLinks(ProductInterface $product): array
{
return $product->getCrossSellProducts();
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Media\Config as MediaConfig;
use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory;
use Magento\Catalog\Model\ResourceModel\Product\Gallery;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Filesystem;
use Magento\Framework\Phrase;
use Magento\MediaStorage\Helper\File\Storage\Database;
use Magento\MediaStorage\Model\File\Uploader;
class Copyimg extends Removeimg
{
const TYPE = 'copyimg';
/**
* @var Database
*/
private $fileStorageDb;
public function __construct(
CollectionFactory $attributeCollectionFactory,
Config $eavConfig,
ProductRepositoryInterface $productRepository,
ResourceConnection $resource,
Filesystem $filesystem,
MediaConfig $mediaConfig,
EntityResolver $entityResolver,
Database $database,
GetProductCollectionByIds $getProductCollectionByIds
) {
parent::__construct(
$attributeCollectionFactory,
$eavConfig,
$productRepository,
$resource,
$filesystem,
$mediaConfig,
$entityResolver,
$getProductCollectionByIds
);
$this->fileStorageDb = $database;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Copy Images')->render(),
'confirm_message' => __('Are you sure you want to copy images?')->render(),
'type' => $this->type,
'label' => __('Copy Images')->render(),
'fieldLabel' => __('From')->render(),
'placeholder' => __('Product ID')->render()
];
}
public function execute(array $productIds, int $storeId, string $val): Phrase
{
if (!$productIds) {
throw new LocalizedException(__('Please select product(s)'));
}
$fromProductId = (int)trim($val);
try {
$fromProduct = $this->productRepository->getById($fromProductId);
} catch (NoSuchEntityException $e) {
throw new LocalizedException(__('Please provide a valid product ID'));
}
$fromProductId = $this->entityResolver->getEntityLinkIds(ProductInterface::class, [$fromProductId]);
if (isset($fromProductId[0])) {
$fromProductId = (int)$fromProductId[0];
}
if (!$fromProductId) {
throw new LocalizedException(__('Please provide a valid product ID'));
}
if (in_array($fromProductId, $productIds)) {
throw new LocalizedException(__('Please remove source product from the selected products'));
}
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
if (!$fromProduct->getData($entityIdName)) {
throw new LocalizedException(__('Please provide a valid product ID'));
}
$entityTypeId = (int)$this->eavConfig->getEntityType(Product::ENTITY)->getId();
$mediaGalleryAttribute = $this->eavConfig->getAttribute($entityTypeId, parent::MEDIA_GALLERY_ATTRIBUTE_CODE);
$mediaGalleryAttributeId = (int)$mediaGalleryAttribute->getId();
$parentImageAttributeIds = $this->getAttributeIdsBySetId((int)$fromProduct->getAttributeSetId(), $entityTypeId);
// we do not use store id as it is a global action
foreach ($this->getProductCollectionByIds->get($productIds, $entityIdName) as $product) {
$imageAttributeIds = $this->getAttributeIdsBySetId((int)$product->getAttributeSetId(), $entityTypeId);
$imageAttributeIds = array_values(array_intersect($parentImageAttributeIds, $imageAttributeIds));
$isCopied = $this->copyData(
$mediaGalleryAttributeId,
(int)$fromProduct->getData($entityIdName),
(int)$product->getData($entityIdName),
$imageAttributeIds,
$entityIdName
);
if (!$isCopied) {
$this->errors[] = __('Can not copy images to product with ID %1', $product->getId());
}
}
return __('Images and labels has been successfully copied');
}
private function copyData(
int $mediaGalleryAttributeId,
int $originalProductId,
int $newProductId,
array $imageAttributeIds,
string $entityIdName
): bool {
$mediaGalleryTable = $this->resource->getTableName(Gallery::GALLERY_TABLE);
$mediaGalleryValueToEntityTable = $this->resource->getTableName(Gallery::GALLERY_VALUE_TO_ENTITY_TABLE);
$productVarcharTable = $this->resource->getTableName(parent::PRODUCT_ENTITY_VARCHAR_TABLE);
$newPic = [];
// definition picture path
foreach ($imageAttributeIds as $key => $attributeId) {
$newPic[$key] = 'no_selection';
}
$select = $this->connection->select()
->from($mediaGalleryTable, ['value_id', 'value'])
->joinInner(
['entity' => $mediaGalleryValueToEntityTable],
$mediaGalleryTable . '.value_id = entity.value_id',
[$entityIdName => $entityIdName]
)
->where('attribute_id = ?', $mediaGalleryAttributeId)
->where($entityIdName . ' = ?', $originalProductId);
$selectPicFields = ['value', 'store_id'];
$valueIdMap = [];
// select old position of basic, small and thumb
foreach ($imageAttributeIds as $key => $attributeId) {
$selectPic[$key] = $this->connection->select()
->from($productVarcharTable, $selectPicFields)
->where('attribute_id = ?', $attributeId)
->where($entityIdName . ' = ?', $originalProductId);
$picOrig[$key] = $this->connection->fetchRow($selectPic[$key])
?: array_fill_keys($selectPicFields, null);
$storeId[$key] = $picOrig[$key]['store_id'];
$selectPicId[$key] = $this->connection->select()
->from($mediaGalleryTable, 'value_id')
->where('value = ?', $picOrig[$key]['value']);
$picId[$key] = $this->connection->fetchCol($selectPicId[$key]);
}
// Duplicate main entries of gallery
foreach ($this->connection->fetchAll($select) as $row) {
try {
$imagePath = $this->copyImage($row['value']);
} catch (FileSystemException $e) {
$imgFilePath = $this->mediaConfig->getMediaPath($row['value']);
$this->errors[] = __('Can not copy image file: %1', $imgFilePath);
continue;
}
$data = [
'attribute_id' => $mediaGalleryAttributeId,
'value' => $imagePath,
];
$this->connection->insert($mediaGalleryTable, $data);
$valueIdMap[$row['value_id']] = $this->connection->lastInsertId($mediaGalleryTable);
$this->connection->insert($mediaGalleryValueToEntityTable, [
'value_id' => $valueIdMap[$row['value_id']],
$entityIdName => $newProductId
]);
// compare old position of basic, small and thumb with current copied picture
foreach ($imageAttributeIds as $key => $attributeId) {
if (in_array($row['value_id'], $picId[$key])) {
$newPic[$key] = $imagePath;
}
}
}
if (!$valueIdMap) {
return false;
}
// Duplicate per store gallery values
$galleryValueTable = $this->resource->getTableName(Gallery::GALLERY_VALUE_TABLE);
$select = $this->connection->select()
->from($galleryValueTable)
->where('value_id IN(?)', array_keys($valueIdMap));
foreach ($this->connection->fetchAll($select) as $row) {
$data = $row;
$data['value_id'] = $valueIdMap[$row['value_id']];
$data[$entityIdName] = $newProductId;
unset($data['record_id']);
$this->connection->insert($galleryValueTable, $data);
}
// update basic, small and thumb
foreach ($imageAttributeIds as $key => $attributeId) {
if ($newPic[$key] !== 'no_selection') {
$data = ['value' => $newPic[$key]];
$where = [
'attribute_id = ?' => $attributeId,
$entityIdName . ' = ?' => $newProductId
];
$update = $this->connection->update($productVarcharTable, $data, $where);
if (!$update && $storeId[$key] !== null) {
$dataToInsert = [
'attribute_id' => $attributeId,
$entityIdName => $newProductId,
'value' => $newPic[$key],
'store_id' => $storeId[$key]
];
$this->connection->insert($productVarcharTable, $dataToInsert);
}
}
}
return true;
}
private function getUniqueFileName(string $file): string
{
if ($this->fileStorageDb->checkDbUsage()) {
$destinationFile = $this->fileStorageDb->getUniqueFilename(
$this->mediaConfig->getBaseMediaUrlAddition(),
$file
);
} else {
$destinationFile = $this->mediaDirectoryWrite->getAbsolutePath($this->mediaConfig->getMediaPath($file));
$destinationFile = $this->mediaDirectoryWrite->getDriver()->getParentDirectory($file)
. '/' . Uploader::getNewFileName($destinationFile);
}
return $destinationFile;
}
private function copyImage(string $file): string
{
if (!$this->mediaDirectoryWrite->isFile($this->mediaConfig->getMediaPath($file))) {
throw new FileSystemException(__('Image not found.'));
}
$destinationFile = $this->getUniqueFileName($file);
if ($this->fileStorageDb->checkDbUsage()) {
$this->fileStorageDb->copyFile(
$this->mediaDirectoryWrite->getAbsolutePath($this->mediaConfig->getMediaShortUrl($file)),
$this->mediaConfig->getMediaShortUrl($destinationFile)
);
$this->mediaDirectoryWrite->delete($this->mediaConfig->getMediaPath($destinationFile));
} else {
$this->mediaDirectoryWrite->copyFile(
$this->mediaConfig->getMediaPath($file),
$this->mediaConfig->getMediaPath($destinationFile)
);
}
return str_replace('\\', '/', $destinationFile);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Magento\Catalog\Api\Data\ProductCustomOptionInterface;
use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory;
use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Option;
use Magento\Catalog\Model\Product\Option\Value;
use Magento\Catalog\Model\Product\OptionFactory;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Phrase;
class Copyoptions extends Command
{
const TYPE = 'copyoptions';
/**
* @var ProductRepositoryInterface
*/
private $productRepository;
/**
* @var OptionFactory
*/
private $optionFactory;
/**
* @var ProductCustomOptionInterfaceFactory
*/
private $productCustomOptionInterfaceFactory;
/**
* @var ProductCustomOptionValuesInterfaceFactory
*/
private $productCustomOptionValuesInterfaceFactory;
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var GetProductCollectionByIds
*/
private $getProductCollectionByIds;
/**
* @var array
*/
private $parentOptions = [];
/**
* @var array
*/
private $customOptionTypes = [
ProductCustomOptionInterface::OPTION_TYPE_FIELD,
ProductCustomOptionInterface::OPTION_TYPE_AREA,
ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN,
ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX,
ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE,
ProductCustomOptionInterface::OPTION_TYPE_RADIO,
ProductCustomOptionInterface::OPTION_TYPE_FILE,
ProductCustomOptionInterface::OPTION_TYPE_DATE,
ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME,
ProductCustomOptionInterface::OPTION_TYPE_TIME
];
/**
* @var array
*/
private $typesWithOptions = [
ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN,
ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX,
ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE,
ProductCustomOptionInterface::OPTION_TYPE_RADIO
];
/**
* @var array
*/
private $commonKeys = [
Option::KEY_TYPE,
Option::KEY_TITLE,
Option::KEY_IS_REQUIRE,
Option::KEY_SORT_ORDER,
'is_delete',
'previous_type',
'previous_group',
'values'
];
/**
* @var array
*/
private $priceKeys = [
Option::KEY_PRICE_TYPE,
Option::KEY_PRICE,
Option::KEY_SKU
];
/**
* @var array
*/
private $fileKeys = [
Option::KEY_FILE_EXTENSION,
Option::KEY_IMAGE_SIZE_X,
Option::KEY_IMAGE_SIZE_Y
];
/**
* @var array
*/
private $txtKeys = [Option::KEY_MAX_CHARACTERS];
/**
* @var array
*/
private $optionValueKeys = [
Value::KEY_TITLE,
Value::KEY_SORT_ORDER,
Value::KEY_PRICE_TYPE,
Value::KEY_PRICE,
Value::KEY_SKU,
'is_delete'
];
public function __construct(
ProductRepositoryInterface $productRepository,
OptionFactory $optionFactory,
ProductCustomOptionInterfaceFactory $productCustomOptionInterfaceFactory,
ProductCustomOptionValuesInterfaceFactory $productCustomOptionValuesInterfaceFactory,
EntityResolver $entityResolver,
GetProductCollectionByIds $getProductCollectionByIds
) {
$this->productRepository = $productRepository;
$this->optionFactory = $optionFactory;
$this->productCustomOptionInterfaceFactory = $productCustomOptionInterfaceFactory;
$this->productCustomOptionValuesInterfaceFactory = $productCustomOptionValuesInterfaceFactory;
$this->entityResolver = $entityResolver;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Copy Custom Options')->render(),
'confirm_message' => __('Are you sure you want to copy custom options?')->render(),
'type' => $this->type,
'label' => __('Copy Custom Options')->render(),
'fieldLabel' => __('From')->render(),
'placeholder' => __('Product ID')->render()
];
$this->getProductCollectionByIds = $getProductCollectionByIds;
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
if (empty($ids)) {
throw new LocalizedException(__('Please select product(s)'));
}
$parentProductId = (int)trim($val);
if (!$parentProductId) {
throw new LocalizedException(__('Please provide a valid product ID'));
}
$parentProduct = $this->productRepository->getById($parentProductId);
$this->parentOptions = $this->getOptionsAsArray($parentProduct);
if (empty($this->parentOptions)) {
throw new LocalizedException(__('Please provide a product with custom options'));
}
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
$num = 0;
/** @var Product $product */
foreach ($this->getProductCollectionByIds->get($ids, $entityIdName) as $product) {
if ($parentProductId == $product->getId()) {
continue;
}
try {
foreach ($this->parentOptions as $option) {
/** @var ProductCustomOptionInterface $customOption */
$customOption = $this->productCustomOptionInterfaceFactory->create(['data' => $option]);
$customOption->setProductSku($product->getSku());
if (isset($option['values'])) {
$customOption->setValues($this->getOptionValues($option['values']));
}
$product->addOption($customOption);
}
$product->setIsMassupdate(true);
$product->setExcludeUrlRewrite(true);
$product->setCanSaveCustomOptions(true);
$product->setHasOptions(true);
$this->productRepository->save($product);
++$num;
} catch (\Exception $error) {
$this->errors[] = __(
'Can not copy the options to the product ID=%1, the error is: %2',
$product->getId(),
$error->getMessage()
);
}
}
return __('Total of %1 products(s) have been successfully updated.', $num);
}
private function getOptionValues(array $optionValues): array
{
$values = [];
foreach ($optionValues as $value) {
if (!$value['price_type']) {
$value['price_type'] = ProductPriceOptionsInterface::VALUE_FIXED;
}
$value = $this->productCustomOptionValuesInterfaceFactory->create(['data' => $value]);
$values[] = $value;
}
return $values;
}
private function getOptionsAsArray(Product $product): array
{
$collection = $this->optionFactory->create()->getProductOptionCollection($product);
$options = [];
foreach ($collection as $option) {
if (in_array($option->getType(), $this->customOptionTypes)) {
$options[] = $this->convertToArray($option);
}
}
return $options;
}
private function convertToArray(Option $option): array
{
$type = $option->getType();
switch ($type) {
case ProductCustomOptionInterface::OPTION_TYPE_FILE:
$optionKeys = array_merge($this->commonKeys, $this->priceKeys, $this->fileKeys);
break;
case ProductCustomOptionInterface::OPTION_TYPE_FIELD:
case ProductCustomOptionInterface::OPTION_TYPE_AREA:
$optionKeys = array_merge($this->commonKeys, $this->priceKeys, $this->txtKeys);
break;
case ProductCustomOptionInterface::OPTION_TYPE_DATE:
case ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME:
case ProductCustomOptionInterface::OPTION_TYPE_TIME:
$optionKeys = array_merge($this->commonKeys, $this->priceKeys);
break;
default:
$optionKeys = $this->commonKeys;
}
$optionAsArray = $option->toArray($optionKeys);
if (in_array($type, $this->typesWithOptions)) {
$optionAsArray['values'] = [];
foreach ($option->getValues() as $value) {
$optionAsArray['values'][] = $value->toArray($this->optionValueKeys);
}
}
return $optionAsArray;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Amasty\Paction\Model\LinkActionsManagement;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\ProductRepository;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Phrase;
class Copyrelate extends Command
{
const TYPE = 'copyrelate';
/**
* @var ProductRepository
*/
private $productRepository;
/**
* @var LinkActionsManagement
*/
private $linkActionsManagement;
/**
* @var GetProductCollectionByIds
*/
private $getProductCollectionByIds;
/**
* @var EntityResolver
*/
private $entityResolver;
public function __construct(
ProductRepository $productRepository,
LinkActionsManagement $linkActionsManagement,
GetProductCollectionByIds $getProductCollectionByIds,
EntityResolver $entityResolver
) {
$this->productRepository = $productRepository;
$this->linkActionsManagement = $linkActionsManagement;
$this->getProductCollectionByIds = $getProductCollectionByIds;
$this->entityResolver = $entityResolver;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Copy Relations')->render(),
'confirm_message' => __('Are you sure you want to copy relations?')->render(),
'type' => $this->type,
'label' => __('Copy Relations')->render(),
'fieldLabel' => __('From')->render()
];
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
$fromId = (int)trim($val);
if (!$fromId) {
throw new LocalizedException(__('Please provide a valid product ID'));
}
if (in_array($fromId, $ids)) {
throw new LocalizedException(__('Please remove source product from the selected products'));
}
$relatedProducts = $this->getLinks($this->productRepository->getById($fromId));
if (empty($relatedProducts)) {
throw new LocalizedException(__('Source product has no relations'));
}
$mainProducts = $this->getProductCollectionByIds->get(
$ids,
$this->entityResolver->getEntityLinkField(ProductInterface::class)
);
$num = 0;
foreach ($mainProducts as $mainProduct) {
foreach ($relatedProducts as $relatedProduct) {
if ($mainProduct->getId() === $relatedProduct->getId()) {
continue;
}
$this->linkActionsManagement->createNewLink(
$mainProduct,
$relatedProduct,
$this->linkActionsManagement->getLinkType($this->type)
);
$num++;
}
}
if ($num === 1) {
$success = __('Product association has been successfully added.');
} else {
$success = __('%1 product associations have been successfully added.', $num);
}
return $success;
}
protected function getLinks(ProductInterface $product): array
{
return $product->getRelatedProducts();
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Amasty\Paction\Model\LinkActionsManagement;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\ProductRepository;
class Copyupsell extends Copyrelate
{
const TYPE = 'copyupsell';
public function __construct(
ProductRepository $productRepository,
LinkActionsManagement $linkActionsManagement,
GetProductCollectionByIds $getProductCollectionByIds,
EntityResolver $entityResolver
) {
parent::__construct(
$productRepository,
$linkActionsManagement,
$getProductCollectionByIds,
$entityResolver
);
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Copy Up-sells')->render(),
'confirm_message' => __('Are you sure you want to copy up-sells?')->render(),
'type' => $this->type,
'label' => __('Copy Up-sells')->render(),
'fieldLabel' => __('From')->render()
];
}
protected function getLinks(ProductInterface $product): array
{
return $product->getUpSellProducts();
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Amasty\Paction\Model\LinkActionsManagement;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product\Link\SaveHandler;
use Magento\Framework\App\ResourceConnection;
class Crosssell extends Relate
{
const TYPE = 'crosssell';
public function __construct(
ProductRepositoryInterface $productRepository,
SaveHandler $saveProductLinks,
ResourceConnection $resource,
ConfigProvider $configProvider,
EntityResolver $entityResolver,
LinkActionsManagement $linkActionsManagement,
GetProductCollectionByIds $getProductCollectionByIds
) {
parent::__construct(
$productRepository,
$saveProductLinks,
$resource,
$configProvider,
$entityResolver,
$linkActionsManagement,
$getProductCollectionByIds
);
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Cross-sell')->render(),
'confirm_message' => __('Are you sure you want to cross-sell?')->render(),
'type' => $this->type,
'label' => __('Cross-sell')->render(),
'fieldLabel' => __('Selected To IDs')->render(),
'placeholder' => __('id1,id2,id3')->render()
];
$this->setFieldLabel();
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory;
use Magento\Eav\Api\Data\AttributeInterface;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManagerInterface;
class Modifyallprices extends Modifyprice
{
const TYPE = 'modifyallprices';
/**
* @var ResourceConnection
*/
private $resource;
/**
* @var CollectionFactory
*/
private $collectionFactory;
public function __construct(
Config $eavConfig,
StoreManagerInterface $storeManager,
ResourceConnection $resource,
EntityResolver $entityResolver,
ConfigProvider $configProvider,
CollectionFactory $collectionFactory
) {
parent::__construct($eavConfig, $storeManager, $resource, $entityResolver, $configProvider);
$this->resource = $resource;
$this->collectionFactory = $collectionFactory;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Update All Types of Prices')->render(),
'confirm_message' => __('Are you sure you want to update all types of prices?')->render(),
'type' => $this->type,
'label' => __('Update All Types of Prices')->render(),
'fieldLabel' => __('By')->render(),
'placeholder' => __('+12.5, -12.5, +12.5%')->render()
];
}
protected function updateAttribute(string $attrCode, array $productIds, int $storeId, string $value): void
{
$priceCodes = [];
$attributes = $this->collectionFactory->create()
->addVisibleFilter()
->addFieldToFilter(AttributeInterface::FRONTEND_INPUT, 'price');
foreach ($attributes as $attribute) {
$priceCodes[$attribute->getId()] = $attribute->getAttributeCode();
}
$attribute = $this->eavConfig->getAttribute('catalog_product', 'price');
$table = $attribute->getBackend()->getTable();
$entityIdName = $this->connection->quoteIdentifier(
$this->entityResolver->getEntityLinkField(ProductInterface::class)
);
$where = [
$this->connection->quoteInto($entityIdName . ' IN(?)', $productIds),
$this->connection->quoteInto('attribute_id IN(?)', array_keys($priceCodes))
];
$defaultStoreId = Store::DEFAULT_STORE_ID;
$storeIds = [];
if ($storeId && $defaultStoreId != $storeId) {
$storeIds = $this->storeManager->getStore($storeId)->getWebsite()->getStoreIds(true);
} else { // all stores
$stores = $this->storeManager->getStores(true);
foreach ($stores as $store) {
$storeIds[] = $store->getId();
}
}
$where[] = $this->connection->quoteInto('store_id IN(?)', $storeIds);
$where[] = new \Zend_Db_Expr('value IS NOT NULL');
// update all price attributes
$sql = $this->prepareQuery($table, $value, $where);
$this->connection->query($sql);
//update tier price
$websiteId = $this->storeManager->getStore($storeId)->getWebsite()->getId();
$where = [
$this->connection->quoteInto($entityIdName . ' IN(?)', $productIds),
$this->connection->quoteInto('website_id = ?', $websiteId)
];
$table = $this->resource->getTableName('catalog_product_entity_tier_price');
$sql = $this->prepareQuery($table, $value, $where);
$this->connection->query($sql);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Store\Model\StoreManagerInterface;
class Modifycost extends Modifyprice
{
const TYPE = 'modifycost';
/**
* @var string
*/
protected $sourceAttributeCode = 'cost';
public function __construct(
Config $eavConfig,
StoreManagerInterface $storeManager,
ResourceConnection $resource,
EntityResolver $entityResolver,
ConfigProvider $configProvider
) {
parent::__construct($eavConfig, $storeManager, $resource, $entityResolver, $configProvider);
$this->type = self::TYPE;
$this->info = array_merge($this->info, [
'confirm_title' => __('Update Cost')->render(),
'confirm_message' => __('Are you sure you want to update cost?')->render(),
'type' => $this->type,
'label' => __('Update Cost')->render()
]);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\Source\Rounding;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Phrase;
use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManagerInterface;
class Modifyprice extends Command
{
const TYPE = 'modifyprice';
/**
* Attribute code used as "source" attribute for price modification
*
* @var string
*/
protected $sourceAttributeCode = 'price';
/**
* @var Config
*/
protected $eavConfig;
/**
* @var StoreManagerInterface
*/
protected $storeManager;
/**
* @var AdapterInterface
*/
protected $connection;
/**
* @var EntityResolver
*/
protected $entityResolver;
/**
* @var ConfigProvider
*/
protected $configProvider;
public function __construct(
Config $eavConfig,
StoreManagerInterface $storeManager,
ResourceConnection $resource,
EntityResolver $entityResolver,
ConfigProvider $configProvider
) {
$this->eavConfig = $eavConfig;
$this->storeManager = $storeManager;
$this->connection = $resource->getConnection();
$this->entityResolver = $entityResolver;
$this->configProvider = $configProvider;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Update Price')->render(),
'confirm_message' => __('Are you sure you want to update price?')->render(),
'type' => $this->type,
'label' => __('Update Price')->render(),
'fieldLabel' => __('By')->render(),
'placeholder' => __('+12.5, -12.5, +12.5%')->render()
];
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
if (!preg_match('/^[+-][0-9]+(\.[0-9]+)?%?$/', $val)) {
throw new LocalizedException(__('Please provide the difference as +12.5, -12.5, +12.5% or -12.5%'));
}
$sign = substr($val, 0, 1);
$val = substr($val, 1);
$percent = ('%' == substr($val, -1, 1));
if ($percent) {
$val = (float)substr($val, 0, -1);
}
if ($val <= 0) {
throw new LocalizedException(__('Please provide a non empty difference'));
}
$value = $this->prepareValue(['sign' => $sign, 'val' => $val, 'percent' => $percent], $storeId);
$this->updateAttribute(
$this->sourceAttributeCode,
$ids,
$storeId,
$value
);
return __('Total of %1 products(s) have been successfully updated', count($ids));
}
protected function updateAttribute(string $attrCode, array $productIds, int $storeId, string $value): void
{
$attribute = $this->eavConfig->getAttribute(Product::ENTITY, $attrCode);
$table = $attribute->getBackend()->getTable();
$entityIdName = $this->connection->quoteIdentifier(
$this->entityResolver->getEntityLinkField(ProductInterface::class)
);
$where = [
$this->connection->quoteInto($entityIdName . ' IN(?)', $productIds),
$this->connection->quoteInto('attribute_id=?', $attribute->getAttributeId()),
];
/**
* If we work in single store mode all values should be saved just
* for default store id. In this case we clear all not default values
*/
$defaultStoreId = Store::DEFAULT_STORE_ID;
if ($this->storeManager->isSingleStoreMode()) {
$this->connection->delete(
$table,
implode(' AND ', array_merge($where, [$this->connection->quoteInto('store_id <> ?', $defaultStoreId)]))
);
}
$storeIds = [];
if ($attribute->isScopeStore()) {
$where[] = $this->connection->quoteInto('store_id = ?', $storeId);
$storeIds[] = $storeId;
} elseif ($attribute->isScopeWebsite() && $storeId != $defaultStoreId) {
$storeIds = $this->storeManager->getStore($storeId)->getWebsite()->getStoreIds(true);
$where[] = $this->connection->quoteInto('store_id IN(?)', $storeIds);
} else {
$where[] = $this->connection->quoteInto('store_id = ?', $defaultStoreId);
}
// in case of store-view or website scope we need to insert default values
// first, to be able to update them.
if ($storeIds) {
$cond = [
$this->connection->quoteInto('t.' . $entityIdName . ' IN(?)', $productIds),
$this->connection->quoteInto('t.attribute_id=?', $attribute->getAttributeId()),
't.store_id = ' . (int)$defaultStoreId,
];
foreach ($storeIds as $id) {
$id = $this->connection->quote($id);
$fields = ['value_id', 'attribute_id', 'store_id', new \Zend_Db_Expr($entityIdName), 'value'];
$select = $this->connection->select()
->from(['t' => $table])
->reset('columns')
->columns([
'value_id',
'attribute_id',
new \Zend_Db_Expr((int)$id),
new \Zend_Db_Expr($entityIdName),
'value'
]);
foreach ($cond as $part) {
$select->where($part);
}
$this->connection->query(
$this->connection->insertFromSelect(
$select,
$table,
$fields,
AdapterInterface::INSERT_IGNORE
)
);
}
}
$sql = $this->prepareQuery($table, $value, $where);
$this->connection->query($sql);
}
protected function prepareValue(array $diff, int $storeId): string
{
$value = $diff['percent'] ? '`value` * ' . $diff['val'] . '/ 100' : $diff['val'];
$value = '`value`' . $diff['sign'] . $value;
$rounding = $this->configProvider->getPriceRoundingType($storeId);
switch ($rounding) {
case Rounding::FIXED:
$fixed = $this->configProvider->getRoundingValue($storeId);
if (!empty($fixed)) {
$fixed = (float)$fixed;
$value = 'FLOOR(' . $value . ') + ' . $fixed;
}
break;
case Rounding::CROP:
$value = 'TRUNCATE(' . $value . ',2)';
break;
case Rounding::NEAREST_INT:
$value = 'ROUND(' . $value . ')';
break;
default: // Rounding::MATH
$value = 'ROUND(' . $value . ',2)';
}
return $value;
}
protected function prepareQuery(string $table, string $value, array $where): string
{
$table = $this->connection->quoteIdentifier($table);
$value = new \Zend_Db_Expr($value);
// phpcs:ignore: Magento2.SQL.RawQuery.FoundRawSql
return "UPDATE $table SET `value` = $value WHERE " . implode(' AND ', $where);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Store\Model\StoreManagerInterface;
class Modifyspecial extends Modifyprice
{
const TYPE = 'modifyspecial';
/**
* @var string
*/
protected $sourceAttributeCode = 'special_price';
public function __construct(
Config $eavConfig,
StoreManagerInterface $storeManager,
ResourceConnection $resource,
EntityResolver $entityResolver,
ConfigProvider $configProvider
) {
parent::__construct($eavConfig, $storeManager, $resource, $entityResolver, $configProvider);
$this->type = self::TYPE;
$this->info = array_merge($this->info, [
'confirm_title' => __('Update Special Price')->render(),
'confirm_message' => __('Are you sure you want to update special price?')->render(),
'type' => $this->type,
'label' => __('Update Special Price')->render()
]);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Amasty\Paction\Model\LinkActionsManagement;
use Amasty\Paction\Model\Source\Direction;
use Amasty\Paction\Model\Source\LinkType;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product\Link\SaveHandler;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Phrase;
class Relate extends Command
{
const TYPE = 'related';
/**
* @var ProductRepositoryInterface
*/
protected $productRepository;
/**
* @var SaveHandler
*/
protected $saveProductLinks;
/**
* @var AdapterInterface
*/
protected $connection;
/**
* @var ConfigProvider
*/
private $configProvider;
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var LinkActionsManagement
*/
private $linkActionsManagement;
/**
* @var GetProductCollectionByIds
*/
private $getProductCollectionByIds;
public function __construct(
ProductRepositoryInterface $productRepository,
SaveHandler $saveProductLinks,
ResourceConnection $resource,
ConfigProvider $configProvider,
EntityResolver $entityResolver,
LinkActionsManagement $linkActionsManagement,
GetProductCollectionByIds $getProductCollectionByIds
) {
$this->productRepository = $productRepository;
$this->saveProductLinks = $saveProductLinks;
$this->connection = $resource->getConnection();
$this->configProvider = $configProvider;
$this->entityResolver = $entityResolver;
$this->linkActionsManagement = $linkActionsManagement;
$this->getProductCollectionByIds = $getProductCollectionByIds;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Relate')->render(),
'confirm_message' => __('Are you sure you want to relate?')->render(),
'type' => $this->type,
'label' => __('Relate')->render(),
'fieldLabel' => __('Selected To IDs')->render(),
'placeholder' => __('id1,id2,id3')->render()
];
$this->setFieldLabel();
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
if (!$ids) {
throw new LocalizedException(__('Please select product(s)'));
}
$vals = explode(',', $val);
$num = 0;
/** @var ProductInterface[] $mainProducts */
$mainProducts = $this->getProductCollectionByIds->get(
$ids,
$this->entityResolver->getEntityLinkField(ProductInterface::class)
);
/** @var ProductInterface[] $targetProducts */
$targetProducts = $this->getProductCollectionByIds->get($vals);
$linkType = $this->linkActionsManagement->getLinkType($this->type);
switch ($this->configProvider->getLinkType($this->type)) {
case LinkType::MULTI_WAY:
foreach ($mainProducts as $mainProduct) {
foreach ($mainProducts as $targetProduct) {
if ($mainProduct->getId() === $targetProduct->getId()) {
continue;
}
$this->linkActionsManagement->createNewLink($mainProduct, $targetProduct, $linkType);
$num++;
}
}
break;
case LinkType::TWO_WAY:
foreach ($targetProducts as $targetProduct) {
foreach ($mainProducts as $mainProduct) {
$this->linkActionsManagement->createNewLink($targetProduct, $mainProduct, $linkType);
$num++;
$this->linkActionsManagement->createNewLink($mainProduct, $targetProduct, $linkType);
$num++;
}
}
break;
case LinkType::DEFAULT:
foreach ($targetProducts as $targetProduct) {
foreach ($mainProducts as $mainProduct) {
if ($this->configProvider->getLinkDirection($this->type) == Direction::IDS_TO_SELECTED) {
$this->linkActionsManagement->createNewLink($mainProduct, $targetProduct, $linkType);
} else {
$this->linkActionsManagement->createNewLink($targetProduct, $mainProduct, $linkType);
}
$num++;
}
}
break;
}
if ($num === 1) {
$success = __('Product association has been successfully added.');
} else {
$success = __('%1 product associations have been successfully added.', $num);
}
if (!$success && $this->configProvider->getLinkType($this->type) == LinkType::MULTI_WAY) {
$this->errors[] = __('Please select more than 1 product');
}
return $success;
}
protected function setFieldLabel(): void
{
if ($this->configProvider->getLinkType($this->type) == LinkType::DEFAULT) {
if ($this->configProvider->getLinkDirection($this->type) == Direction::IDS_TO_SELECTED) {
$this->info['fieldLabel'] = 'IDs to Selected'; // new option
} else {
$this->info['fieldLabel'] = 'Selected To IDs'; // old option
}
} elseif ($this->configProvider->getLinkType($this->type) == LinkType::MULTI_WAY) {
$this->info['fieldLabel'] = '';
$this->info['hide_input'] = 1;
}
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Model\ResourceModel\Category;
use Magento\Framework\App\ResourceConnection;
class Removecategory extends Addcategory
{
const TYPE = 'removecategory';
public function __construct(
ResourceConnection $resource,
Category\CollectionFactory $categoryCollectionFactory,
CategoryRepositoryInterface $categoryRepository,
EntityResolver $entityResolver
) {
parent::__construct($resource, $categoryCollectionFactory, $categoryRepository, $entityResolver);
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Remove Category')->render(),
'confirm_message' => __('Are you sure you want to remove category?')->render(),
'type' => $this->type,
'label' => __('Remove Category')->render(),
'fieldLabel' => __('Category IDs')->render(),
'placeholder' => __('id1,id2,id3')->render()
];
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Media\Config as MediaConfig;
use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory;
use Magento\Catalog\Model\ResourceModel\Product\Gallery;
use Magento\Eav\Model\Config;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem;
use Magento\Framework\Phrase;
class Removeimg extends Command
{
const MEDIA_GALLERY_ATTRIBUTE_CODE = 'media_gallery';
const PRODUCT_ENTITY_VARCHAR_TABLE = 'catalog_product_entity_varchar';
const TYPE = 'removeimg';
/**
* @var CollectionFactory
*/
protected $attributeCollectionFactory;
/**
* @var Config
*/
protected $eavConfig;
/**
* @var ProductRepositoryInterface
*/
protected $productRepository;
/**
* @var AdapterInterface
*/
protected $connection;
/**
* @var Filesystem\Directory\WriteInterface
*/
protected $mediaDirectoryWrite;
/**
* @var MediaConfig
*/
protected $mediaConfig;
/**
* @var EntityResolver
*/
protected $entityResolver;
/**
* @var ResourceConnection
*/
protected $resource;
/**
* @var GetProductCollectionByIds
*/
protected $getProductCollectionByIds;
/**
* @var array
*/
protected $imgAttributeIdsBySetId = [];
public function __construct(
CollectionFactory $attributeCollectionFactory,
Config $eavConfig,
ProductRepositoryInterface $productRepository,
ResourceConnection $resource,
Filesystem $filesystem,
MediaConfig $mediaConfig,
EntityResolver $entityResolver,
GetProductCollectionByIds $getProductCollectionByIds
) {
$this->attributeCollectionFactory = $attributeCollectionFactory;
$this->eavConfig = $eavConfig;
$this->productRepository = $productRepository;
$this->connection = $resource->getConnection();
$this->resource = $resource;
$this->mediaDirectoryWrite = $filesystem->getDirectoryWrite(DirectoryList::MEDIA);
$this->mediaConfig = $mediaConfig;
$this->entityResolver = $entityResolver;
$this->getProductCollectionByIds = $getProductCollectionByIds;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Remove Images')->render(),
'confirm_message' => __('Are you sure you want to remove images?')->render(),
'type' => $this->type,
'label' => __('Remove Images')->render(),
'fieldLabel' => ''
];
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
if (empty($ids)) {
throw new LocalizedException(__('Please select product(s)'));
}
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
$entityTypeId = (int)$this->eavConfig->getEntityType(Product::ENTITY)->getId();
$mediaGalleryAttribute = $this->eavConfig->getAttribute($entityTypeId, self::MEDIA_GALLERY_ATTRIBUTE_CODE);
$mediaGalleryAttributeId = (int)$mediaGalleryAttribute->getId();
// we do not use store ID as it is a global action
foreach ($this->getProductCollectionByIds->get($ids, $entityIdName) as $product) {
$imageAttributeIds = $this->getAttributeIdsBySetId((int)$product->getAttributeSetId(), $entityTypeId);
$this->removeDataAndFiles(
$mediaGalleryAttributeId,
(int)$product->getData($entityIdName),
$imageAttributeIds,
$entityIdName
);
}
return __('Images and labels has been successfully deleted');
}
private function removeDataAndFiles(
int $mediaGalleryAttributeId,
int $productId,
array $imageAttributeIds,
string $entityIdName
): void {
$mediaGalleryTable = $this->resource->getTableName(Gallery::GALLERY_TABLE);
$quotedEntityIdName = $this->resource->getConnection()->quoteIdentifier($entityIdName);
// Delete varchar
foreach ($imageAttributeIds as $attributeId) {
$this->connection->delete(
$this->resource->getTableName(self::PRODUCT_ENTITY_VARCHAR_TABLE),
[
'attribute_id = ?' => $attributeId,
$quotedEntityIdName . ' = ?' => $productId
]
);
}
$select = $this->connection->select()
->from($mediaGalleryTable, ['value_id', 'value'])
->joinInner(
['entity' => $this->resource->getTableName(Gallery::GALLERY_VALUE_TO_ENTITY_TABLE)],
$mediaGalleryTable . '.value_id = entity.value_id',
[$entityIdName => $entityIdName]
)
->where('attribute_id = ?', $mediaGalleryAttributeId)
->where($quotedEntityIdName . ' = ?', $productId);
$valueIds = [];
// Delete files
foreach ($this->connection->fetchAll($select) as $row) {
$imgFilePath = $this->mediaDirectoryWrite
->getAbsolutePath($this->mediaConfig->getMediaShortUrl($row['value']));
if ($this->mediaDirectoryWrite->isFile($imgFilePath)) {
try {
$this->mediaDirectoryWrite->delete($imgFilePath);
} catch (FileSystemException $e) {
$this->errors[] = __('Can not delete image file: %1', $imgFilePath);
}
} else {
$this->errors[] = __('%1 is not a file', $imgFilePath);
}
$valueIds[] = $row['value_id'];
}
// Delete media
$this->connection->delete(
$mediaGalleryTable,
$this->connection->quoteInto('value_id IN(?)', $valueIds)
);
// Delete labels
$this->connection->delete(
$this->resource->getTableName(Gallery::GALLERY_VALUE_TABLE),
$this->connection->quoteInto('value_id IN(?)', $valueIds)
);
}
protected function getAttributeIdsBySetId(int $attributeSetId, int $entityTypeId): array
{
if (array_key_exists($attributeSetId, $this->imgAttributeIdsBySetId)) {
return $this->imgAttributeIdsBySetId[$attributeSetId];
}
$imgAttributeIds = [];
/* @var $collection \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection */
$collection = $this->attributeCollectionFactory->create();
$collection->setEntityTypeFilter($entityTypeId)
->setAttributeSetFilter($attributeSetId)
->setFrontendInputTypeFilter('media_image');
foreach ($collection as $attribute) {
/* @var $attribute \Magento\Eav\Model\Entity\Attribute */
$imgAttributeIds[] = $attribute->getId();
}
$this->imgAttributeIdsBySetId[$attributeSetId] = $imgAttributeIds;
return $imgAttributeIds;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface;
use Magento\Framework\Phrase;
class Removeoptions extends Command
{
const TYPE = 'removeoptions';
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var ProductCustomOptionRepositoryInterface
*/
private $customOptionRepository;
/**
* @var GetProductCollectionByIds
*/
private $getProductCollectionByIds;
public function __construct(
EntityResolver $entityResolver,
ProductCustomOptionRepositoryInterface $customOptionRepository,
GetProductCollectionByIds $getProductCollectionByIds
) {
$this->entityResolver = $entityResolver;
$this->customOptionRepository = $customOptionRepository;
$this->getProductCollectionByIds = $getProductCollectionByIds;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Remove Custom Options')->render(),
'confirm_message' => __('Are you sure you want to remove custom options?')->render(),
'type' => $this->type,
'label' => __('Remove Custom Options')->render(),
'fieldLabel' => ''
];
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
$num = 0;
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
foreach ($this->getProductCollectionByIds->get($ids, $entityIdName) as $product) {
try {
$options = $product->getOptions();
if (empty($options)) {
continue;
}
foreach ($options as $option) {
$this->customOptionRepository->delete($option);
}
++$num;
} catch (\Exception $e) {
$this->errors[] = __(
'Can not remove the options to the product ID=%1, the error is: %2',
$product->getId(),
$e->getMessage()
);
}
}
return __('Total of %1 products(s) have been successfully updated.', $num);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\EntityResolver;
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Model\ResourceModel\Category;
use Magento\Framework\App\ResourceConnection;
class Replacecategory extends Addcategory
{
const TYPE = 'replacecategory';
public function __construct(
ResourceConnection $resource,
Category\CollectionFactory $categoryCollectionFactory,
CategoryRepositoryInterface $categoryRepository,
EntityResolver $entityResolver
) {
parent::__construct($resource, $categoryCollectionFactory, $categoryRepository, $entityResolver);
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Replace Category')->render(),
'confirm_message' => __('Are you sure you want to replace category?')->render(),
'type' => $this->type,
'label' => __('Replace Category')->render(),
'fieldLabel' => __('Category IDs')->render(),
'placeholder' => __('id1,id2,id3')->render()
];
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\Eav\Model\Config;
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Phrase;
class Replacetext extends Command
{
const REPLACE_MODIFICATOR = '->';
const REPLACE_FIELD = 'value';
const TYPE = 'replacetext';
/**
* @var Config
*/
protected $eavConfig;
/**
* @var AdapterInterface
*/
private $connection;
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var ConfigProvider
*/
private $configProvider;
/**
* @var ResourceConnection
*/
private $resource;
public function __construct(
Config $eavConfig,
ResourceConnection $resource,
EntityResolver $entityResolver,
ConfigProvider $configProvider
) {
$this->eavConfig = $eavConfig;
$this->connection = $resource->getConnection();
$this->entityResolver = $entityResolver;
$this->configProvider = $configProvider;
$this->resource = $resource;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Replace Text')->render(),
'confirm_message' => __('Are you sure you want to replace text?')->render(),
'type' => $this->type,
'label' => __('Replace Text')->render(),
'fieldLabel' => __('Replace')->render(),
'placeholder' => __('search->replace')->render()
];
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
$searchReplace = $this->generateReplaces($val);
$this->searchAndReplace($searchReplace, $ids, $storeId);
return __('Total of %1 products(s) have been successfully updated.', count($ids));
}
protected function generateReplaces(string $inputText): array
{
$modificatorPosition = stripos($inputText, self::REPLACE_MODIFICATOR);
if ($modificatorPosition === false) {
throw new LocalizedException(__('Replace field must contain: search->replace'));
}
$search = trim(
substr($inputText, 0, $modificatorPosition)
);
$replace = trim(
substr(
$inputText,
(strlen($search) + strlen(self::REPLACE_MODIFICATOR)),
strlen($inputText)
)
);
return [$search, $replace];
}
protected function searchAndReplace(array $searchReplace, array $ids, int $storeId): void
{
list($search, $replace) = $searchReplace;
$attrGroups = $this->getAttrGroups();
$table = $this->resource->getTableName('catalog_product_entity');
$entityIdName = $this->entityResolver->getEntityLinkField(ProductInterface::class);
$set = [];
$conditions[$entityIdName . ' IN (?)'] = $ids;
foreach ($attrGroups as $backendType => $attrIds) {
if ($backendType === AbstractAttribute::TYPE_STATIC) {
foreach ($attrIds as $attrId => $attrName) {
$set[$attrName] = $this->getSetSql($attrName, $search, $replace);
}
} else {
$table = $this->resource->getTableName('catalog_product_entity_' . $backendType);
$set[self::REPLACE_FIELD] = $this->getSetSql(self::REPLACE_FIELD, $search, $replace);
$conditions['store_id = ?'] = $storeId;
$conditions['attribute_id IN (?)'] = array_keys($attrIds);
}
$this->connection->update(
$table,
$set,
$conditions
);
}
}
protected function getSetSql(string $attrName, string $search, string $replace): \Zend_Db_expr
{
return new \Zend_Db_expr(sprintf(
'REPLACE(`%s`, %s, %s)',
$attrName,
$this->connection->quote($search),
$this->connection->quote($replace)
));
}
protected function getAttrGroups(): array
{
$productAttributes = $this->configProvider->getReplaceAttributes();
$attrGroups = [];
foreach ($productAttributes as $item) {
$attribute = $this->eavConfig->getAttribute(Product::ENTITY, $item);
$attrGroups[$attribute->getBackendType()][$attribute->getId()] = $attribute->getName();
}
return $attrGroups;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\LinkActionsManagement;
use Magento\Framework\App\ResourceConnection;
class Uncrosssell extends Unrelate
{
const TYPE = 'uncrosssell';
public function __construct(ResourceConnection $resource, LinkActionsManagement $linkActionsManagement)
{
parent::__construct($resource, $linkActionsManagement);
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Remove Cross-Sells')->render(),
'confirm_message' => __('Are you sure you want to remove cross-Sells?')->render(),
'type' => $this->type,
'label' => __('Remove Cross-Sells')->render(),
'fieldLabel' => __('Select Algorithm')->render()
];
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Amasty\Paction\Model\LinkActionsManagement;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Phrase;
class Unrelate extends Command
{
const TYPE = 'unrelated';
/**
* @var AdapterInterface
*/
protected $connection;
/**
* @var ResourceConnection
*/
private $resource;
/**
* @var LinkActionsManagement
*/
private $linkActionsManagement;
public function __construct(
ResourceConnection $resource,
LinkActionsManagement $linkActionsManagement
) {
$this->connection = $resource->getConnection();
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Remove Relations')->render(),
'confirm_message' => __('Are you sure you want to remove relations?')->render(),
'type' => $this->type,
'label' => __('Remove Relations')->render(),
'fieldLabel' => __('Select Algorithm')->render()
];
$this->resource = $resource;
$this->linkActionsManagement = $linkActionsManagement;
}
public function execute(array $ids, int $storeId, string $val): Phrase
{
if (!$ids) {
throw new LocalizedException(__('Please select product(s)'));
}
$table = $this->resource->getTableName('catalog_product_link');
switch ($val) {
case 1: // between selected
$where = [
'product_id IN(?)' => $ids,
'linked_product_id IN(?)' => $ids,
];
break;
case 2: // selected products from all
$where = [
'linked_product_id IN(?)' => $ids,
];
break;
default: // Remove all relations from selected products
$where = [
'product_id IN(?)' => $ids,
];
}
$this->connection->delete(
$table,
array_merge($where, ['link_type_id = ?' => $this->linkActionsManagement->getLinkTypeId($this->type)])
);
return __('Product associations have been successfully deleted.');
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\LinkActionsManagement;
use Magento\Framework\App\ResourceConnection;
class Unupsell extends Unrelate
{
const TYPE = 'unupsell';
public function __construct(ResourceConnection $resource, LinkActionsManagement $linkActionsManagement)
{
parent::__construct($resource, $linkActionsManagement);
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Remove Up-sells')->render(),
'confirm_message' => __('Are you sure you want to remove up-sells?')->render(),
'type' => $this->type,
'label' => __('Remove Up-sells')->render(),
'fieldLabel' => __('Select Algorithm')->render()
];
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\Command;
use Magento\Backend\Model\UrlInterface;
use Magento\Catalog\Helper\Product\Edit\Action\Attribute;
use Magento\Framework\App\Response\Http;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Exception\LocalizedException;
class Updateadvancedprices extends Command
{
const TYPE = 'updateadvancedprices';
/**
* @var Http
*/
private $response;
/**
* @var UrlInterface
*/
private $url;
/**
* @var Attribute
*/
private $attributeHelper;
public function __construct(
Http $response,
UrlInterface $url,
Attribute $attributeHelper
) {
$this->response = $response;
$this->url = $url;
$this->attributeHelper = $attributeHelper;
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Update Advanced Prices')->render(),
'confirm_message' => __('Are you sure you want to update prices?')->render(),
'type' => $this->type,
'label' => __('Update Advanced Prices')->render(),
'fieldLabel' => ''
];
}
public function execute(array $ids, int $storeId, string $val): ResponseInterface
{
if (!$ids) {
throw new LocalizedException(__('Please select product(s)'));
}
$url = $this->url->getUrl(
'catalog/product_action_attribute/edit',
['_current' => true, 'active_tab' => 'tier_prices']
);
$this->attributeHelper->setProductIds($ids);
return $this->response->setRedirect($url);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Command;
use Amasty\Paction\Model\ConfigProvider;
use Amasty\Paction\Model\EntityResolver;
use Amasty\Paction\Model\GetProductCollectionByIds;
use Amasty\Paction\Model\LinkActionsManagement;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product\Link\SaveHandler;
use Magento\Framework\App\ResourceConnection;
class Upsell extends Relate
{
const TYPE = 'upsell';
public function __construct(
ProductRepositoryInterface $productRepository,
SaveHandler $saveProductLinks,
ResourceConnection $resource,
ConfigProvider $configProvider,
EntityResolver $entityResolver,
LinkActionsManagement $linkActionsManagement,
GetProductCollectionByIds $getProductCollectionByIds
) {
parent::__construct(
$productRepository,
$saveProductLinks,
$resource,
$configProvider,
$entityResolver,
$linkActionsManagement,
$getProductCollectionByIds
);
$this->type = self::TYPE;
$this->info = [
'confirm_title' => __('Up-sell')->render(),
'confirm_message' => __('Are you sure you want to up-sell?')->render(),
'type' => $this->type,
'label' => __('Up-sell')->render(),
'placeholder' => __('id1,id2,id3')->render(),
'fieldLabel' => ''
];
$this->setFieldLabel();
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model;
use Magento\Backend\Model\Url;
class CommandResolver
{
/**
* @var array
*/
protected $commands = [];
/**
* @var string
*/
protected $actionUrl;
public function __construct(
Url $urlBuilder,
array $commands = []
) {
$this->actionUrl = $urlBuilder->getUrl('amasty_paction/massaction/index');
$this->commands = $commands;
}
public function getCommand(string $name): ?Command
{
return $this->commands[$name] ?? null;
}
public function getCommandDataByName(string $name): array
{
/* initialization for delimiter lines*/
$data = [
'confirm_title' => '',
'confirm_message' => '',
'type' => $name,
'label' => '------------',
'url' => '',
'fieldLabel' => ''
];
if ($command = $this->getCommand($name)) {
$data = $command->getCreationData();
$data['url'] = $this->actionUrl;
}
return $data;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model;
use Amasty\Base\Model\ConfigProviderAbstract;
class ConfigProvider extends ConfigProviderAbstract
{
protected $pathPrefix = 'amasty_paction/';
const COMMANDS = 'general/commands';
const PRICE_ROUNDING_TYPE = 'general/round';
const ROUNDING_VALUE = 'general/fixed';
const COPY_ATTRIBUTES = 'general/attr';
const REPLACE_IN_ATTRIBUTES = 'general/replace_in_attr';
const APPEND_TEXT_POSITION = 'general/append_text_position';
const RELATE_TYPE = 'links/related';
const RELATE_DIRECTION = 'links/related_reverse';
const UPSELL_TYPE = 'links/upsell';
const UPSELL_DIRECTION = 'links/upsell_reverse';
const CROSSELL_TYPE = 'links/crosssell';
const CROSSELL_DIRECTION = 'links/crosssell_reverse';
public function getCommands($storeId = null): array
{
$commands = [];
if ($value = $this->getValue(self::COMMANDS, $storeId)) {
$commands = explode(',', $value);
}
return $commands;
}
public function getPriceRoundingType($storeId = null): string
{
return $this->getValue(self::PRICE_ROUNDING_TYPE, $storeId);
}
public function getRoundingValue($storeId = null): float
{
return (float)$this->getValue(self::ROUNDING_VALUE, $storeId);
}
public function getCopyAttributes($storeId = null): array
{
$attributes = [];
if ($value = $this->getValue(self::COPY_ATTRIBUTES, $storeId)) {
$attributes = explode(',', $value);
}
return $attributes;
}
public function getReplaceAttributes($storeId = null): array
{
$attributes = [];
if ($value = $this->getValue(self::REPLACE_IN_ATTRIBUTES, $storeId)) {
$attributes = explode(',', $value);
}
return $attributes;
}
public function getAppendTextPosition($storeId = null): string
{
return $this->getValue(self::APPEND_TEXT_POSITION, $storeId);
}
public function getLinkType(string $link, $storeId = null): ?int
{
switch ($link) {
case 'related':
$type = (int)$this->getValue(self::RELATE_TYPE, $storeId);
break;
case 'upsell':
$type = (int)$this->getValue(self::UPSELL_TYPE, $storeId);
break;
case 'crosssell':
$type = (int)$this->getValue(self::CROSSELL_TYPE, $storeId);
break;
default:
$type = null;
}
return $type;
}
public function getLinkDirection(string $link, $storeId = null): ?int
{
switch ($link) {
case 'related':
$direction = (int)$this->getValue(self::RELATE_DIRECTION, $storeId);
break;
case 'upsell':
$direction = (int)$this->getValue(self::UPSELL_DIRECTION, $storeId);
break;
case 'crosssell':
$direction = (int)$this->getValue(self::CROSSELL_DIRECTION, $storeId);
break;
default:
$direction = null;
}
return $direction;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model;
use Magento\Framework\EntityManager\EntityMetadataInterface;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
class EntityResolver
{
/**
* @var MetadataPool
*/
private $metadataPool;
/**
* @var AdapterInterface
*/
private $connection;
/**
* @var ResourceConnection
*/
private $resource;
public function __construct(
MetadataPool $metadataPool,
ResourceConnection $resource
) {
$this->metadataPool = $metadataPool;
$this->connection = $resource->getConnection();
$this->resource = $resource;
}
public function getEntityLinkField(string $entityType): string
{
return $this->getEntityMetadata($entityType)->getLinkField();
}
public function getEntityLinkIds(string $entityType, array $ids): array
{
if ($this->getEntityLinkField($entityType) === 'entity_id') {
return $ids;
}
$tableName = $this->getEntityMetadata($entityType)->getEntityTable();
$select = $this->connection->select()
->from($tableName, ['row_id'])
->where('entity_id IN (?)', $ids);
return $this->connection->fetchCol($select);
}
private function getEntityMetadata(string $entityType): EntityMetadataInterface
{
return $this->metadataPool->getMetadata($entityType);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model;
use Magento\Catalog\Model\ProductRepository;
use Magento\Framework\Api\SearchCriteriaBuilderFactory;
class GetProductCollectionByIds
{
/**
* @var SearchCriteriaBuilderFactory
*/
private $searchCriteriaBuilderFactory;
/**
* @var EntityResolver
*/
private $entityResolver;
/**
* @var ProductRepository
*/
private $productRepository;
public function __construct(
SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory,
EntityResolver $entityResolver,
ProductRepository $productRepository
) {
$this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory;
$this->entityResolver = $entityResolver;
$this->productRepository = $productRepository;
}
public function get(array $ids, string $entityField = 'entity_id')
{
/** @var \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder */
$criteriaBuilder = $this->searchCriteriaBuilderFactory->create();
$searchCriteria = $criteriaBuilder->addFilter(
$entityField,
$ids,
'IN'
)->create();
return $this->productRepository->getList($searchCriteria)->getItems();
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory;
use Magento\Catalog\Model\Product\Link;
use Magento\Catalog\Model\Product\Link\SaveHandler;
class LinkActionsManagement
{
/**
* @var ProductLinkInterfaceFactory
*/
private $productLinkInterfaceFactory;
/**
* @var SaveHandler
*/
private $saveProductLinks;
public function __construct(
ProductLinkInterfaceFactory $productLinkInterfaceFactory,
SaveHandler $saveProductLinks
) {
$this->productLinkInterfaceFactory = $productLinkInterfaceFactory;
$this->saveProductLinks = $saveProductLinks;
}
public function createNewLink(ProductInterface $mainProduct, ProductInterface $linkedProduct, string $type): void
{
/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLinks */
$productLinks = $this->productLinkInterfaceFactory->create();
$linkDataAll = $mainProduct->getProductLinks();
$linkData = $productLinks
->setSku($mainProduct->getSku())
->setLinkedProductSku($linkedProduct->getSku())
->setLinkType($type);
$linkDataAll[] = $linkData;
$mainProduct->setProductLinks($linkDataAll);
$this->saveProductLinks->execute(ProductInterface::class, $mainProduct);
}
public function getLinkTypeId(string $action): ?int
{
$types = [
'copycrosssell' => Link::LINK_TYPE_CROSSSELL,
'crosssell' => Link::LINK_TYPE_CROSSSELL,
'uncrosssell' => Link::LINK_TYPE_CROSSSELL,
'copyupsell' => Link::LINK_TYPE_UPSELL,
'upsell' => Link::LINK_TYPE_UPSELL,
'unupsell' => Link::LINK_TYPE_UPSELL,
'copyrelate' => Link::LINK_TYPE_RELATED,
'related' => Link::LINK_TYPE_RELATED,
'unrelated' => Link::LINK_TYPE_RELATED
];
return $types[$action] ?? null;
}
public function getLinkType(string $action): ?string
{
$types = [
'copycrosssell' => 'crosssell',
'crosssell' => 'crosssell',
'uncrosssell' => 'crosssell',
'copyupsell' => 'upsell',
'upsell' => 'upsell',
'unupsell' => 'upsell',
'copyrelate' => 'related',
'related' => 'related',
'unrelate' => 'related'
];
return $types[$action] ?? null;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Source;
use Magento\Framework\Data\OptionSourceInterface;
class Append implements OptionSourceInterface
{
const POSITION_BEFORE = 'before';
const POSITION_AFTER = 'after';
public function toOptionArray()
{
$result = [];
foreach ($this->toArray() as $value => $label) {
$result[] = [
'value' => $value,
'label' => $label
];
}
return $result;
}
public function toArray(): array
{
return [
self::POSITION_BEFORE => __('Before Attribute Text'),
self::POSITION_AFTER => __('After Attribute Text')
];
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Source;
use Amasty\Paction\Model\CommandResolver;
use Magento\Framework\Data\OptionSourceInterface;
class Commands implements OptionSourceInterface
{
/**
* @var CommandResolver
*/
protected $commandResolver;
/**
* @var array
*/
protected $types = [
'',
'addcategory',
'removecategory',
'replacecategory',
'',
'modifycost',
'modifyprice',
'modifyspecial',
'modifyallprices',
'updateadvancedprices',
'addspecial',
'addprice',
'addspecialbycost',
'',
'related',
'upsell',
'crosssell',
'',
'unrelated',
'unupsell',
'uncrosssell',
'',
'copyrelate',
'copyupsell',
'copycrosssell',
'',
'copyoptions',
'removeoptions',
'copyattr',
'copyimg',
'removeimg',
'',
'changeattributeset',
'changevisibility',
'',
'amdelete',
'',
'appendtext',
'replacetext',
''
];
public function __construct(
CommandResolver $commandResolver
) {
$this->commandResolver = $commandResolver;
}
public function toOptionArray()
{
$options = [];
foreach ($this->types as $i => $type) {
$data = $this->commandResolver->getCommandDataByName($type);
$options[] = [
'value' => $type ?: $i,
'label' => __($data['label']),
];
}
return $options;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Source;
use Magento\Framework\Data\OptionSourceInterface;
class Direction implements OptionSourceInterface
{
const SELECTED_TO_IDS = 0;
const IDS_TO_SELECTED = 1;
public function toOptionArray()
{
$result = [];
foreach ($this->toArray() as $value => $label) {
$result[] = [
'value' => $value,
'label' => $label
];
}
return $result;
}
public function toArray(): array
{
return [
self::SELECTED_TO_IDS => __('Selected to IDs'),
self::IDS_TO_SELECTED => __('IDs to Selected')
];
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Source;
use Magento\Framework\Data\OptionSourceInterface;
class LinkType implements OptionSourceInterface
{
const DEFAULT = 0;
const TWO_WAY = 1;
const MULTI_WAY = 2;
public function toOptionArray()
{
$result = [];
foreach ($this->toArray() as $value => $label) {
$result[] = [
'value' => $value,
'label' => $label
];
}
return $result;
}
public function toArray(): array
{
return [
self::DEFAULT => __('Default'),
self::TWO_WAY => __('2 Way'),
self::MULTI_WAY => __('Multi Way')
];
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Model\Source;
class Rounding implements \Magento\Framework\Option\ArrayInterface
{
const FIXED = 'fixed';
const MATH = 'math';
const CROP = 'crop';
const NEAREST_INT = 'nearest_int';
public function toOptionArray()
{
$result = [];
foreach ($this->toArray() as $value => $label) {
$result[] = [
'value' => $value,
'label' => $label
];
}
return $result;
}
public function toArray(): array
{
return [
self::FIXED => __('To specific value'),
self::MATH => __('Rounding to two decimal places'),
self::CROP => __('Truncate to two decimal places without rounding'),
self::NEAREST_INT => __('Rounding to the nearest integer')
];
}
}
<?php
namespace Amasty\Paction\Model\Source;
use Magento\Framework\Data\OptionSourceInterface;
class TierPrice implements OptionSourceInterface
{
const VALUE_FIXED = 'fixed';
const VALUE_PERCENT = 'percent';
public function toOptionArray()
{
return [
['value' => self::VALUE_FIXED, 'label' => __('Fixed')],
['value' => self::VALUE_PERCENT, 'label' => __('Discount')],
];
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Plugin\Catalog\Controller\Adminhtml\Product\Action\Attribute;
use Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice as TierPriceBlock;
use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory;
use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Helper\Product\Edit\Action\Attribute;
use Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Catalog\Pricing\Price\TierPrice;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Stdlib\ArrayManager;
class Save
{
/**
* @var Collection
*/
private $collection;
/**
* @var RequestInterface
*/
private $request;
/**
* @var ProductRepositoryInterface
*/
private $productRepository;
/**
* @var ProductTierPriceInterfaceFactory
*/
private $productTierPriceInterfaceFactory;
/**
* @var ProductTierPriceExtensionFactory
*/
private $productTierPriceExtensionFactory;
/**
* @var Attribute
*/
private $attributeHelper;
/**
* @var ArrayManager
*/
private $arrayManager;
public function __construct(
CollectionFactory $collectionFactory,
RequestInterface $request,
ProductRepositoryInterface $productRepository,
ProductTierPriceInterfaceFactory $productTierPriceInterfaceFactory,
ProductTierPriceExtensionFactory $productTierPriceExtensionFactory,
Attribute $attributeHelper,
ArrayManager $arrayManager
) {
$this->collection = $collectionFactory->create();
$this->request = $request;
$this->productRepository = $productRepository;
$this->productTierPriceInterfaceFactory = $productTierPriceInterfaceFactory;
$this->productTierPriceExtensionFactory = $productTierPriceExtensionFactory;
$this->attributeHelper = $attributeHelper;
$this->arrayManager = $arrayManager;
}
public function beforeExecute(\Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save $subject): void
{
$productIds = $this->attributeHelper->getProductIds();
$requestParams = $this->request->getParams();
$attrData = $requestParams['attributes'] ?? [];
$isNeedDeletePrices = $this->request->getParam(TierPriceBlock::TIER_PRICE_CHANGE_CHECKBOX_NAME);
$newTierPrices = $this->arrayManager->get(TierPrice::PRICE_CODE, $attrData)
? $this->prepareTierPrices($attrData[TierPrice::PRICE_CODE])
: [];
if ($newTierPrices || $isNeedDeletePrices) {
foreach ($productIds as $productId) {
$product = $this->productRepository->getById($productId);
$product->setMediaGalleryEntries($product->getMediaGalleryEntries());
$productTierPrices = $isNeedDeletePrices
? $newTierPrices
// phpcs:ignore
: array_merge($product->getTierPrices(), $newTierPrices);
$product->setTierPrices($productTierPrices);
$this->productRepository->save($product);
}
unset($attrData[TierPrice::PRICE_CODE]);
}
$requestParams['attributes'] = $attrData;
$this->request->setParams($requestParams);
}
private function prepareTierPrices(array $tierPriceDataArray): array
{
$result = [];
foreach ($tierPriceDataArray as $item) {
if (!$item['price_qty']) {
continue;
}
$tierPriceExtensionAttribute = $this->productTierPriceExtensionFactory->create()
->setWebsiteId($item['website_id']);
if ($isPercentValue = $item['value_type'] === ProductPriceOptionsInterface::VALUE_PERCENT) {
$tierPriceExtensionAttribute->setPercentageValue($item['price']);
}
$key = implode(
'-',
[$item['website_id'], $item['cust_group'], (int)$item['price_qty']]
);
$result[$key] = $this->productTierPriceInterfaceFactory
->create()
->setCustomerGroupId($item['cust_group'])
->setQty($item['price_qty'])
->setValue(!$isPercentValue ? $item['price'] : '')
->setExtensionAttributes($tierPriceExtensionAttribute);
}
return array_values($result);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Plugin\Ui\Model;
use Amasty\Paction\Model\CommandResolver;
use Amasty\Paction\Model\ConfigProvider;
use Magento\Catalog\Model\Product\AttributeSet\Options;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Ui\Component\Action;
class AbstractReader
{
const UI_COMPONENT = 'Amasty_Paction/js/grid/tree-massactions';
const ACTIONS_WITHOUT_INPUT = ['amdelete', 'removeimg', 'updateadvancedprices', 'removeoptions'];
const ACTIONS_WITH_SELECT = ['unrelated', 'unupsell', 'uncrosssell'];
/**
* @var ConfigProvider
*/
private $configProvider;
/**
* @var array|null
*/
private $attributeSets;
/**
* @var Visibility
*/
private $visibility;
/**
* @var CommandResolver
*/
private $commandResolver;
public function __construct(
ConfigProvider $configProvider,
Options $attributeSets,
Visibility $visibility,
CommandResolver $commandResolver
) {
$this->configProvider = $configProvider;
$this->attributeSets = $attributeSets->toOptionArray();
$this->visibility = $visibility;
$this->commandResolver = $commandResolver;
}
/**
* @param array $result
*
* @return array
*/
protected function addMassactions(array $result): array
{
if (isset($result['children']['listing_top']['children']['listing_massaction']['children'])
&& isset($result['children']['product_listing_data_source'])
) {
$children = &$result['children']['listing_top']['children']['listing_massaction']['children'];
$availableActions = $this->configProvider->getCommands();
if (!empty($availableActions)) {
foreach ($availableActions as $item) {
if (array_key_exists($item, $children)) {
continue;
}
$children[$item] = $this->generateElement($item);
}
$component = &$result['children']['listing_top']['children']['listing_massaction']['arguments']
['data']['item']['config']['item']['component']['value'];
if ($component !== self::UI_COMPONENT) {
$component = self::UI_COMPONENT;
}
}
}
return $result;
}
/**
* @param string $name
*
* @return array
*/
private function generateElement(string $name): array
{
$data = $this->commandResolver->getCommandDataByName($name);
$placeholder = (array_key_exists('placeholder', $data)) ? $data['placeholder'] : '';
$result = [
'arguments' => [
'data' => [
"name" => "data",
"xsi:type" => "array",
"item" => [
'config' => [
"name" => "config",
"xsi:type" => "array",
"item" => [
"component" => [
"name" => "component",
"xsi:type" => "string",
"value" => "uiComponent"
],
"amasty_actions" => [
"name" => "component",
"xsi:type" => "string",
"value" => 'true'
],
"confirm" => [
"name" => "confirm",
"xsi:type" => "array",
"item" => [
"title" => [
"name" => "title",
"xsi:type" => "string",
"translate" => "true",
"value" => $data['confirm_title']
],
"message" => [
"name" => "message",
"xsi:type" => "string",
"translate" => "true",
"value" => $data['confirm_message']
]
]
],
"type" => [
"name" => "type",
"xsi:type" => "string",
"value" => 'amasty_' . $data['type']
],
"label" => [
"name" => "label",
"xsi:type" => "string",
"translate" => "true",
"value" => $data['label']
],
"url" => [
"name" => "url",
"xsi:type" => "url",
"path" => $data['url']
]
]
]
]
],
'actions' => [
"name" => "actions",
"xsi:type" => "array",
'item' => [
0 => [
"name" => "0",
"xsi:type" => "array",
"item" => [
"typefield" => [
"name" => "type",
"xsi:type" => "string",
"value" => "textbox"
],
"fieldLabel" => [
"name" => "fieldLabel",
"xsi:type" => "string",
"value" => $data['fieldLabel']
],
"placeholder" => [
"name" => "placeholder",
"xsi:type" => "string",
"value" => $placeholder
],
"label" => [
"name" => "label",
"xsi:type" => "string",
"translate" => "true",
"value" => ""
],
"url" => [
"name" => "url",
"xsi:type" => "url",
"path" => $data['url']
],
"type" => [
"name" => "type",
"xsi:type" => "string",
"value" => 'amasty_' . $data['type']
],
]
]
]
]
],
'attributes' => [
'class' => Action::class,
'name' => $name
],
'children' => []
];
if (array_key_exists('hide_input', $data)) {
$result['arguments']['actions']['item'][0]['item']['hide_input'] = [
"name" => "hide_input",
"xsi:type" => "string",
"value" => '1'
];
}
if (strlen($name) <= 2
|| in_array($name, self::ACTIONS_WITHOUT_INPUT)
|| (isset($data['hide_input']) && $data['hide_input'] == 1)
) {
unset($result['arguments']['actions']);
}
if (in_array($name, self::ACTIONS_WITH_SELECT)) {
$result['arguments']['actions']['item'][0]['item']['typefield']['value'] = 'select';
$result['arguments']['actions']['item'][0]['item']['child'] = [
"name" => "child",
"xsi:type" => "array",
'item' => [
0 => [
"name" => "0",
"xsi:type" => "array",
"item" => [
"label" => [
"name" => "label",
"xsi:type" => "string",
"value" => __('Remove relations between selected products only')->render()
],
"fieldvalue" => [
"name" => "fieldvalue",
"xsi:type" => "string",
"value" => '1'
],
]
],
1 => [
"name" => "1",
"xsi:type" => "array",
"item" => [
"label" => [
"name" => "label",
"xsi:type" => "string",
"value" => __('Remove selected products from ALL relations in the catalog')->render()
],
"fieldvalue" => [
"name" => "fieldvalue",
"xsi:type" => "string",
"value" => '2'
],
]
],
2 => [
"name" => "2",
"xsi:type" => "array",
"item" => [
"label" => [
"name" => "label",
"xsi:type" => "string",
"value" => __('Remove all relations from selected products')->render()
],
"fieldvalue" => [
"name" => "fieldvalue",
"xsi:type" => "string",
"value" => '3'
],
]
]
]
];
}
if ($name == 'changeattributeset') {
$result['arguments']['actions']['item'][0]['item']['typefield']['value'] = 'select';
$result['arguments']['actions']['item'][0]['item']['child'] = [
"name" => "child",
"xsi:type" => "array",
'item' => []
];
$itemIndex = 0;
foreach ($this->attributeSets as $attributeSet) {
$result['arguments']['actions']['item'][0]['item']['child']['item'][$itemIndex] = [
"name" => $itemIndex,
"xsi:type" => "array",
"item" => [
"label" => [
"name" => "label",
"xsi:type" => "string",
"value" => $attributeSet['label']
],
"fieldvalue" => [
"name" => "fieldvalue",
"xsi:type" => "string",
"value" => $attributeSet['value']
],
]
];
$itemIndex++;
}
}
if ($name == 'changevisibility') {
$result['arguments']['actions']['item'][0]['item']['typefield']['value'] = 'select';
$result['arguments']['actions']['item'][0]['item']['child'] = [
"name" => "child",
"xsi:type" => "array",
'item' => []
];
$itemIndex = 0;
foreach ($this->visibility->getOptionArray() as $key => $visibility) {
$result['arguments']['actions']['item'][0]['item']['child']['item'][$itemIndex] = [
"name" => $itemIndex,
"xsi:type" => "array",
"item" => [
"label" => [
"name" => "label",
"xsi:type" => "string",
"value" => $visibility->getText()
],
"fieldvalue" => [
"name" => "fieldvalue",
"xsi:type" => "string",
"value" => (string)$key
],
]
];
$itemIndex++;
}
}
return $result;
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Plugin\Ui\Model;
use Magento\Ui\Model\Manager as UiManager;
class Manager extends AbstractReader
{
public function afterGetData(UiManager $subject, array $result): array
{
return $this->addMassactions($result);
}
}
<?php
declare(strict_types=1);
namespace Amasty\Paction\Plugin\Ui\Model;
use Magento\Ui\Config\Reader as ConfigReader;
class Reader extends AbstractReader
{
public function afterRead(ConfigReader $subject, array $result): array
{
return $this->addMassactions($result);
}
}
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* Created By : Rohan Hapani
*/
namespace Amasty\Paction\Ui\Component\Listing\Column;
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
class Category extends \Magento\Ui\Component\Listing\Columns\Column
{
/**
* @var \Magento\Catalog\Model\ProductCategoryList
*/
private $productCategory;
/**
* @var \Magento\Catalog\Api\CategoryRepositoryInterface
*/
private $categoryRepository;
/**
* @param ContextInterface $context
* @param UiComponentFactory $uiComponentFactory
* @param array $components
* @param array $data
* @param \Magento\Catalog\Model\ProductCategoryList $productCategory
* @param \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository
*/
public function __construct(
ContextInterface $context,
UiComponentFactory $uiComponentFactory,
array $components = [],
array $data = [],
\Magento\Catalog\Model\ProductCategoryList $productCategory,
\Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository
) {
parent::__construct($context, $uiComponentFactory, $components, $data);
$this->productCategory = $productCategory;
$this->categoryRepository = $categoryRepository;
}
/**
* Prepare date for category column
* @param array $dataSource
* @return array
*/
public function prepareDataSource(array $dataSource)
{
$fieldName = $this->getData('name');
if (isset($dataSource['data']['items'])) {
foreach ($dataSource['data']['items'] as &$item) {
$productId = $item['entity_id'];
$categoryIds = $this->getCategoryIds($productId);
$categories = [];
if (count($categoryIds)) {
foreach ($categoryIds as $categoryId) {
$categoryData = $this->categoryRepository->get($categoryId);
$categories[] = $categoryData->getName();
}
}
$item[$fieldName] = implode(',', $categories);
}
}
return $dataSource;
}
/**
* get all the category id
*
* @param int $productId
* @return array
*/
private function getCategoryIds(int $productId)
{
$categoryIds = $this->productCategory->getCategoryIds($productId);
$category = [];
if ($categoryIds) {
$category = array_unique($categoryIds);
}
return $category;
}
}
\ No newline at end of file
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* Created By : Rohan Hapani
*/
namespace Amasty\Paction\Ui\DataProvider\Product;
class ProductDataProvider extends \Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider
{
/**
* For filter grid according to category
* @param \Magento\Framework\Api\Filter $filter
*/
public function addFilter(\Magento\Framework\Api\Filter $filter)
{
if ($filter->getField() == 'category_id') {
$this->getCollection()->addCategoriesFilter(['in' => $filter->getValue()]);
} elseif (isset($this->addFilterStrategies[$filter->getField()])) {
$this->addFilterStrategies[$filter->getField()]
->addFilter(
$this->getCollection(),
$filter->getField(),
[$filter->getConditionType() => $filter->getValue()]
);
} else {
parent::addFilter($filter);
}
}
}
\ No newline at end of file
{
"name": "amasty\/paction",
"description": "Mass Product Action by Amasty",
"require": {
"php": "^7.3.0",
"amasty\/base": ">=1.12.14"
},
"type": "magento2-module",
"version": "1.1.24",
"license": [
"Commercial"
],
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"Amasty\\Paction\\": ""
}
}
}
\ No newline at end of file
<?xml version="1.0"?>
<!--
/**
* @author Amasty Team
* @copyright Copyright (c) 2016 Amasty (http://www.amasty.com)
* @package Amasty_Paction
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
<acl>
<resources>
<resource id="Magento_Backend::admin">
<resource id="Magento_Catalog::catalog" title="Products" sortOrder="30">
<resource id="Magento_Catalog::catalog_inventory" title="Inventory" sortOrder="10">
<resource id="Amasty_Paction::paction" title="Amasty Mass Product Actions" sortOrder="30"/>
</resource>
</resource>
<resource id="Magento_Backend::stores">
<resource id="Magento_Backend::stores_settings">
<resource id="Magento_Config::config">
<resource id="Amasty_Paction::config"
title="Amasty Mass Product Actions Settings"
sortOrder="130116" />
</resource>
</resource>
</resource>
</resource>
</resources>
</acl>
</config>
\ No newline at end of file
<?xml version="1.0"?>
<!--
/**
* @author Amasty Team
* @copyright Copyright (c) 2016 Amasty (http://www.amasty.com)
* @package Amasty_Paction
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save">
<plugin name="Amasty_Paction::TierPriceSave" type="Amasty\Paction\Plugin\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save" />
</type>
<preference for="Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider" type="Amasty\Paction\Ui\DataProvider\Product\ProductDataProvider" />
</config>
\ No newline at end of file
<?xml version="1.0"?>
<!--
/**
* @author Amasty Team
* @copyright Copyright (c) 2016 Amasty (http://www.amasty.com)
* @package Amasty_Paction
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="admin">
<route id="amasty_paction" frontName="amasty_paction">
<module name="Amasty_Paction" />
</route>
</router>
</config>
\ No newline at end of file
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="amasty_paction" translate="label" type="text" sortOrder="130116" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Mass Product Actions</label>
<tab>amasty</tab>
<resource>Amasty_Paction::config</resource>
<group id="amasty_information" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Information</label>
<frontend_model>Amasty\Paction\Block\Adminhtml\System\Config\Information</frontend_model>
</group>
<group id="general" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0">
<label>General</label>
<field id="commands" translate="label comment" type="multiselect" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Enabled Actions</label>
<source_model>Amasty\Paction\Model\Source\Commands</source_model>
<comment><![CDATA[Press CTRL+mouse to select multiple values.]]></comment>
</field>
<field id="round" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Price Rounding</label>
<source_model>Amasty\Paction\Model\Source\Rounding</source_model>
</field>
<field id="fixed" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Specific Value</label>
<comment><![CDATA[Indicate 0.99 to round 9.43 to 9.99, indicate 0.5 to round 9.43 to 9.50.]]></comment>
<depends>
<field id="round">fixed</field>
</depends>
</field>
<field id="attr" translate="label comment" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Copy Attributes</label>
<comment><![CDATA[Please specify comma separated attribute codes like short_description, meta_keyword, meta_title, etc]]></comment>
</field>
<field id="replace_in_attr" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Replace in Attributes</label>
<comment><![CDATA[Please specify comma separated attribute codes like short_description, meta_keyword, meta_title, etc]]></comment>
</field>
<field id="append_text_position" translate="label comment" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Append Text Position</label>
<source_model>Amasty\Paction\Model\Source\Append</source_model>
</field>
</group>
<group id="links" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Product Linking Algorithms</label>
<field id="related" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Related</label>
<source_model>Amasty\Paction\Model\Source\LinkType</source_model>
</field>
<field id="related_reverse" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Relate Direction</label>
<source_model>Amasty\Paction\Model\Source\Direction</source_model>
<depends>
<field id="related">0</field>
</depends>
</field>
<field id="upsell" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Up-sell</label>
<source_model>Amasty\Paction\Model\Source\LinkType</source_model>
</field>
<field id="upsell_reverse" translate="label comment" type="select" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Up-sell Direction</label>
<source_model>Amasty\Paction\Model\Source\Direction</source_model>
<depends>
<field id="upsell">0</field>
</depends>
</field>
<field id="crosssell" translate="label comment" type="select" sortOrder="50" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Cross-sell</label>
<source_model>Amasty\Paction\Model\Source\LinkType</source_model>
</field>
<field id="crosssell_reverse" translate="label comment" type="select" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Cross-sell Direction</label>
<source_model>Amasty\Paction\Model\Source\Direction</source_model>
<depends>
<field id="crosssell">0</field>
</depends>
</field>
</group>
</section>
</system>
</config>
<?xml version="1.0"?>
<!--
/**
* @author Amasty Team
* @copyright Copyright (c) 2016 Amasty (http://www.amasty.com)
* @package Amasty_Paction
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<amasty_paction>
<general>
<commands>0,addcategory,removecategory,replacecategory,4,modifycost,modifyprice,modifyspecial,modifyallprices,updateadvancedprices,addspecial,addprice,addspecialbycost,11,related,upsell,crosssell,13,copyrelate,copyupsell,copycrosssell,15,unrelated,unupsell,uncrosssell,19,copyoptions,removeoptions,copyattr,copyimg,removeimg,24,changeattributeset,changevisibility,27,amdelete,29,appendtext,replacetext,32</commands>
<round>math</round>
<fixed>0.99</fixed>
<attr></attr>
<replace_in_attr>description,short_description,name</replace_in_attr>
<append_text_position>after</append_text_position>
</general>
<links>
<upsell>0</upsell>
<upsell_reverse>0</upsell_reverse>
<crosssell>0</crosssell>
<crosssell_reverse>0</crosssell_reverse>
<relate>0</relate>
<related_reverse>0</related_reverse>
</links>
</amasty_paction>
</default>
</config>
<?xml version="1.0"?>
<!--
/**
* @author Amasty Team
* @copyright Copyright (c) 2016 Amasty (http://www.amasty.com)
* @package Amasty_Paction
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Ui\Model\Manager">
<plugin name="Amasty_Paction::checkEnabled" type="Amasty\Paction\Plugin\Ui\Model\Manager"/>
</type>
<type name="Magento\Ui\Config\Reader">
<plugin name="Amasty_Paction::checkEnabledM2" type="Amasty\Paction\Plugin\Ui\Model\Reader"/>
</type>
<type name="Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice\Content">
<arguments>
<argument name="tierPriceValueType" xsi:type="object">Amasty\Paction\Model\Source\TierPrice</argument>
</arguments>
</type>
<type name="Amasty\Paction\Model\CommandResolver">
<arguments>
<argument name="commands" xsi:type="array">
<item name="addcategory" xsi:type="object">Amasty\Paction\Model\Command\Addcategory\Proxy</item>
<item name="addprice" xsi:type="object">Amasty\Paction\Model\Command\Addprice\Proxy</item>
<item name="addspecial" xsi:type="object">Amasty\Paction\Model\Command\Addspecial\Proxy</item>
<item name="addspecialbycost" xsi:type="object">Amasty\Paction\Model\Command\Addspecialbycost\Proxy</item>
<item name="amdelete" xsi:type="object">Amasty\Paction\Model\Command\Amdelete\Proxy</item>
<item name="appendtext" xsi:type="object">Amasty\Paction\Model\Command\Appendtext\Proxy</item>
<item name="changeattributeset" xsi:type="object">Amasty\Paction\Model\Command\Changeattributeset\Proxy</item>
<item name="changevisibility" xsi:type="object">Amasty\Paction\Model\Command\Changevisibility\Proxy</item>
<item name="copyattr" xsi:type="object">Amasty\Paction\Model\Command\Copyattr\Proxy</item>
<item name="copycrosssell" xsi:type="object">Amasty\Paction\Model\Command\Copycrosssell\Proxy</item>
<item name="copyimg" xsi:type="object">Amasty\Paction\Model\Command\Copyimg\Proxy</item>
<item name="copyoptions" xsi:type="object">Amasty\Paction\Model\Command\Copyoptions\Proxy</item>
<item name="copyrelate" xsi:type="object">Amasty\Paction\Model\Command\Copyrelate\Proxy</item>
<item name="copyupsell" xsi:type="object">Amasty\Paction\Model\Command\Copyupsell\Proxy</item>
<item name="crosssell" xsi:type="object">Amasty\Paction\Model\Command\Crosssell\Proxy</item>
<item name="modifycost" xsi:type="object">Amasty\Paction\Model\Command\Modifycost\Proxy</item>
<item name="modifyprice" xsi:type="object">Amasty\Paction\Model\Command\Modifyprice\Proxy</item>
<item name="modifyspecial" xsi:type="object">Amasty\Paction\Model\Command\Modifyspecial\Proxy</item>
<item name="modifyallprices" xsi:type="object">Amasty\Paction\Model\Command\Modifyallprices\Proxy</item>
<item name="related" xsi:type="object">Amasty\Paction\Model\Command\Relate\Proxy</item>
<item name="removecategory" xsi:type="object">Amasty\Paction\Model\Command\Removecategory\Proxy</item>
<item name="removeimg" xsi:type="object">Amasty\Paction\Model\Command\Removeimg\Proxy</item>
<item name="removeoptions" xsi:type="object">Amasty\Paction\Model\Command\Removeoptions\Proxy</item>
<item name="replacecategory" xsi:type="object">Amasty\Paction\Model\Command\Replacecategory\Proxy</item>
<item name="replacetext" xsi:type="object">Amasty\Paction\Model\Command\Replacetext\Proxy</item>
<item name="uncrosssell" xsi:type="object">Amasty\Paction\Model\Command\Uncrosssell\Proxy</item>
<item name="unrelated" xsi:type="object">Amasty\Paction\Model\Command\Unrelate\Proxy</item>
<item name="unupsell" xsi:type="object">Amasty\Paction\Model\Command\Unupsell\Proxy</item>
<item name="updateadvancedprices" xsi:type="object">Amasty\Paction\Model\Command\Updateadvancedprices\Proxy</item>
<item name="upsell" xsi:type="object">Amasty\Paction\Model\Command\Upsell\Proxy</item>
</argument>
</arguments>
</type>
</config>
<?xml version="1.0"?>
<!--
/**
* @author Amasty Team
* @copyright Copyright (c) 2016 Amasty (http://www.amasty.com)
* @package Amasty_Paction
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Amasty_Paction" setup_version="1.0.0">
<sequence>
<module name="Magento_Catalog" />
<module name="Ey_Archive" />
<module name="Magento_Ui" />
<module name='Magento_InventoryCatalogAdminUi' />
</sequence>
</module>
</config>
"Advanced Pricing","Advanced Pricing"
"Tier Prices","Tier Prices"
"ALL GROUPS","ALL GROUPS"
"Please update Amasty Base module. Re-upload it and replace all the files.","Please update Amasty Base module. Re-upload it and replace all the files."
"Something went wrong while updating the product(s) data.","Something went wrong while updating the product(s) data."
"Something was wrong. Please try again.","Something was wrong. Please try again."
"Assign Category","Assign Category"
"Are you sure you want to assign category?","Are you sure you want to assign category?"
"Category IDs","Category IDs"
"id1,id2,id3","id1,id2,id3"
"Can not handle the category ID=%1, the error is: %2","Can not handle the category ID=%1, the error is: %2"
"Total of %1 category(ies) and %2 product(s) have been successfully updated.","Total of %1 category(ies) and %2 product(s) have been successfully updated."
"Please provide comma separated category IDs","Please provide comma separated category IDs"
"Magento2 does not allow to save the category ID=%1","Magento2 does not allow to save the category ID=%1"
"Modify Price using Cost","Modify Price using Cost"
"Are you sure you want to modify price using cost?","Are you sure you want to modify price using cost?"
"Modify Special Price using Price","Modify Special Price using Price"
"Are you sure you want to modify special price using price?","Are you sure you want to modify special price using price?"
"Modify Special Price using Cost","Modify Special Price using Cost"
"Are you sure you want to modify special price using cost?","Are you sure you want to modify special price using cost?"
"Fast Delete","Fast Delete"
"Are you sure you want to apply Fast Delete?","Are you sure you want to apply Fast Delete?"
"Please select product(s)","Please select product(s)"
"Products have been successfully deleted. We recommend to refresh indexes at the System > Index Management page.","Products have been successfully deleted. We recommend to refresh indexes at the System > Index Management page."
"Append Text","Append Text"
"Are you sure you want to append text?","Are you sure you want to append text?"
Append,Append
attribute_code->text,attribute_code->text
"Total of %1 products(s) have been successfully updated.","Total of %1 products(s) have been successfully updated."
"Field must contain ""' . . '""","Field must contain ""' . . '"""
"There is no product attribute with code `%1`, ","There is no product attribute with code `%1`, "
"Change Attribute Set","Change Attribute Set"
"Are you sure you want to change attribute set?","Are you sure you want to change attribute set?"
To,To
"Attribute Set Id","Attribute Set Id"
"Please provide a valid Attribute Group ID","Please provide a valid Attribute Group ID"
"Provided Attribute set non product Attribute set.","Provided Attribute set non product Attribute set."
"Can not change the attribute set for product ID %1, error is: %2","Can not change the attribute set for product ID %1, error is: %2"
"Total of %1 products(s) have not been updated, the reason: impossibility to change attribute set for configurable product","Total of %1 products(s) have not been updated, the reason: impossibility to change attribute set for configurable product"
"Change Visibility","Change Visibility"
"Are you sure you want to change visibility?","Are you sure you want to change visibility?"
"Can not change visibility for product ID %1, error is: %2","Can not change visibility for product ID %1, error is: %2"
"Copy Attributes","Copy Attributes"
"Are you sure you want to copy attributes?","Are you sure you want to copy attributes?"
From,From
"ID of product","ID of product"
"Please provide a valid product ID","Please provide a valid product ID"
"Please remove source product from the selected products","Please remove source product from the selected products"
"Please set attribute codes in the module configuration","Please set attribute codes in the module configuration"
"There is no product attribute with code `%1`, please compare values in the module configuration with stores > attributes > product.","There is no product attribute with code `%1`, please compare values in the module configuration with stores > attributes > product."
"Attribute `%1` is unique and can not be copied. Please remove the code in the module configuration.","Attribute `%1` is unique and can not be copied. Please remove the code in the module configuration."
"Attribute `%1` is static and can not be copied. Please remove the code in the module configuration.","Attribute `%1` is static and can not be copied. Please remove the code in the module configuration."
"Attributes have been successfully copied.","Attributes have been successfully copied."
"Copy Cross-sells","Copy Cross-sells"
"Are you sure you want to copy cross-sells?","Are you sure you want to copy cross-sells?"
"Copy Images","Copy Images"
"Are you sure you want to copy images?","Are you sure you want to copy images?"
"Product ID","Product ID"
"Can not copy images to product with ID %1","Can not copy images to product with ID %1"
"Images and labels has been successfully copied","Images and labels has been successfully copied"
"Can not copy image file: %1","Can not copy image file: %1"
"Image not found.","Image not found."
"Copy Custom Options","Copy Custom Options"
"Are you sure you want to copy custom options?","Are you sure you want to copy custom options?"
"Please provide a product with custom options","Please provide a product with custom options"
"Can not copy the options to the product ID=%1, the error is: %2","Can not copy the options to the product ID=%1, the error is: %2"
"Copy Relations","Copy Relations"
"Are you sure you want to copy relations?","Are you sure you want to copy relations?"
"Source product has no relations","Source product has no relations"
"Product association has been successfully added.","Product association has been successfully added."
"%1 product associations have been successfully added.","%1 product associations have been successfully added."
"Copy Up-sells","Copy Up-sells"
"Are you sure you want to copy up-sells?","Are you sure you want to copy up-sells?"
Cross-sell,Cross-sell
"Are you sure you want to cross-sell?","Are you sure you want to cross-sell?"
"Selected To IDs","Selected To IDs"
"Update All Types of Prices","Update All Types of Prices"
"Are you sure you want to update all types of prices?","Are you sure you want to update all types of prices?"
By,By
"+12.5, -12.5, +12.5%","+12.5, -12.5, +12.5%"
"Update Cost","Update Cost"
"Are you sure you want to update cost?","Are you sure you want to update cost?"
"Update Price","Update Price"
"Are you sure you want to update price?","Are you sure you want to update price?"
"Please provide the difference as +12.5, -12.5, +12.5% or -12.5%","Please provide the difference as +12.5, -12.5, +12.5% or -12.5%"
"Please provide a non empty difference","Please provide a non empty difference"
"Total of %1 products(s) have been successfully updated","Total of %1 products(s) have been successfully updated"
"Update Special Price","Update Special Price"
"Are you sure you want to update special price?","Are you sure you want to update special price?"
Relate,Relate
"Are you sure you want to relate?","Are you sure you want to relate?"
"Please select more than 1 product","Please select more than 1 product"
"Remove Category","Remove Category"
"Are you sure you want to remove category?","Are you sure you want to remove category?"
"Remove Images","Remove Images"
"Are you sure you want to remove images?","Are you sure you want to remove images?"
"Images and labels has been successfully deleted","Images and labels has been successfully deleted"
"Can not delete image file: %1","Can not delete image file: %1"
"%1 is not a file","%1 is not a file"
"Remove Custom Options","Remove Custom Options"
"Are you sure you want to remove custom options?","Are you sure you want to remove custom options?"
"Can not remove the options to the product ID=%1, the error is: %2","Can not remove the options to the product ID=%1, the error is: %2"
"Replace Category","Replace Category"
"Are you sure you want to replace category?","Are you sure you want to replace category?"
"Replace Text","Replace Text"
"Are you sure you want to replace text?","Are you sure you want to replace text?"
Replace,Replace
search->replace,search->replace
"Replace field must contain: search->replace","Replace field must contain: search->replace"
"Remove Cross-Sells","Remove Cross-Sells"
"Are you sure you want to remove cross-Sells?","Are you sure you want to remove cross-Sells?"
"Select Algorithm","Select Algorithm"
"Remove Relations","Remove Relations"
"Are you sure you want to remove relations?","Are you sure you want to remove relations?"
"Product associations have been successfully deleted.","Product associations have been successfully deleted."
"Remove Up-sells","Remove Up-sells"
"Are you sure you want to remove up-sells?","Are you sure you want to remove up-sells?"
"Update Advanced Prices","Update Advanced Prices"
"Are you sure you want to update prices?","Are you sure you want to update prices?"
Up-sell,Up-sell
"Are you sure you want to up-sell?","Are you sure you want to up-sell?"
"Before Attribute Text","Before Attribute Text"
"After Attribute Text","After Attribute Text"
label,label
"Selected to IDs","Selected to IDs"
"IDs to Selected","IDs to Selected"
Default,Default
"2 Way","2 Way"
"Multi Way","Multi Way"
"To specific value","To specific value"
"Rounding to two decimal places","Rounding to two decimal places"
"Truncate to two decimal places without rounding","Truncate to two decimal places without rounding"
"Rounding to the nearest integer","Rounding to the nearest integer"
Fixed,Fixed
Discount,Discount
"Remove relations between selected products only","Remove relations between selected products only"
"Remove selected products from ALL relations in the catalog","Remove selected products from ALL relations in the catalog"
"Remove all relations from selected products","Remove all relations from selected products"
"Web Site","Web Site"
"Customer Group","Customer Group"
Quantity,Quantity
"Price Type","Price Type"
"Item Price","Item Price"
Action,Action
Add,Add
"Delete Previously Saved Advanced Pricing","Delete Previously Saved Advanced Pricing"
Apply,Apply
Actions,Actions
"Select Items","Select Items"
Delete,Delete
"Delete Tier","Delete Tier"
"Required field is empty.","Required field is empty."
"Mass Product Actions","Mass Product Actions"
Information,Information
General,General
"Enabled Actions","Enabled Actions"
"Press CTRL+mouse to select multiple values.","Press CTRL+mouse to select multiple values."
"Price Rounding","Price Rounding"
"Specific Value","Specific Value"
"Indicate 0.99 to round 9.43 to 9.99, indicate 0.5 to round 9.43 to 9.50.","Indicate 0.99 to round 9.43 to 9.99, indicate 0.5 to round 9.43 to 9.50."
"Please specify comma separated attribute codes like short_description, meta_keyword, meta_title, etc","Please specify comma separated attribute codes like short_description, meta_keyword, meta_title, etc"
"Replace in Attributes","Replace in Attributes"
"Append Text Position","Append Text Position"
"Product Linking Algorithms","Product Linking Algorithms"
Related,Related
"Relate Direction","Relate Direction"
"Up-sell Direction","Up-sell Direction"
"Cross-sell Direction","Cross-sell Direction"
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Amasty_Paction',
__DIR__
);
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="attributes_tabs">
<block class="Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice" name="tab_tier_prices"/>
<action method="addTab">
<argument name="name" xsi:type="string">tier_prices</argument>
<argument name="block" xsi:type="string">tab_tier_prices</argument>
</action>
</referenceBlock>
</body>
</page>
<?php
/* @var $block \Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice\Content */
const COL_COUNT_WITHOUT_WEBSITE = 5;
const COL_COUNT_WITH_WEBSITE = 6;
$allGroupId = $block->getAllGroupsId();
$groups = array_replace_recursive($allGroupId, $block->getCustomerGroups());
$htmlId = $block->getElement()->getHtmlId();
$htmlClass = $block->getElement()->getClass();
$htmlName = $block->getElement()->getName();
$readonly = $block->getElement()->getReadonly();
$showWebsite = $block->isMultiWebsites();
$editWebsite = $block->isAllowChangeWebsite();
$priceValueValidation = $block->getPriceValidation('validate-greater-than-zero');
?>
<div class="field" id="attribute-<?= $block->escapeHtmlAttr($htmlId) ?>-container" data-attribute-code="<?= $block->escapeHtmlAttr($htmlId) ?>"
data-apply-to="<?= $block->escapeHtmlAttr($block->getApplyToJson())?>">
<label class="label">
<?= $block->escapeHtml($block->getElement()->getLabel()) ?>
</label>
<div class="control" data-bind="scope: 'tier-price'">
<table class="admin__control-table ampaction-table-container">
<thead>
<tr>
<th class="col-websites <?php if (!$showWebsite): ?>'-hide'<?php endif ?>"><?= $block->escapeHtml(__('Web Site')) ?></th>
<th class="col-customer-group"><?= $block->escapeHtml(__('Customer Group')) ?></th>
<th class="col-qty required"><?= $block->escapeHtml(__('Quantity')) ?></th>
<th class="col-price-value-type required"><?= $block->escapeHtml(__('Price Type')) ?></th>
<th class="col-price required"><?= /* @noEscape */ $block->getPriceColumnHeader(__('Item Price')) ?></th>
<th class="col-delete"><?= $block->escapeHtml(__('Action')) ?></th>
</tr>
</thead>
<tbody id="<?= $block->escapeHtmlAttr($htmlId) ?>_container">
<!-- ko template: getTemplate() --><!-- /ko -->
</tbody>
<tfoot>
<tr>
<td colspan="<?= /* @noEscape */ !$showWebsite ? COL_COUNT_WITHOUT_WEBSITE : COL_COUNT_WITH_WEBSITE ?>" class="col-actions-add">
<button data-bind="click: addRow"
title="<?= $block->escapeHtml(__('Add')) ?>"
aria-label="<?= $block->escapeHtml(__('Add')) ?>">
<?= $block->escapeHtml(__('Add')) ?>
</button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"tier-price": {
"component": "Amasty_Paction/js/tier-prices",
"showWebsite": "<?= /* @noEscape */ $showWebsite ?>",
"htmlClass": "<?= /* @noEscape */ $htmlClass ?>",
"websites": <?= /* @noEscape */ $block->getWebsitesJson() ?>,
"groups": <?= /* @noEscape */ $block->getGroupsJson() ?>,
"htmlName": "<?= /* @noEscape */ $htmlName ?>",
"priceValueValidationClass": "<?= /* @noEscape */ $priceValueValidation ?>",
"priceTypes": <?= /* @noEscape */ $block->getPriceValueTypesJson() ?>
}
}
}
}
}
</script>
<?php
/* @var $block \Amasty\Paction\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\TierPrice\Checkbox */
?>
<div class="field" style="margin-left: 20%;">
<span class="attribute-change-checkbox">
<input type="checkbox"
id="<?= $block->escapeHtmlAttr($block->getCheckboxElementId())?>"
name="<?= $block->escapeHtmlAttr($block->getCheckboxElementName()) ?>"
class="checkbox" />
<label class="label" for="<?= $block->escapeHtmlAttr($block->getCheckboxElementId()) ?>">
<?= $block->escapeHtml(__('Delete Previously Saved Advanced Pricing')) ?>
</label>
</span>
</div>
<?xml version="1.0" encoding="UTF-8"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<listingToolbar name="listing_top">
<massaction name="listing_massaction">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="selectProvider" xsi:type="string">product_listing.product_listing.product_columns.ids</item>
<item name="component" xsi:type="string">Amasty_Paction/js/grid/tree-massactions</item>
<item name="indexField" xsi:type="string">entity_id</item>
</item>
</argument>
</massaction>
<filters name="listing_filters">
<filterSelect name="category_id" provider="${ $.parentName }" component="Magento_Ui/js/form/element/ui-select" template="ui/grid/filters/elements/ui-select">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="filterOptions" xsi:type="boolean">true</item>
<item name="levelsVisibility" xsi:type="number">1</item>
</item>
</argument>
<settings>
<options class="Magento\Catalog\Ui\Component\Product\Form\Categories\Options"/>
<caption translate="true">– Please Select a Category –</caption>
<label translate="true">Categories</label>
<dataScope>category_id</dataScope>
<imports>
<link name="visible">componentType = column, index = ${ $.index }:visible</link>
</imports>
</settings>
</filterSelect>
</filters>
</listingToolbar>
<columns name="product_columns" class="Magento\Catalog\Ui\Component\Listing\Columns">
<column name="category_id" class="Amasty\Paction\Ui\Component\Listing\Column\Category">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="label" xsi:type="string" translate="true">Categories</item>
<item name="sortOrder" xsi:type="number">35</item>
</item>
</argument>
</column>
</columns>
</listing>
//
// Amasty Product Actions
// ____________________________________________
//
// Common
// --------------------------------------------
& when (@media-common = true) {
.ampaction-form-container {
& {
display: flex;
align-items: center;
padding: 8px;
}
.ampaction-input {
margin-right: 8px;
vertical-align: middle;
}
.ampaction-label {
display: inline-block;
margin-right: 8px;
max-width: 84px;
vertical-align: middle;
}
.ampaction-select {
margin-right: 8px;
max-width: 170px;
vertical-align: middle;
}
.ampgrid-button {
vertical-align: middle;
}
}
.ampaction-table-container .col-websites.-hide {
display: none;
}
// override magento styles
.ampaction-select-container {
.action-select-wrap .action-menu._active {
max-height: 300px;
}
.action-menu .ampaction-action-submenu {
top: 100%;
margin-top: 2px;
}
.action-menu .ampaction-line {
cursor: default;
pointer-events: none;
}
}
// END override magento styles
}
define([
'underscore',
'Magento_Ui/js/grid/massactions',
'uiRegistry',
'mageUtils',
'Magento_Ui/js/lib/collapsible',
'Magento_Ui/js/modal/confirm',
'Magento_Ui/js/modal/alert',
'mage/translate'
], function (_, Massactions, registry, utils, Collapsible, confirm, alert, $t) {
'use strict';
return Massactions.extend({
defaults: {
fieldEmptyMsg: $t('Required field is empty.')
},
/**
* Default action callback. Sends selections data
* via POST request.
*
* @param {Object} action - Action data.
* @param {Object} data - Selections data.
*/
defaultCallback: function (action, data) {
var itemsType = data.excludeMode ? 'excluded' : 'selected',
params = {};
params[itemsType] = data[itemsType];
if (!params[itemsType].length) {
params[itemsType] = false;
}
_.extend(params, data.params || {});
if (action.type && action.type.indexOf('amasty') === 0) {
params.action = action.type;
}
utils.submit({
url: action.url,
data: params
});
},
applyMassaction: function (parent, action) {
var data = this.getSelections(),
value,
callback;
value = action.value;
action = this.getAction(action.type);
if (!value) {
alert({ content: this.fieldEmptyMsg });
return this;
}
if (!data.total) {
alert({ content: this.noItemsMsg });
return this;
}
callback = this.massactionCallback.bind(this, action, data, value);
action.confirm ? this._confirm(action, callback) : callback();
},
massactionCallback: function (action, data, value) {
var itemsType = data.excludeMode ? 'excluded' : 'selected',
params = {};
params[itemsType] = data[itemsType];
params['amasty_paction_field'] = value;
params.action = action.type;
if (!params[itemsType].length) {
params[itemsType] = false;
}
_.extend(params, data.params || {});
utils.submit({
url: action.url,
data: params
});
}
});
});
/**
* @author Amasty Team
* @copyright Copyright (c) 2016 Amasty (http://www.amasty.com)
* @package Amasty_Paction
*/
define([
'ko',
'underscore',
'Amasty_Paction/js/grid/massactions'
], function (ko, _, Massactions) {
'use strict';
return Massactions.extend({
defaults: {
template: 'Amasty_Paction/grid/tree-massactions',
submenuTemplate: 'ui/grid/submenu',
amastysubmenuTemplate: 'Amasty_Paction/grid/submenu',
selectProvider: '',
modules: {
selections: '${ $.selectProvider }'
},
listens: {
opened: 'hideSubmenus'
}
},
/**
* Initializes observable properties.
*
* @returns {Massactions} Chainable.
*/
initObservable: function () {
this._super()
.recursiveObserveActions(this.actions());
return this;
},
/**
* Recursive initializes observable actions.
*
* @param {Array} actions - Action objects.
* @returns {Massactions} Chainable.
*/
recursiveObserveActions: function (actions) {
_.each(actions, function (action) {
if (action.actions) {
action.visible = ko.observable(false);
action.parent = actions;
this.recursiveObserveActions(action.actions);
}
}, this);
return this;
},
/**
* Applies specified action.
*
* @param {String} actionIndex - Actions' identifier.
* @returns {Massactions} Chainable.
*/
applyAction: function (actionIndex) {
var action = this.getAction(actionIndex),
visibility;
if (action.visible) {
visibility = action.visible();
this.hideSubmenus(action.parent);
action.visible(!visibility);
return this;
}
return this._super(actionIndex);
},
/**
* Retrieves action object associated with a specified index.
*
* @param {String} actionIndex - Actions' identifier.
* @param {Array} actions - Action objects.
* @returns {Object} Action object.
*/
getAction: function (actionIndex, actions) {
var currentActions = actions || this.actions(),
result = false;
_.find(currentActions, function (action) {
if (action.type === actionIndex) {
result = action;
return true;
}
if (action.actions) {
result = this.getAction(actionIndex, action.actions);
return result;
}
}, this);
return result;
},
/**
* Recursive hide all sub folders in given array.
*
* @param {Array} actions - Action objects.
* @returns {Massactions} Chainable.
*/
hideSubmenus: function (actions) {
var currentActions = actions || this.actions();
_.each(currentActions, function (action) {
if (action.visible && action.visible()) {
action.visible(false);
}
if (action.actions) {
this.hideSubmenus(action.actions);
}
}, this);
return this;
},
applyMassaction: function (action) {
return this._super(this, action);
}
});
});
define([
'uiComponent',
'uiLayout',
'mageUtils'
], function (Component, layout, utils) {
'use strict';
return Component.extend({
defaults: {
template: 'Amasty_Paction/table/row',
showWebsite: false,
websites: [],
groups: [],
priceTypes: [],
htmlClass: '',
htmlName: '',
priceValueValidationClass: '',
elemIndex: 0,
rowsConfig: {
component: 'Magento_Ui/js/form/element/abstract',
template: 'Amasty_Paction/table/row'
}
},
deleteRow: function (element) {
this.removeChild(element);
},
addRow: function () {
var row = this.createRow(this.elemIndex);
layout([ row ]);
this.insertChild(row.name);
this.elemIndex += 1;
},
createRow: function (index) {
return utils.extend(this.rowsConfig, {
'websites': this.websites,
'groups': this.groups,
'htmlClass': this.htmlClass,
'htmlName': this.htmlName,
'name': 'price-row-' + index,
'priceValueValidationClass': this.priceValueValidationClass,
'priceTypes': this.priceTypes
});
},
getWebsite: function (name, currency) {
currency = currency ? '[' + currency + ']' : '';
return name + ' ' + currency;
}
});
});
<ul class="ampaction-action-submenu action-submenu"
data-bind="foreach: {data: action.actions, as: 'action'}, css: { _active: action.visible }">
<li data-bind="css: { '_visible': $data.visible}">
<div class="ampaction-form-container">
<label class="ampaction-label" data-bind="i18n: action.fieldLabel"></label>
<!-- ko if: $data.typefield == 'textbox' -->
<input type="text"
class="ampaction-input admin__control-text"
data-bind="attr: {placeholder: action.placeholder}, value: action.value"/>
<!-- /ko -->
<!-- ko if: $data.typefield == 'select' -->
<select id="amasty-paction-select"
class="ampaction-select admin__control-select"
data-bind="
options: action.child,
optionsText: 'label',
optionsValue: 'fieldvalue',
value: action.value">
</select>
<!-- /ko -->
<button class="ampgrid-button action-save"
data-bind="
click: $parent.applyMassaction.bind($parent, action),
i18n: 'Apply',
attr: {title: $t('Apply'), 'aria-label': $t('Apply')}">
</button>
</div>
</li>
</ul>
<div class="action-select-wrap ampaction-select-container"
data-bind="css: {'_active': opened}, outerClick: close.bind($data)">
<button class="action-select"
data-bind="
click: toggleOpened,
i18n: 'Actions',
attr: {title: $t('Select Items'), 'aria-label': $t('Select Items')}">
</button>
<div class="action-menu-items">
<ul class="action-menu"
data-bind="css: {'_active': opened}, foreach: {data: actions, as: 'action'}">
<li data-bind="css: {
'_visible': $data.visible,
'_parent': $data.actions,
'ampaction-line' : $data.amasty_actions && !$data.confirm.title}">
<span class="action-menu-item"
data-bind="text: label, click: $parent.applyAction.bind($parent, type)">
</span>
<!-- ko if: ($data.actions && $data.amasty_actions) -->
<!-- ko template: {name: $parent.amastysubmenuTemplate, data: $parent} -->
<!-- /ko -->
<!-- /ko-->
<!-- ko if: ($data.actions && !$data.amasty_actions) -->
<!-- ko template: {name: $parent.submenuTemplate, data: $parent} -->
<!-- /ko -->
<!-- /ko -->
</li>
</ul>
</div>
</div>
<!-- ko foreach: elems -->
<tr data-ampaction-js="row">
<td class="col-websites">
<select data-bind="
attr: {name: $parent.htmlName + '[' + $index() + '][website_id]'}, css: $parent.htmlClass">
<!-- ko foreach: $parent.websites -->
<option data-bind="value: $index(), text: $parents[1].getWebsite(name, currency)"></option>
<!-- /ko -->
</select>
</td>
<td class="col-customer-group">
<select class="requried-entry input-text admin__control-text custgroup required-entry"
data-bind="attr: {name: $parent.htmlName + '[' + $index() + '][cust_group]'}, css: $parent.htmlClass">
<!-- ko foreach: { data: Object.keys($parent.groups), as: 'groupKey' } -->
<option data-bind="value: groupKey, text: $parents[1].groups[groupKey]"></option>
<!-- /ko -->
</select>
</td>
<td class="col-qty">
<input class="qty required-entry validate-greater-than-zero"
type="text"
data-bind="attr: {name: $parent.htmlName + '[' + $index() + '][price_qty]'}, css: $parent.htmlClass"
/>
</td>
<td class="col-price-value-type">
<select class="value-type required-entry"
data-bind="
attr: {name: $parent.htmlName + '[' + $index() + '][value_type]'},
css: $parent.htmlClass,
options: $parent.priceTypes,
optionsText: 'label',
optionsValue: 'value',">
</select>
</td>
<td class="col-price">
<input type="text"
data-bind="
attr: {name: $parent.htmlName + '[' + $index() + '][price]'},
css: $parent.htmlClass + ' ' + $parent.priceValueValidationClass"/>
</td>
<td class="col-delete">
<button data-bind="
attr: {title: $t('Delete Tier'), 'aria-label': $t('Delete Tier')},
i18n: 'Delete',
click: $parent.deleteRow.bind($parent, $data)"></button>
</td>
</tr>
<!-- /ko -->
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment