Magento - Reassign catalog rules after modifying product attributes - php

I'm in the process of developing an extension for Magento 1.5.1.0, which allows me to add catalog price rules to products which quantity in stock is reduced to zero. I have added an attribute to my attribute-set called auto_discount_active. This attribute is my on/off switch which works as condition for my price rule.
I wrote an Observer that reacts on the events sales_order_place_after and catalog_product_save_before. It's task is to check wether to stock quantity of the current product has been changed and set my custom attribute to on or off.
The method which handles the catalog_product_save_before event works fine. After saving an article in the backend, the price rule becomes (in)active like it should. The code looks like following:
class Company_AutoDiscount_Model_Observer
{
public function updateAutoDiscount($observer)
{
/**
* #var Varien_Event
*/
$event = $observer->getEvent();
$product = $event->getProduct();
$data = $product->getStockData();
$discount = $data['qty'] < 1 ? true : false;
$attributes = $product->getAttributes();
$attribute = $attributes["auto_discount_active"];
if ($product->getAutoDiscountAllowed())
{
$product->setAutoDiscountActive($discount);
}
return $this;
}
}
Now I want to do the same thing, if someone places an order in my shop. That for I use the event sales_order_place_after which works so far. But after changing the custom attributes value, the price rules are not updated. My observer method looks like this:
public function updateAutoDiscountAfterOrder($observer)
{
/**
* #var Varien_Event
*/
$event = $observer->getEvent();
$order = $event->getOrder();
foreach ($order->getItemsCollection() as $item)
{
$productId = $item->getProductId();
$productIds[] = $productId;
$product = Mage::getModel('catalog/product')->setStoreId($order->getStoreId())->load($productId);
$data = $product->getStockData();
$discount = $data['qty'] < 1 ? true : false;
if ($product->getAutoDiscountAllowed())
{
$product->setAutoDiscountActive($discount);
$product->save();
}
Mage::getModel('catalogrule/rule')->applyAllRulesToProduct($productId);
}
return $this;
}
After placing an order and saving the bought article manually in the backend without changes, the price rule gets updated. But I have get the update working in my observer method.
What do I have to do to get the catalog price rule being assigned, after changing the custom attribute?
Thx in advance!

Okay, I want to advise you on some fairly major code optimisations.
You can reduce your collection size and remove the conditional logic inside your loop by using:
$order->getItemsCollection()->addFieldToFilter('is_in_stock', 0);
You could also update all the attributes with a much faster method than save(), by using:
Mage::getSingleton('catalog/product_action')
->updateAttributes($order->getItemsCollection()->addFieldToFilter('is_in_stock', 0)->getAllIds(), array('auto_discount_active' => 1), 0);
Also, bear in mind, you'll also need to apply your observer to any product stock level modification, ie. product save, import, credit memo (refund) - so its a fairly expansive area. You would probably be better served rewriting the stock class, as there isn't too many events dispatched that will give you enough scope to cover this.
Finally, to perform the assignation of rules, I would suggest extending the resource model for the rule (Mage/CatalogRule/Model/Mysql4/Rule.php) so that you can pass in your array of product ids (to save it iterating through the entire catalogue).
You could simply extend getRuleProductIds() to take a Mage::registry variable (if set) with your product ids from the collection above. Then after running the code above, you could just execute
Mage::getModel('catalogrule/rule')->load(myruleid)->save();
Which will re-index and apply rules to new products as necessary - for only the products that have changed.
I would imagine this method cutting overheads by an extremely significant amount.

Related

Magento 2: Get Parent Category ID of Category ID

