Question

By default catalogrule promotions are applied to price field. Changing this was one classic request in most Magento 1 projects we have managed

One approach could be to apply catalogrule promotions to the lowest value of price & special_price, for instance, this way https://stackoverflow.com/questions/18120342/catalog-price-rules-applied-to-special-price

A better, more complete, approach could be having a price selector when editing catalogrule rules, so admin users can choose which price they want to apply for the rule they are editing

In Magento2, I have seen price value seems to be retrieved in Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder::build() method

$priceAttr = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'price');

This is called, among other places, in Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice::execute() method, which seems to be responsible for catalog rule prices updates

So, the easy part (using special_price instead of price) apparently could be achieved just rewriting that Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder::build() method

Any ideas to achieve the hard one?

Was it helpful?

Solution

I post our solution, after some working days

This module install a new attribute for catalog rules (price_source, which is a selector of price / special_price)

Then, we rewrite the needed classes to add special_price values when calculating the price when applying the catalog rules

app/code/Sinapsis/CatalogRule/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">
    <preference for="Magento\CatalogRule\Model\Indexer\ProductPriceCalculator" type="Sinapsis\CatalogRule\Model\Indexer\ProductPriceCalculator" />
    <preference for="Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder" type="Sinapsis\CatalogRule\Model\Indexer\RuleProductsSelectBuilder" />
</config>

app/code/Sinapsis/CatalogRule/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="module.xsd">
    <module name="Sinapsis_CatalogRule" setup_version="1.0.0">
    </module>
</config>

app/code/Sinapsis/CatalogRule/Model/Indexer/ProductPriceCalculator.php

...
public function calculate($ruleData, $productData = null)
{
    if ($productData !== null && isset($productData['rule_price'])) {
        $productPrice = $productData['rule_price'];
    } else {
        // get price source from rule
        $productPrice = $ruleData['default_price'];
        if ($ruleData['price_source'] == 2) {
            $productPrice = $ruleData['default_special_price'];
        }
    }
    ...

app/code/Sinapsis/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php

...
public function build(
    $websiteId,
    \Magento\Catalog\Model\Product $product = null,
    $useAdditionalTable = false
) {
    ...

    if ($product && $product->getEntityId()) {
        $select->where('rp.product_id=?', $product->getEntityId());
    }

    // join price_source
    $select->joinInner(
        ['catalogrule' => $this->resource->getTableName('catalogrule')],
        'catalogrule.rule_id=rp.rule_id',
        ['price_source']
    );

    $linkField = $this->metadataPool
        ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class)
        ->getLinkField();
    $select->join(
        ['e' => $this->resource->getTableName('catalog_product_entity')],
        sprintf('e.entity_id = rp.product_id'),
        []
    );

    /**
     * Join default price and websites prices to result
     */

    foreach (array('price', 'special_price') as $sourcePrice) {
        $priceTableAlias = 'pp_default';
        if ($sourcePrice == 'special_price') {
            $priceTableAlias = 'sp_default';
        }

        $priceAttr = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $sourcePrice);
        $priceTable = $priceAttr->getBackend()->getTable();
        $attributeId = $priceAttr->getId();

        $joinCondition = '%1$s.' . $linkField . '=e.' . $linkField . ' AND (%1$s.attribute_id='
            . $attributeId
            . ') and %1$s.store_id=%2$s';

        $select->join(
            [$priceTableAlias => $priceTable],
            sprintf($joinCondition, $priceTableAlias, \Magento\Store\Model\Store::DEFAULT_STORE_ID),
            []
        );
    }

    $website = $this->storeManager->getWebsite($websiteId);
    $defaultGroup = $website->getDefaultGroup();
    if ($defaultGroup instanceof \Magento\Store\Model\Group) {
        $storeId = $defaultGroup->getDefaultStoreId();
    } else {
        $storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID;
    }

    $select->joinInner(
        ['product_website' => $this->resource->getTableName('catalog_product_website')],
        'product_website.product_id=rp.product_id '
        . 'AND product_website.website_id = rp.website_id '
        . 'AND product_website.website_id='
        . $websiteId,
        []
    );

    foreach (array('price', 'special_price') as $sourcePrice) {
        $priceKey = 'pp';
        if ($sourcePrice == 'special_price') {
            $priceKey = 'sp';
        }
        $priceTableAlias = $priceKey . '_default';

        $priceAttr = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $sourcePrice);
        $priceTable = $priceAttr->getBackend()->getTable();

        $tableAlias = $priceKey . $websiteId;
        $select->joinLeft(
            [$tableAlias => $priceTable],
            sprintf($joinCondition, $tableAlias, $storeId),
            []
        );
        $select->columns([
            'default_' . $sourcePrice => $connection->getIfNullSql($tableAlias . '.value', $priceTableAlias .'.value'),
        ]);
    }
    return $connection->query($select);
}

app/code/Sinapsis/CatalogRule/Model/PriceOptions.php

<?php
namespace Sinapsis\CatalogRule\Model;

use Magento\Framework\Data\OptionSourceInterface;

class PriceOptions implements OptionSourceInterface
{

    public function toOptionArray()
    {
        return [
            ['label' => __('Price'), 'value' => '1'],
            ['label' => __('Special price'), 'value' => '2']
        ];
    }
}

app/code/Sinapsis/CatalogRule/Setup/InstallSchema.php

<?php
namespace Sinapsis\CatalogRule\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

class InstallSchema implements InstallSchemaInterface
{

    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;
        $installer->startSetup();

        $installer->getConnection()->addColumn(
            $installer->getTable('catalogrule'),
            'price_source',
            [
                'type' => \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
                'length' => 1,
                'nullable' => false,
                'default' => 1,
                'comment' => 'Price Source'
            ]
        );

        $installer->endSetup();
    }
}

app/code/Sinapsis/CatalogRule/view/adminhtml/ui_component/catalog_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="rule_information">
        <field name="price_source" formElement="select">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="source" xsi:type="string">catalog_rule</item>
                </item>
            </argument>
            <settings>
                <dataType>text</dataType>
                <label translate="true">Price Source</label>
                <visible>true</visible>
                <dataScope>price_source</dataScope>
                <validation>
                    <rule name="required-entry" xsi:type="boolean">true</rule>
                </validation>
            </settings>
            <formElements>
                <select>
                    <settings>
                        <options class="Sinapsis\CatalogRule\Model\PriceOptions"/>
                    </settings>
                </select>
            </formElements>
        </field>
    </fieldset>
</form>

app/code/Sinapsis/CatalogRule/registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Sinapsis_CatalogRule',
    __DIR__
);
Licensed under: CC-BY-SA with attribution
Not affiliated with magento.stackexchange
scroll top