1

I need to apply discount on another different product Y based on X Product Step Qty. So that why I was created custom simple action in sales rule. I created custom action class for calculation discount.

see my code below I share

Vendor\Module\etc\adminhtml\di.xml

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\SalesRule\Model\Rule\Metadata\ValueProvider">
        <plugin name="salesrule-plugin" type="Vendor\Module\Plugin\Rule\Metadata\ValueProvider" sortOrder="1" />
    </type>
</config>

Vendor\Module\Plugin\Rule\Metadata\ValueProvider

<?php

namespace Vendor\Module\Plugin\Rule\Metadata;

class ValueProvider {
    public function afterGetMetadataValues(
        \Magento\SalesRule\Model\Rule\Metadata\ValueProvider $subject,
        $result
    ) {


        $applyOptions = [
            'label' => __('Custom'),
            'value' => [
                [
                    'label' => 'Buy X Get next Y with M% discount',
                    'value' => 'buy_x_get_next_y_with_percent',
                ]
            ],
        ];
        array_push($result['actions']['children']['simple_action']['arguments']['data']['config']['options'], $applyOptions);
        return $result;
    }
}

vendor\module\etc\di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory">
        <arguments>
            <argument name="discountRules" xsi:type="array">
                <item name="buy_x_get_next_y_with_percent" xsi:type="string">\Vendor\Module\Model\Rule\Action\Discount\BuyXGetNextYWithPercent</item>
            </argument>
        </arguments>
     </type>
</config>

Vendor\Module\Model\Rule\Action\Discount\BuyXGetNextYWithPercent

<?php
namespace Vendor\Module\Model\Rule\Action\Discount;

use Magento\SalesRule\Model\Rule\Action\Discount\AbstractDiscount;

/**
 * Buy X Get next Y with M% discount (buy_x_get_next_y_with_percent)
 * 
 * e.g. Rule :- Buy 3 Britannia Bourbon, Get 2 Good Day with 10% off
 * 
 *      Applicable Cases :-
 *          * case 1 :- Bourbon = 3 & good day = 2
 *          * case 2 :- Bourbon = 3 & good day = 7
 *          * case 3 :- Bourbon = 4 & good day = 2
 *          * case 4 :- Bourbon = 5 & good day = 3
 *          * case 5 :- Bourbon = 6 & good day = 4
 *          * case 6 :- Bourbon = 6 & good day = 15
 *          * case 7 :- Bourbon = 7 & good day = 4
 * 
 * 
 *      Not Applicable cases (discount = 0) :- 
 *          * case 1 :- Bourbon = 2 & good day = 2
 *          * case 2 :- Bourbon = 3 & good day = 1
 *          * case 3 :- Bourbon = 6 & good day = 2
 *          * case 4 :- Bourbon = 6 & good day = 3
 * 
 */
class BuyXGetNextYWithPercent extends AbstractDiscount
{
    /**
     * Calculate Buy X Get next Y with M% discount amount
     *
     * @param \Magento\SalesRule\Model\Rule $rule
     * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
     * @param float $qty
     * @return \Magento\SalesRule\Model\Rule\Action\Discount\Data
     */
    public function calculate($rule, $item, $qty)
    {
        $rulePercent = min(100, $rule->getDiscountAmount());
        $discountData = $this->_calculate($rule, $item, $qty, $rulePercent);
        
        return $discountData;
    }

    /**
     * @param \Magento\SalesRule\Model\Rule $rule
     * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
     * @param float $qty
     * @param float $rulePercent
     * @return Data
     */
    protected function _calculate($rule, $item, $qty, $rulePercent)
    {
        /** @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData */
        $discountData = $this->discountFactory->create();

        $skus = [];
        $conditions = $rule->getConditions();
        if($conditions && gettype($conditions) === 'object')
        {
            $conditions = $conditions->getData();
            if (isset($conditions['conditions'])) {
                $conditions = $conditions['conditions'];
                foreach ($conditions as $condition) {
                    if (isset($condition['conditions'])) {
                        $productConditions = $condition['conditions'];
                        foreach ($productConditions as $productCondition) {
                            if (isset($productCondition['attribute']) && $productCondition['attribute'] == "sku" && isset($productCondition['value'])) {
                                $skuValues = $productCondition['value'];
                                $skuValues = explode(",",$skuValues);
                                foreach ($skuValues as $skuValue) {
                                    $skus[] = $skuValue;
                                }
                            }
                        }
                    }
                }
            }
        }
        $uniqSkus = array_unique($skus);
        
        if(count($uniqSkus) > 0) {
            $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
            /** @var \Magento\Checkout\Model\Session $checkoutSession */
            $checkoutSession = $objectManager->get(\Magento\Checkout\Model\Session::class);
            $quote = $checkoutSession->getQuote();
            /** @var Magento\Quote\Model\Quote\Item\AbstractItem $quoteI */
            foreach($quote->getItems() as $quoteI) {
                if(in_array($quoteI->getSku(), $uniqSkus)) {
                    $getApplicableStepQty[] = $this->getApplicableDiscount($quoteI->getTotalQty(), $rule);
                }
            }

            $applicableQty = min($getApplicableStepQty) * $rule->getYProductDiscountQty();
            $maxDiscountQty = $rule->getDiscountQty();
            if(! $maxDiscountQty) {
                // if max qty is not set or zero
                $applicableQty = min($applicableQty, $item->getTotalQty());
            } else {
                $applicableQty = min($applicableQty, $maxDiscountQty);
            }

            if($applicableQty > 0 && $item->getTotalQty() >= $applicableQty) { 
                $itemPrice = $this->validator->getItemPrice($item);
                $baseItemPrice = $this->validator->getItemBasePrice($item);
                $itemOriginalPrice = $this->validator->getItemOriginalPrice($item);
                $baseItemOriginalPrice = $this->validator->getItemBaseOriginalPrice($item);

                $_rulePct = $rulePercent / 100;
                $discountData->setAmount(($applicableQty * $itemPrice - $item->getDiscountAmount()) * $_rulePct);
                $discountData->setBaseAmount(($applicableQty * $baseItemPrice - $item->getBaseDiscountAmount()) * $_rulePct);
                $discountData->setOriginalAmount(($applicableQty * $itemOriginalPrice - $item->getDiscountAmount()) * $_rulePct);
                $discountData->setBaseOriginalAmount(
                    ($applicableQty * $baseItemOriginalPrice - $item->getBaseDiscountAmount()) * $_rulePct
                );

                if ($maxDiscountQty >= $applicableQty || !$maxDiscountQty) {
                    $discountPercent = min(100, $item->getDiscountPercent() + $rulePercent);
                    $item->setDiscountPercent($discountPercent);
                }
            }
        }

        return $discountData;
    }