How does one get the parent category ID of a category ID in Magento 2?
In Magento 1, I did it with the following:
$product_id = 101; //for example
$product = Mage::getModel('catalog/product')->load($product_id); //get product object by product ID
$category_ids = $product->getCategoryIds(); //array of all categories that the product is in
foreach ($category_ids as $cat_ids) {
$parent_id = Mage::getModel('catalog/category')->load($cat_id)->getParentId(); //
echo $parent_id; //outputs an int ID of parent category
}
In Magento 2, I've been attempting the same with the following:
$product_id = 101; //again, for example
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$productRepository = $objectManager->create('\Magento\Catalog\Model\ProductRepository');
$product = $productRepository->getById($product_id); //get product object by product ID
$category_ids = $product->getCategoryIds(); //array of all categories that the product is in
foreach ($category_ids as $cat_ids) {
echo $cat_ids;
}
Up to here, my code is working perfectly and the $category_ids is an array of all the categories that the product is in. However I cannot figure out how to get the parent category IDs of each child category ID in the $category_ids array.
NOTICE* I'm aware that I'm not officially supposed to directly used the ObjectManager, so please save this from your answer. I am seeking to specifically use the ObjectManager in this manner to iterate over $category_ids and load the parent category IDs for each child category ID.
Like so often, there are multiple ways to achieve this.
The CategoryFactory route
To load a category directly, you load it via the Factory Singleton responsible for the \Magento\Catalog\Model\Category class. This is the \Magento\Catalog\Model\CategoryFactory class. From each instance of Category, you can simple call the method getParentId() to get the parent ID.
foreach ($categoryIds as $categoryId) {
try {
$category = $this->_categoryFactory->create()->load($categoryId);
} catch (\Exception $e) {
/* Handle this appropriately... */
}
echo 'Parent Category ID: ', $category->getParentId(), PHP_EOL;
}
In this example, $categoryIds is the array of Category IDs you extracted from your \Magento\Catalog\Model\Product instance.
The CategoryRepository route
Or preferably you can use a Singleton instance of the \Magento\Catalog\Model\CategoryRepository class as a wrapper around the Factory. It will handle all the loading with some added error handling and it will also store a reference to the loaded category for later reuse. So if you are doing this multiple times during one execution, or suspect that you will load the same category later on, using the Repository will optimize your performance.
foreach ($categoryIds as $categoryId) {
try {
$category = $this->_categoryRepository->get($categoryId);
} catch (\Exception $e) {
/* Handle this appropriately... */
}
echo 'Parent Category ID: ', $category->getParentId(), PHP_EOL;
}
The Collection route
This should be a much faster route, as you (1) load all categories once from database instead of using several multiple sql calls in the backend and (2) you have some control over what is populated in the Category, and what is left out. Please be aware, that pretty much only what you put in addAttributeToSelect() will be populated in the Collection. But if you're only after the parent_id this should not be an issue.
First, make sure you are familiar with collections, then acquire a CollectionFactory Singleton for Magento\Catalog\Model\ResourceModel\Category\CollectionFactory and then populate it like so:
/** #var \Magento\Catalog\Model\ResourceModel\Category */
$collection = $this->_categoryCollectionFactory->create();
# Specifically select the parent_id attribute
$collection->addAttributeToSelect('parent_id');
# Only select categories with certain entity_ids (category ids)
$collection->addFieldToFilter('entity_id', ['in' => $categoryIds])
# Iterate over results and print them out!
foreach ($collection as $category) {
echo 'Parent Category ID: ', $category->getParentId(), PHP_EOL;
}
With great powers comes great risk, however. This above code will have no error correction whatsoever. If there is a logical database error, such as a product which points to a missing category, this category will just be omitted from the collection and it will be up to you as a programmer to spot that and deal with it. Also, you will have to decide for yourself on how you are handling store view and active/inactive categories via filters to the collection.
The Direct Database route
Ok, I would not recommend this route unless you know exactly what you are doing, and are in desperate need for performance.
This will be crazy-fast, but there are all sorts of problems, like relying on the underlying data storage and data structure, not to mention that you are open to (very unlikely, to be fair) future updates to the underlying database structure, either directly via Magento upgrades or via (nasty) 3rd party modules. Not the mention the dangers of SQL injections or XSS attacks. (Though, you should always keep this in mind, with all 4 methods.)
As you are using the ObjectManager directly, I assume you won't mind these drawbacks, however, so I though I'd give you this option as well.
The basic pseudo-sql is:
select parent_id from <name of catalog_category_entity table> where entity_id in (<sanitized, comma-separated list of category ids);
First, acquire an instance of the \Magento\Framework\App\ResourceConnection class. You will use this to get the necessary table name for catalog_category_entity, as well as getting the database connection. Then you should sanitize your data and finally, the bind and execute the query and fetch your data.
/** #var \Magento\Framework\App\Connection */
$connection = $this->_resourceConnection->getConnection();
# Get prefixed table name of catalog_category_entity
$categoryEntityTableName = $this->_resourceConnection->getTableName('catalog_category_entity');
# Sanitize the $categoryIds array using a bit of overkill
array_walk_recursive($categoryIds, function(&$value, $key){
$value = filter_var($value, FILTER_SANITIZE_NUMBER_INT);
});
# Prepare a sql statement which fetches entity_id and parent_id
$preparedStatement = $this->connection->prepare('select entity_id, parent_id from ' . $categoryEntityTableName . ' where entity_id in (' . implode(',', array_fill(0, sizeof($categoryIds), '?')) . ')');
# Bind sanitized $categoryIds array to statement and execute said statement in one single step
$preparedStatement->execute($categoryIds);
# fetch result as a key-value pair array of entity_id=>parent_id
$parentIds = $preparedStatement->fetchAll(\PDO::FETCH_KEY_PAIR);
# Iterate over results and print them out!
foreach ($parentIds as $categoryId => $parentId) {
echo 'Parent Category ID: ', (int)$parentId, PHP_EOL;
}
Footnote
I assume you are well aware of the pros and cons of using the ObjectManager directly, so I'll spare you the lecture ;-). However, for future reference I'll also have to state to future readers stumbling upon this answer that if they are unaware on how to acquire instances of the CategoryFactory, CategoryRepository, CollectionFactory or ResourceConnection classes, I highly recommend them to do so via the intended Dependency Injection mechanism.

