diff --git a/Observer/RemoveGiftItems.php b/Observer/RemoveGiftItems.php index 053a90b..cebb9ae 100644 --- a/Observer/RemoveGiftItems.php +++ b/Observer/RemoveGiftItems.php @@ -34,7 +34,7 @@ public function __construct(CheckoutSession $checkoutSession) /** * Delete all gift items. They will be re-added by SalesRule (If possible). * - * @event sales_quote_address_collect_totals_before + * @event sales_quote_remove_item * @param Observer $observer * @return void */ diff --git a/Observer/ResetGiftItems.php b/Observer/ResetGiftItems.php index c856b72..389026f 100644 --- a/Observer/ResetGiftItems.php +++ b/Observer/ResetGiftItems.php @@ -2,6 +2,7 @@ namespace C4B\FreeProduct\Observer; +use C4B\FreeProduct\SalesRule\Action\ForeachGiftAction; use C4B\FreeProduct\SalesRule\Action\GiftAction; use Magento\Framework\Event\Observer; @@ -10,9 +11,19 @@ use Magento\Quote\Model\Quote; /** - * Observer for resetting gift cart items + * Observer for resetting gift cart items. + * When quote totals are collected, all gifts are removed and are later re-added by Discount total collector. + * It is triggered by two events: + * - quote collect before: for normal quote operations (adding items, changing qty, removing item) + * - address collect before: When shipping is estimated the above event is not triggered. + * + * There is some weird handling of quote items. There are two ways to get them: getItems() and getItemsCollection() + * New quote items are added into the collection, but not into getItems. This is apparently how it should be because + * otherwise newly added quote items are added again since they don't have an item_id yet and in case of bundle items this would fail. + * So quote->setItems should not be used here: + * @see \Magento\Quote\Model\QuoteRepository\SaveHandler::save + * @see \Magento\Quote\Model\Quote\Item\CartItemPersister::save * - * @category C4B * @package C4B_FreeProduct * @author Dominik Meglič * @copyright code4business Software GmbH @@ -21,45 +32,77 @@ class ResetGiftItems implements ObserverInterface { /** - * Delete all gift items. They will be re-added by SalesRule (If possible). - * + * @var bool + */ + private $areGiftItemsReset = false; + + /** + * @event sales_quote_collect_totals_before * @event sales_quote_address_collect_totals_before * @param Observer $observer * @return void + * @throws \Exception */ - public function execute(\Magento\Framework\Event\Observer $observer) + public function execute(Observer $observer) { - /** @var ShippingAssignmentInterface $shippingAssignment */ - $shippingAssignment = $observer->getEvent()->getData('shipping_assignment'); /** @var Quote $quote */ $quote = $observer->getEvent()->getData('quote'); - /** @var Quote\Address $address */ - $address = $shippingAssignment->getShipping()->getAddress(); + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $observer->getEvent()->getData('shipping_assignment'); - if ($shippingAssignment->getItems() == null || $address->getAddressType() != Quote\Address::TYPE_SHIPPING) + if ($quote->getItems() == null || $this->areGiftItemsReset) { return; } - $newShippingAssignmentItems = $this->removeOldGiftQuoteItems($shippingAssignment); + if ($shippingAssignment instanceof ShippingAssignmentInterface) + { + /** @var Quote\Address $address */ + $address = $shippingAssignment->getShipping()->getAddress(); - $shippingAssignment->setItems($newShippingAssignmentItems); + if ($address->getAddressType() != Quote\Address::ADDRESS_TYPE_SHIPPING) + { + return; + } + } + else + { + $address = $quote->getShippingAddress(); + } + + $realQuoteItems = $this->removeOldGiftQuoteItems($quote->getItemsCollection()); + $this->areGiftItemsReset = true; $address->unsetData(GiftAction::APPLIED_FREEPRODUCT_RULE_IDS); $address->unsetData('cached_items_all'); - $this->updateExtensionAttributes($quote, $shippingAssignment); + if ($shippingAssignment instanceof ShippingAssignmentInterface) + { + $shippingAssignment->setItems($realQuoteItems); + $this->updateExtensionAttributes($quote, $shippingAssignment); + } } /** - * @param ShippingAssignmentInterface $shippingAssignment - * @return array + * A new gift item was added so if cart totals are collected again, all gift items will be reset. + * + * @return void + */ + public function reportGiftItemAdded() + { + $this->areGiftItemsReset = false; + } + + /** + * @param \Magento\Quote\Model\ResourceModel\Quote\Item\Collection|\Magento\Framework\Data\Collection $quoteItemsCollection + * @return Quote\Item[] + * @throws \Exception */ - protected function removeOldGiftQuoteItems($shippingAssignment): array + protected function removeOldGiftQuoteItems($quoteItemsCollection) { - $newShippingAssignment = []; + $realQuoteItems = []; /** @var Quote\Item $quoteItem */ - foreach ($shippingAssignment->getItems() as $quoteItem) + foreach ($quoteItemsCollection->getItems() as $key => $quoteItem) { if ($quoteItem->isDeleted()) { @@ -78,15 +121,12 @@ protected function removeOldGiftQuoteItems($shippingAssignment): array } } else { - /** - * Reset shipping assignment to prevent others from working on old items - * @see \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector::processAppliedTaxes - * @see \Magento\Tax\Model\Plugin\OrderSave::saveOrderTax - */ - $newShippingAssignment[] = $quoteItem; + $quoteItem->unsetData(ForeachGiftAction::APPLIED_FREEPRODUCT_RULE_IDS); + $realQuoteItems[$key] = $quoteItem; } } - return $newShippingAssignment; + + return $realQuoteItems; } /** diff --git a/Plugin/SalesRule/Model/MetadataValueProvider.php b/Plugin/SalesRule/Model/MetadataValueProvider.php index d9244ac..f3cbf5b 100644 --- a/Plugin/SalesRule/Model/MetadataValueProvider.php +++ b/Plugin/SalesRule/Model/MetadataValueProvider.php @@ -3,6 +3,8 @@ namespace C4B\FreeProduct\Plugin\SalesRule\Model; use C4B\FreeProduct\SalesRule\Action\GiftAction; +use C4B\FreeProduct\SalesRule\Action\ForeachGiftAction; + use \Magento\SalesRule\Model\Rule\Metadata\ValueProvider as Source; /** @@ -28,6 +30,9 @@ public function afterGetMetadataValues(Source $subject, $resultMetadataValues) $resultMetadataValues['actions']['children']['simple_action']['arguments']['data']['config']['options'][] = [ 'label' => __('Add a Gift'), 'value' => GiftAction::ACTION ]; + $resultMetadataValues['actions']['children']['simple_action']['arguments']['data']['config']['options'][] = [ + 'label' => __('Add a Gift (For each cart item)'), 'value' => ForeachGiftAction::ACTION + ]; return $resultMetadataValues; } diff --git a/README.md b/README.md index 6b755c0..60e8c7d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The development and the function of the original Magento1 extension is described Requirements ------- -- PHP >= 7.0.0 +- PHP >= 7.1 - Magento >= 2.2 Supported Product Types @@ -35,8 +35,13 @@ Configuration ------- Sales rules for carts are configured in _Marketing->Cart Price Rules_: - In the Actions tab, the Apply field should be set to Add a Gift -- Gift SKU: Product that will be added. Only simple and virtual products without (required) custom options are supported -- Discount Amount: The qty of added gifts +- Gift SKU: Product that will be added. Only simple and virtual products without (required) custom options are supported. Multiple comma-separated SKUs can be specified +- Discount Amount: The qty of added gifts +- The gift item is added once for the whole cart + +Action **Add a Gift (for each cart item)** works similarly but will add the gift item for each product in cart. The qty of said product is also taken into consideration. + +This action usually needs conditions to match only specific items *(Apply the rule only to cart items matching the following conditions)*. Limitations: ------- diff --git a/SalesRule/Action/ForeachGiftAction.php b/SalesRule/Action/ForeachGiftAction.php new file mode 100644 index 0000000..190aca5 --- /dev/null +++ b/SalesRule/Action/ForeachGiftAction.php @@ -0,0 +1,117 @@ + + * @copyright code4business Software GmbH + * @license http://opensource.org/licenses/osl-3.0.php + */ +class ForeachGiftAction extends GiftAction +{ + const ACTION = 'add_gift_foreach'; + /** + * @var ResetGiftItems + */ + private $resetGiftItems; + /** + * @var LoggerInterface + */ + private $logger; + + + /** + * @param Discount\DataFactory $discountDataFactory + * @param ProductRepositoryInterface $productRepository + * @param ResetGiftItems $resetGiftItems + * @param LoggerInterface $logger + */ + public function __construct(Discount\DataFactory $discountDataFactory, + ProductRepositoryInterface $productRepository, + ResetGiftItems $resetGiftItems, + LoggerInterface $logger) + { + parent::__construct($discountDataFactory, $productRepository, $resetGiftItems, $logger); + $this->resetGiftItems = $resetGiftItems; + $this->logger = $logger; + } + + /** + * @param \Magento\SalesRule\Model\Rule $rule + * @param AbstractItem $item + * @param float $qty + * @return Discount\Data + */ + public function calculate($rule, $item, $qty) + { + $appliedRuleIds = $item->getData(static::APPLIED_FREEPRODUCT_RULE_IDS); + + if ($item->getAddress()->getAddressType() != Address::TYPE_SHIPPING + || ($appliedRuleIds != null && isset($appliedRuleIds[$rule->getId()]))) + { + return $this->getDiscountData($item); + } + + $skus = explode(',', $rule->getData(static::RULE_DATA_KEY_SKU)); + $isRuleAdded = false; + + foreach ($skus as $sku) + { + try + { + $quoteItem = $item->getQuote()->addProduct($this->getGiftProduct($sku), $rule->getDiscountAmount() * $qty); + $item->getQuote()->setItemsCount($item->getQuote()->getItemsCount() + 1); + $item->getQuote()->setItemsQty((float)$item->getQuote()->getItemsQty() + $quoteItem->getQty()); + $this->resetGiftItems->reportGiftItemAdded(); + + if (is_string($quoteItem)) + { + throw new \Exception($quoteItem); + } + + $isRuleAdded = true; + } catch (\Exception $e) + { + $this->logger->error( + sprintf('Exception occurred while adding gift product %s to cart. Rule: %d, Exception: %s', implode(',', $skus), $rule->getId(), $e->getMessage()), + [__METHOD__] + ); + } + } + if ($isRuleAdded) + { + $this->addAppliedRuleIdToItem($rule->getRuleId(), $item); + } + + return $this->getDiscountData($item); + } + + /** + * @param int $ruleId + * @param AbstractItem $quoteItem + */ + protected function addAppliedRuleIdToItem(int $ruleId, AbstractItem $quoteItem) + { + $appliedRules = $quoteItem->getData(static::APPLIED_FREEPRODUCT_RULE_IDS); + + if ($appliedRules == null) + { + $appliedRules = []; + } + + $appliedRules[$ruleId] = $ruleId; + + $quoteItem->setData(static::APPLIED_FREEPRODUCT_RULE_IDS, $appliedRules); + } +} \ No newline at end of file diff --git a/SalesRule/Action/GiftAction.php b/SalesRule/Action/GiftAction.php index 5b56bc9..bd3038c 100644 --- a/SalesRule/Action/GiftAction.php +++ b/SalesRule/Action/GiftAction.php @@ -2,6 +2,8 @@ namespace C4B\FreeProduct\SalesRule\Action; +use C4B\FreeProduct\Observer\ResetGiftItems; + use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; @@ -15,7 +17,6 @@ /** * Handles applying a "Add a Gift" type SalesRule. * - * @category C4B * @package C4B_FreeProduct * @author Dominik Meglič * @copyright code4business Software GmbH @@ -29,7 +30,6 @@ class GiftAction implements Discount\DiscountInterface const RULE_DATA_KEY_SKU = 'gift_sku'; const PRODUCT_TYPE_FREEPRODUCT = 'freeproduct_gift'; const APPLIED_FREEPRODUCT_RULE_IDS = '_freeproduct_applied_rules'; - /** * @var Discount\DataFactory */ @@ -38,26 +38,36 @@ class GiftAction implements Discount\DiscountInterface * @var ProductRepositoryInterface */ private $productRepository; + /** + * @var ResetGiftItems + */ + private $resetGiftItems; /** * @var LoggerInterface */ private $logger; + /** * @param Discount\DataFactory $discountDataFactory * @param ProductRepositoryInterface $productRepository + * @param ResetGiftItems $resetGiftItems * @param LoggerInterface $logger */ public function __construct(Discount\DataFactory $discountDataFactory, ProductRepositoryInterface $productRepository, + ResetGiftItems $resetGiftItems, LoggerInterface $logger) { $this->discountDataFactory = $discountDataFactory; $this->productRepository = $productRepository; + $this->resetGiftItems = $resetGiftItems; $this->logger = $logger; } /** + * Add gift product to quote, if not yet added + * * @param \Magento\SalesRule\Model\Rule $rule * @param AbstractItem $item * @param float $qty @@ -73,24 +83,36 @@ public function calculate($rule, $item, $qty) return $this->getDiscountData($item); } - $sku = $rule->getData(static::RULE_DATA_KEY_SKU); + $skus = explode(',', $rule->getData(static::RULE_DATA_KEY_SKU)); + $isRuleAdded = false; - try + foreach ($skus as $sku) { - $quoteItem = $item->getQuote()->addProduct($this->getGiftProduct($sku), $rule->getDiscountAmount()); - - if (is_string($quoteItem)) + try + { + $quoteItem = $item->getQuote()->addProduct($this->getGiftProduct($sku), $rule->getDiscountAmount()); + $item->getQuote()->setItemsCount($item->getQuote()->getItemsCount() + 1); + $item->getQuote()->setItemsQty((float)$item->getQuote()->getItemsQty() + $quoteItem->getQty()); + $this->resetGiftItems->reportGiftItemAdded(); + + if (is_string($quoteItem)) + { + throw new \Exception($quoteItem); + } + + $isRuleAdded = true; + } catch (\Exception $e) { - throw new \Exception($quoteItem); + $this->logger->error( + sprintf('Exception occurred while adding gift product %s to cart. Rule: %d, Exception: %s', implode(',', $skus), $rule->getId(), $e->getMessage()), + [__METHOD__] + ); } + } - $this->addAppliedRuleId($rule->getRuleId(), $item->getAddress()); - } catch (\Exception $e) + if ($isRuleAdded) { - $this->logger->error( - sprintf('Exception occurred while adding gift product %s to cart. Rule: %d, Exception: %s', $sku, $rule->getId(), $e->getMessage()), - [__METHOD__] - ); + $this->addAppliedRuleId($rule->getRuleId(), $item->getAddress()); } return $this->getDiscountData($item); diff --git a/composer.json b/composer.json index 1a57829..3b5407f 100644 --- a/composer.json +++ b/composer.json @@ -5,14 +5,14 @@ "homepage": "https://github.com/code4business/freeproduct2", "require": { "magento/module-sales-rule": ">=100.0.0", - "php": ">=7.0" + "php": ">=7.1" }, "require-dev": { "tddwizard/magento2-fixtures": "^0.1.0", "phpunit/phpunit": "~6.2.0" }, "type": "magento2-module", - "version": "1.0.5", + "version": "1.2.0", "license": [ "OSL-3.0" ], diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml index d6726f3..2104586 100644 --- a/etc/adminhtml/di.xml +++ b/etc/adminhtml/di.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/etc/di.xml b/etc/di.xml index 882ecc5..883a6b2 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -4,6 +4,7 @@ C4B\FreeProduct\SalesRule\Action\GiftAction + C4B\FreeProduct\SalesRule\Action\ForeachGiftAction diff --git a/etc/events.xml b/etc/events.xml index 3788e82..6311900 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -1,9 +1,14 @@ + + + + - + + - + \ No newline at end of file diff --git a/i18n/en_US.csv b/i18n/en_US.csv index bfb21a6..1368abc 100644 --- a/i18n/en_US.csv +++ b/i18n/en_US.csv @@ -1,3 +1,4 @@ "Add a Gift","Add a Gift" +"Add a Gift (For each cart item)","Add a Gift (For each cart item)" "Gift SKU","Gift SKU" -"Only simple and virtual types are supported.","Only simple and virtual types are supported." \ No newline at end of file +"Only simple and virtual types are supported.","Only simple and virtual types are supported." diff --git a/i18n/sl_SI.csv b/i18n/sl_SI.csv index 30c2623..821663d 100644 --- a/i18n/sl_SI.csv +++ b/i18n/sl_SI.csv @@ -1,3 +1,4 @@ "Add a Gift","Dodaj darilo" +"Add a Gift (For each cart item)","Dodaj darilo (Za vsak artikel v košarici)" "Gift SKU","SKU darila" -"Only simple and virtual types are supported.","Podprta sta samo enostavni in virtualni tip artikla." \ No newline at end of file +"Only simple and virtual types are supported.","Podprta sta samo enostavni in virtualni tip artikla."