    /**
     * get applicable qty for given qty
     * @param float $qty
     * @param \Magento\SalesRule\Model\Rule $rule
     * @return float
     */
    private function getApplicableDiscount($qty, $rule)
    {
        $step = $rule->getDiscountStep();
        if ($step) {
            $qty = floor($qty / $step);
        }

        return $qty;
    }

}

Vendor\Module\view\adminhtml\ui_component\sales_rule_form.xml

<?xml version="1.0" encoding="UTF-8"?>
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <fieldset name="actions" sortOrder="30">
        <field name="y_product_discount_qty" formElement="input">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="source" xsi:type="string">sales_rule</item>
                    <item name="sortOrder" xsi:type="number">4</item>
                </item>
            </argument>
            <settings>
                <validation>
                    <rule name="required-entry" xsi:type="boolean">true</rule>
                    <rule name="validate-number" xsi:type="boolean">true</rule>
                    <rule name="validate-greater-than-zero" xsi:type="boolean">true</rule>
                </validation>
                <dataType>text</dataType>
                <label translate="true">Discount Qty (Get Y)</label>
                <dataScope>y_product_discount_qty</dataScope>
            </settings>
        </field>
    </fieldset>
</form>

now, In admin panel create cart price rule as same as below snapshot.

sales rule in admin panel

-: Issue :-

Magneto throw error of Exception #0 (LogicException): Infinite loop detected, review the trace for the looping path at BuyXGetNextYWithPercent.php:91 line which is $checkoutSession->getQuote().

-: what i need :- I need to get quote data in Sales Rule custom action class without any Infinite loop error.

How can we get quote data without any issue in cart price rule?

OR

Please help me about how to discount apply on another product Y based on X Product Step Qty

1 Answer 1

1

I've run your code, it applied the discount properly without errors.
Here is the result when applying code and all configs like your setting: https://i.stack.imgur.com/xMn3r.png

Not sure if you created Session class by Object Manager directory related to your error or not, but anyway, you should avoid using Object Manager to create a new class in your code.

In your specific case, you can get Quote from $item param in _calculate() method,
replace:

$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
/** @var \Magento\Checkout\Model\Session $checkoutSession */
$checkoutSession = $objectManager->get(\Magento\Checkout\Model\Session::class);
$quote = $checkoutSession->getQuote();

with

$quote = $item->getQuote();

After that, remove generated class, compile code, and clear cache.

Edit
Suggestion from the original poster:
Use $quote->getAllVisibleItems() instead of $quote->getItems() to get quote item data.
Replace

foreach($quote->getItems() as $quoteI) {
    if(in_array($quoteI->getSku(), $uniqSkus)) {
        $getApplicableStepQty[] = $this->getApplicableDiscount($quoteI->getTotalQty(), $rule);
    }
}

with

$quoteItems = $quote->getAllVisibleItems();
foreach($quoteItems as $quoteI) {
    if (in_array($quoteI->getSku(), $uniqSkus)) {
        $getApplicableStepQty[] = $this->getApplicableDiscount($quoteI->getTotalQty(), $rule);
    }
}
10
  • If your issue still persists, please post a full exception error trace in your post. I'll take a look.
    – Tu Van
    Dec 31, 2022 at 10:27
  • 1
    okay, I will check it again. but at currently I'm busy in other tasks. but I will comeback & try again it. If I not found any issue then I will accept your solution Jan 2, 2023 at 5:04
  • 1
    Sure, feel free to do that, I'll review it.
    – Tu Van
    Jan 3, 2023 at 12:14
  • 1
    @HarshPatel You suggested adding $quoteItems = $quote->getAllVisibleItems(); into the replace... with section is not correct because $quoteItems does not exist in your code. That code should be added to another section and describe that use $quote->getAllVisibleItems() instead of $quote->getItems() to get quote item data for improvement. I've added it to my answer.
    – Tu Van
    Jan 5, 2023 at 18:53
  • 1
    Anyway, glad to see your issue resolved. Cheers!
    – Tu Van
    Jan 5, 2023 at 19:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.