Eloquent Collections, how to modify the data of each registry contained in results?

I have the following situation, Im trying to modify the price of products displayed in a platform.
Everything works ok for only 1 product (eg: product view) but I dont know what I have to do in order to modify the price of each product in an eloquent collection.
this is the code in my app:
ProductRepository.php:
public function CalcPrice($product){
$x = $product->price; //eg 5
$y = 4;
$amount= $x + $y;
return $amount;
}
For the details view of each product inside ProductController I have the following code and everything works perfect:
public function details($id){
$product = $this->product->getProductById($id);
$productprice = $this->product->getCalcPrice($product = $product);
return view('products.view',compact('product','productprice'))
}
On the other hand, my idea is to use the code contained in ProductRepository.php function CalcPrice in a collection.
My main doubt is what do I have to do, because in a collection probably I can have a variable $category in order to retrieve all products in a category, but I will not have a variable for each $product (for eg: a $productid like in details).
What can I do in order to eg:
modify each product price contained in a collection of a category
using CalcPrice function code?
eg: of code:
productrepository.php
public function AllProductsInCategory($catid)
{
return App\Product::where('categoryid', $catid)
->get();
}
but each product displaying their ($product->price + 4) as CalcPrice performs. thanks!.
You can achieve this by defining an attribute accessor on model and append it to model. This way it would be available for you on each instance like its other attributes.
As Taylor Otwell mentioned here, "This is intentional and for performance reasons." However there is an easy way to achieve this, say you have model named Product;
class Product extends Eloquent {
protected $appends = array('calc_price');
public function getCalcPriceAttribute()
{
//if you want to call your method for some reason
return $this->getCalcPrice($this);
// Otherwise more clean way would be something like this
// return $this->price + 4 // un-comment if you don't want to call getCalcPrice() method
}
}
Now you can access calculated price on each $product by simply calling $product->calc_price.

Attach simple products to a configurable product programmatically

I am trying to join some existing simple products programmatically to an existing configurable product.
I hardly found any hints / documentation on this. I examined the MAGMI Magento Mass Importer Plugin (in particular the magmi_productimportengine.php-file) with no success.
After that I found this snippet:
function attachProductToConfigurable($childProduct, $configurableProduct)
{
$loader = Mage::getResourceModel('catalog/product_type_configurable')
->load($configurableProduct, $configurableProduct->getId());
$ids = $configurableProduct
->getTypeInstance()
->getUsedProductIds();
$newids = array();
foreach ($ids as $id) {
$newids[$id] = 1;
}
$newids[$childProduct->getId()] = 1;
//$loader->saveProducts( $_configurableProduct->getid(), array_keys( $newids ) );
$loader->saveProducts($configurableProduct, array_keys($newids));
}
But when I am trying to call the function like this:
$sProduct = Mage::getModel('catalog/product')
->loadByAttribute('sku', $v);
$cProduct = Mage::getModel('catalog/product')
->loadByAttribute('sku', $sku);
attachProductToConfigurable($sProduct, $cProduct);
(each simple product SKU gets passed step by step to the configurable product)
Fatal error: Call to a member function getId() on a non-object in ... on line 1018
which is this line from the function itself
$loader = Mage::getResourceModel('catalog/product_type_configurable')
->load($configurableProduct, $configurableProduct
->getId());
Since I do not find anything similar to joining simple SKUs to an existing configurable product, I am stuck looking up what might be wrong upon initializing the function calls, resource models etc..
Any ideas on what to keep an eye on to get this going are highly appreciated.
Give this a try:
Mage::getResourceSingleton('catalog/product_type_configurable')
->saveProducts($mainConfigrableProduct, $simpleProductIds);
Where $mainConfigrableProduct must be an instance of the configurable product, and $simpleProductIds is an array with the ids of the simple products associated to the configurable products.
On a side note, be very careful when doing this. The simple products must be in the same attribute set as the configurable products. Here is what can happen if they are not.

