How to create sales rule programmatically in Magento2
Question
I am migrating code from Magento1 to Magento2 and need to create a group of sales rules programatically. I need to do this because it's based on the price of the item, plus $1 if they purchase two (buy one at full price, get a second for $1).
I was previously able to accomplish this using this code. I am wondering how one would go about it under Magento2?
$discount = ($price - (($price + 1) / 2));
$shoppingCartPriceRule = Mage::getModel('salesrule/rule');
$shoppingCartPriceRule->setName('Add a second for $1 - ' . $sku)
->setDescription('Buy one item at regular price, and receive a second item for just $1.00 more!')
->setFromDate('2000-01-01')
->setToDate(NULL)
->setUsesPerCustomer('0')
->setCustomerGroupIds(array('0','1','2','3',))
->setIsActive('1')
->setStopRulesProcessing('0')
->setIsAdvanced('1')
->setProductIds(NULL)
->setSortOrder('1')
->setSimpleAction('by_fixed')
->setDiscountAmount($discount)
->setDiscountQty(NULL)
->setDiscountStep('0')
->setSimpleFreeShipping('0')
->setApplyToShipping('0')
->setTimesUsed('0')
->setIsRss('0')
->setWebsiteIds(array('1',))
->setCouponType('1')
->setCouponCode(NULL)
->setUsesPerCoupon(NULL);
// Add Sku Condition
$skuCond = Mage::getModel('salesrule/rule_condition_product')
->setType('salesrule/rule_condition_product')
->setAttribute('sku')
->setOperator('==')
->setValue($sku);
$shoppingCartPriceRule->getActions()->addCondition($skuCond);
// Add Qty Condition
$qtyCond = Mage::getModel('salesrule/rule_condition_product')
->setType('salesrule/rule_condition_product')
->setAttribute('quote_item_qty')
->setOperator('()')
->setValue('2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96');
$shoppingCartPriceRule->getActions()->addCondition($qtyCond);
Thank you!
Solution
If you have object manager then you can go with following example code
$price = 100; $sku = '24-WG085'; $discount = ($price - (($price + 1) / 2)); $shoppingCartPriceRule = $this->_objectManager->create('Magento\SalesRule\Model\Rule'); $shoppingCartPriceRule->setName('Add a second for $1 - ' . $sku) ->setDescription('Buy one item at regular price, and receive a second item for just $1.00 more!') ->setFromDate('2000-01-01') ->setToDate(NULL) ->setUsesPerCustomer('0') ->setCustomerGroupIds(array('0','1','2','3',)) ->setIsActive('1') ->setStopRulesProcessing('0') ->setIsAdvanced('1') ->setProductIds(NULL) ->setSortOrder('1') ->setSimpleAction('by_fixed') ->setDiscountAmount($discount) ->setDiscountQty(NULL) ->setDiscountStep('0') ->setSimpleFreeShipping('0') ->setApplyToShipping('0') ->setTimesUsed('0') ->setIsRss('0') ->setWebsiteIds(array('1',)) ->setCouponType('1') ->setCouponCode(NULL) ->setUsesPerCoupon(NULL); $item_found = $this->_objectManager->create('Magento\SalesRule\Model\Rule\Condition\Product\Found') ->setType('Magento\SalesRule\Model\Rule\Condition\Product\Found') ->setValue(1) // 1 == FOUND ->setAggregator('all'); // match ALL conditions $shoppingCartPriceRule->getConditions()->addCondition($item_found); $conditions = $this->_objectManager->create('Magento\SalesRule\Model\Rule\Condition\Product') ->setType('Magento\SalesRule\Model\Rule\Condition\Product') ->setAttribute('sku') ->setOperator('==') ->setValue($sku); $item_found->addCondition($conditions); $actions = $this->_objectManager->create('Magento\SalesRule\Model\Rule\Condition\Product') ->setType('Magento\SalesRule\Model\Rule\Condition\Product') ->setAttribute('sku') ->setOperator('==') ->setValue($sku); $shoppingCartPriceRule->getActions()->addCondition($actions); // Add Qty Condition $qtyCond = $this->_objectManager->create('Magento\SalesRule\Model\Rule\Condition\Product') ->setType('Magento\SalesRule\Model\Rule\Condition\Product') ->setAttribute('quote_item_qty') ->setOperator('()') ->setValue('2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96'); $shoppingCartPriceRule->getActions()->addCondition($qtyCond); $shoppingCartPriceRule->save();
Otherwise create a object manager using DI
/** * @var \Magento\Framework\ObjectManagerInterface */ protected $_objectManager; public function __construct( \Magento\Framework\ObjectManagerInterface $_objectManager ) { $this->_objectManager = $_objectManager; }
OTHER TIPS
Sohel Rana's answer is excellent. To answer Mir's point, here is an example of how DI would be used to bring in the relevant classes in a class context, this is a small tweak to the accepted answer:
protected $ruleFactory;
protected $productRuleFactory;
protected $foundProductRuleFactory;
protected $ruleResource;
public function __construct(
\Magento\SalesRule\Model\RuleFactory $ruleFactory,
\Magento\SalesRule\Model\Rule\Condition\ProductFactory $productRuleFactory,
\Magento\SalesRule\Model\Rule\Condition\Product\FoundFactory $foundProductRuleFactory,
\Magento\SalesRule\Model\ResourceModel\Rule $ruleResource
) {
$this->ruleFactory = $ruleFactory;
$this->productRuleFactory = $productRuleFactory;
$this->foundProductRuleFactory = $foundProductRuleFactory;
$this->ruleResource = $ruleResource;
}
public function createRule()
{
$price = 100;
$sku = '24-WG085';
$discount = ($price - (($price + 1) / 2));
$shoppingCartPriceRule = $this->ruleFactory->create();
$shoppingCartPriceRule->setName('Add a second for $1 - ' . $sku)
->setDescription('Buy one item at regular price, and receive a second item for just $1.00 more!')
->setFromDate('2000-01-01')
->setToDate(NULL)
->setUsesPerCustomer('0')
->setCustomerGroupIds(array('0','1','2','3',))
->setIsActive('1')
->setStopRulesProcessing('0')
->setIsAdvanced('1')
->setProductIds(NULL)
->setSortOrder('1')
->setSimpleAction('by_fixed')
->setDiscountAmount($discount)
->setDiscountQty(NULL)
->setDiscountStep('0')
->setSimpleFreeShipping('0')
->setApplyToShipping('0')
->setTimesUsed('0')
->setIsRss('0')
->setWebsiteIds(array('1',))
->setCouponType('1')
->setCouponCode(NULL)
->setUsesPerCoupon(NULL);
$item_found = $this->foundProductRuleFactory->create()
->setType('Magento\SalesRule\Model\Rule\Condition\Product\Found')
->setValue(1) // 1 == FOUND
->setAggregator('all'); // match ALL conditions
$shoppingCartPriceRule->getConditions()->addCondition($item_found);
$conditions = $this->productRuleFactory->create()
->setType('Magento\SalesRule\Model\Rule\Condition\Product')
->setAttribute('sku')
->setOperator('==')
->setValue($sku);
$item_found->addCondition($conditions);
$actions = $this->productRuleFactory->create()
->setType('Magento\SalesRule\Model\Rule\Condition\Product')
->setAttribute('sku')
->setOperator('==')
->setValue($sku);
$shoppingCartPriceRule->getActions()->addCondition($actions);
// Add Qty Condition
$qtyCond = $this->productRuleFactory->create()
->setType('Magento\SalesRule\Model\Rule\Condition\Product')
->setAttribute('quote_item_qty')
->setOperator('()')
->setValue('2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96');
$shoppingCartPriceRule->getActions()->addCondition($qtyCond);
$this->ruleResource->save($shoppingCartPriceRule);
}
The original save statement will work fine, but Magento have depreciated the direct save, so it's more correct to save via the resource model.
If anyone has problems with:
Uncaught Error: Call to a member function setOperator() on null", all you have to do is to replace with "setData('operator', '==').
I hope this code help someone work in magento 2.3, because it work for me
this class Magento\Framework\App\Action\Context $context just if you work in a controller otherwise you have to delete it
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\App\StateFactory $stateFactory,
\Magento\SalesRule\Model\Rule\Condition\Product\FoundFactory $foundFactory,
\Magento\SalesRule\Model\Rule\Condition\Product\CombineFactory $combineFactory,
\Magento\SalesRule\Model\RuleFactory $ruleFactory,
\Magento\SalesRule\Model\ResourceModel\Rule $resourceRule,
\Psr\Log\LoggerInterface $logger
) {
parent::__construct($context);
$this->stateFactory = $stateFactory;
$this->foundFactory = $foundFactory;
$this->combineFactory = $combineFactory;
$this->ruleFactory = $ruleFactory;
$this->resourceRule = $resourceRule;
$this->logger = $logger;
}
public function execute()
{
try {
$this->stateFactory->create()->setAreaCode('adminhtml');
$coupon['name'] = 'coupon name';
$coupon['desc'] = 'coupon description';
$coupon['start'] = date('Y-m-d');
$coupon['end'] = date('Y-m-d', strtotime("+3 months"));
//this code will normally be auto generated but we generated
$coupon['code'] ='GIFT-WSH-' . rand(1000, 99999);
/** @var Rule $rule */
$rule = $this->ruleFactory->create();
$rule->setName($coupon['name'])
->setDescription($coupon['desc'])
->setFromDate($coupon['start'])
->setToDate($coupon['end'])
->setUsesPerCustomer(1)
->setIsActive(1)
->setIsAdvanced("1")
->setSimpleAction('by_fixed')
->setDiscountAmount(60)
->setApplyToShipping("no")
->setIsRss(1)
->setCouponType(2)
->setCouponCode($coupon['code'])
->setUsesPerCoupon(1)
->setCustomerGroupIds(array('0','1','2','3'))
->setWebsiteIds(array('1'));
$itemFound = $this->foundFactory->create()
->setType('Magento\SalesRule\Model\Rule\Condition\Product\Found')
->setValue(1)
->setAggregator('all');
$rule->getConditions()->addCondition($itemFound);
$conditions = $this->combineFactory->create()
->setType('Magento\SalesRule\Model\Rule\Condition\Product\Combine')
->setAttribute('attribute_set_id')
->setOperator('==')
->setValue(13);
$itemFound->addCondition($conditions);
// Validating rule data before Saving
$validateResult = $rule->validateData(new \Magento\Framework\DataObject($rule->getData()));
if ($validateResult !== true) {
foreach ($validateResult as $errorMessage) {
dump($errorMessage);
}
}
$this->resourceRule->save($rule);
return $rule;
} catch (\Exception $e) {
$this->logger->error($e);
return null;
}
}
This solution was the best for me. Just try to create rule in admin pannel and after that find it in salesrule table. There you can see "conditions_serialized" column and there will be different classes. In my case it was something like this
{"type":"Magento\\SalesRule\\Model\\Rule\\Condition\\Combine","attribute":null,"operator":null,"value":"1","is_value_processed":null,"aggregator":"all","conditions":
[
{"type":"Magento\\SalesRule\\Model\\Rule\\Condition\\Address","attribute":"base_subtotal_with_discount","operator":">=","value":0,"is_value_processed":false},
{"type":"Magento\\SalesRule\\Model\\Rule\\Condition\\Address","attribute":"base_subtotal_with_discount","operator":"<=","value":600,"is_value_processed":false}
]}
Take these classes and include them into your code. Watch for the structure nesting.
use Magento\Customer\Model\GroupManagement;
use Magento\SalesRule\Model\Rule\Condition\Address;
use Magento\SalesRule\Model\Rule\Condition\Combine;
use Magento\SalesRule\Model\RuleFactory;
use Magento\Customer\Model\ResourceModel\Group\Collection;
public function createRules()
{
/*$this->customerGroupColl is instance of
Magento\Customer\Model\ResourceModel\Group\Collection*/
$customerGroups = $this->customerGroupColl->toOptionArray();
/*"$this->ruleFactory" is
Magento\SalesRule\Model\RuleFactory instance;*/
$salesRule = $this->ruleFactory->create();
// General rule data
$salesRule->setData(
[
'name' => 'some rule name',
'description' => 'some rule description',
'is_active' => 1,
'customer_group_ids' => array_keys($customerGroups), //[0,1,2...]
'coupon_type' => Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON,
'simple_action' => Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION,
'discount_amount' => 50,
'discount_step' => 0,
'stop_rules_processing' => 0,
'website_ids' => [
1, 2
],
'store_labels' => [
0 => 'Label for store with Id=0',
1 => 'Label for store with Id=1'
]
);
/*including conditions. The required classes for type keys can be found in
"conditions_serialized" column*/
$salesRule->getConditions()->loadArray(
[
'type' => Combine::class,
'attribute' => null,
'operator' => null,
'value' => '1',
'is_value_processed' => null,
'aggregator' => 'all',
'conditions' => [
[
'type' => Address::class,
'attribute' => 'base_subtotal_with_discount',
'operator' => '>=',
'value' => 0,
'is_value_processed' => false,
],
[
'type' => Address::class,
'attribute' => 'base_subtotal_with_discount',
'operator' => '<=',
'value' => 600,
'is_value_processed' => false,
]
],
]
);
$salesRule->save();
}