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\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 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>
<?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);
}
});
});
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