CakePHP searchable tags

I have made an online shop for clothes and probably I need to make some tagging system.
The whole application is build on CakePHP and I need an idea for managing all the products, something similar to ebay.
For example to tag each product with it's price , type, producer, size , status
And for example some of them should be multi-searchable, to be able to search for an item with: price between $10 and $20, with size S or M
Have an attributes table that will basically act as a key/value storage and assign these attributes to each product.
Attributes itself could have an attribute_options table from where you can read the different available sizes for an attribute.
You'll then just have to search the attributes table and product table.
From what you describe, you shouldn't need any additional tables. Just add them as fields in the product table, and query based on that. It will be faster, more logically laid out...etc.
Your example would be searchable like below. (It seems a bit overkill, but will make any future finds really simple, and follows the fat controller, skinny model mantra:
//ProductsController
public function whatever() {
$opts = array(
'price_high' => 10,
'price_low' => 20,
'sizes' => array('S', 'M')
);
$this->Product->getProducts($opts);
}
//Product Model
public function getProduts($opts = null) {
//initialize variables
$params = array('conditions'=>array());
//size(s)
if(!empty($opts['sizes']) {
array_push($params['conditions'], array('Product.size'=>$opts['sizes']));
}
//price(s)
if(!empty($opts['price_high']) {
array_push($params['conditions'], array('Product.price <='=>$opts['price_high']));
}
if(!empty($opts['price_low']) {
array_push($params['conditions'], array('Product.price >='=>$opts['price_low']));
}
return $this->find('all', $params);
}

How do I check if a product is in comparison list magento

You can add product to compare. I have to show the link "Add to Compare" if the product is not added already otherwise show "Compare". I have to check if the product is in comparison list.
I have list.phtml file.
I tried this but this gives all the products added in comparison list.
$_productCollection = Mage::helper('catalog/product_compare')->getItemCollection()
I can loop through the returned products and can check if the product is in this collection but I am looking for a single call which take the product id or sku and return true or false accordingly.
I also added the filter like this but does not work
$_productCollection = Mage::helper('catalog/product_compare')->getItemCollection()
->addAttributeToFilter('sku', $item->getSku());
Try to use
Mage_Catalog_Model_Product_Compare_List
and its method:
getItemCollection
Like this:
$collection = Mage::getModel('catalog/product_compare_list')->getItemCollection();
$collection->.....Additional filters go here.
Why helper didn't worked? Because collection is already loaded there:
v 1.6
public function getItemCollection()
{
if (!$this->_itemCollection) {
$this->_itemCollection = Mage::getResourceModel('catalog/product_compare_item_collection')
->useProductItem(true)
->setStoreId(Mage::app()->getStore()->getId());
if (Mage::getSingleton('customer/session')->isLoggedIn()) {
$this->_itemCollection->setCustomerId(Mage::getSingleton('customer/session')->getCustomerId());
} elseif ($this->_customerId) {
$this->_itemCollection->setCustomerId($this->_customerId);
} else {
$this->_itemCollection->setVisitorId(Mage::getSingleton('log/visitor')->getId());
}
Mage::getSingleton('catalog/product_visibility')
->addVisibleInSiteFilterToCollection($this->_itemCollection);
/* Price data is added to consider item stock status using price index */
$this->_itemCollection->addPriceData();
$this->_itemCollection->addAttributeToSelect('name')
->addUrlRewrite()
->load();
/* update compare items count */
$this->_getSession()->setCatalogCompareItemsCount(count($this->_itemCollection));
}
return $this->_itemCollection;
}
So you can load collection by model and filter itself in template or in your own custom helper - model.

Categories