From 9b9b3f72c1b1ffdacb02f5f3fbef2320002d973e Mon Sep 17 00:00:00 2001
From: ah-net <103565001+ah-net@users.noreply.github.com>
Date: Tue, 15 Apr 2025 10:49:51 +0200
Subject: [PATCH 1/4] feat: calculate group/bundle prices
---
Model/Config.php | 12 ++
Model/Write/Price/IteratorInitializer.php | 1 +
.../Products/CollectionDecorator/Price.php | 201 +++++++++++++++++-
etc/adminhtml/system.xml | 18 ++
etc/config.xml | 2 +
5 files changed, 227 insertions(+), 7 deletions(-)
diff --git a/Model/Config.php b/Model/Config.php
index 151ae3c..23770d6 100644
--- a/Model/Config.php
+++ b/Model/Config.php
@@ -40,6 +40,8 @@ class Config
public const BATCH_SIZE_PRODUCTS = 'tweakwise/export/batch_size_products';
public const BATCH_SIZE_PRODUCTS_CHILDREN = 'tweakwise/export/batch_size_products_children';
public const PATH_SKIP_CHILD_BY_COMPOSITE_TYPE = 'tweakwise/export/skip_child_by_composite_type';
+ public const CALCULATE_COMPOSITE_PRICES = 'tweakwise/export/calculate_composite_prices';
+ public const ADD_TAX_TO_PRICES = 'tweakwise/export/add_tax_to_prices';
/**
* Default feed filename
@@ -305,4 +307,14 @@ public function getBatchSizeProductsChildren(): int
{
return (int) $this->config->getValue(self::BATCH_SIZE_PRODUCTS_CHILDREN);
}
+
+ public function calculateCombinedPrices($store = null): bool
+ {
+ return (bool) $this->config->isSetFlag(self::CALCULATE_COMPOSITE_PRICES, ScopeInterface::SCOPE_STORE, $store);
+ }
+
+ public function addVat($store = null): bool
+ {
+ return (bool) $this->config->isSetFlag(self::ADD_TAX_TO_PRICES, ScopeInterface::SCOPE_STORE, $store);
+ }
}
diff --git a/Model/Write/Price/IteratorInitializer.php b/Model/Write/Price/IteratorInitializer.php
index af25a32..505b3f1 100644
--- a/Model/Write/Price/IteratorInitializer.php
+++ b/Model/Write/Price/IteratorInitializer.php
@@ -34,5 +34,6 @@ public function initializeAttributes(EavIterator $iterator)
$iterator->selectAttribute('status');
$iterator->selectAttribute('visibility');
$iterator->selectAttribute('type_id');
+ $iterator->selectAttribute('tax_class_id');
}
}
diff --git a/Model/Write/Products/CollectionDecorator/Price.php b/Model/Write/Products/CollectionDecorator/Price.php
index c5f705e..6a4fbf7 100644
--- a/Model/Write/Products/CollectionDecorator/Price.php
+++ b/Model/Write/Products/CollectionDecorator/Price.php
@@ -3,6 +3,10 @@
namespace Tweakwise\Magento2TweakwiseExport\Model\Write\Products\CollectionDecorator;
// phpcs:disable Magento2.Legacy.RestrictedCode.ZendDbSelect
+use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Tax\Model\Calculation;
+use Tweakwise\Magento2TweakwiseExport\Exception\InvalidArgumentException;
use Tweakwise\Magento2TweakwiseExport\Model\Config;
use Tweakwise\Magento2TweakwiseExport\Model\Write\Products\Collection;
use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
@@ -10,9 +14,11 @@
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Store\Model\StoreManagerInterface;
use Zend_Db_Select;
+use Magento\Tax\Model\TaxCalculation;
class Price implements DecoratorInterface
{
+ private const XML_PATH_ROUNDING_METHOD = 'tax/calculation/rounding_method';
/**
* @var CollectionFactory
*/
@@ -28,16 +34,30 @@ class Price implements DecoratorInterface
*/
protected $config;
+ /**
+ * @var float
+ */
+ private float $exchangeRate = 1.0;
+
+ /**
+ * @var string|null
+ */
+ private ?string $roundingMethod = null;
+
/**
* Price constructor.
* @param CollectionFactory $collectionFactory
* @param StoreManagerInterface $storeManager
* @param Config $config
+ * @param Calculation $taxCalculation
+ * @param ScopeConfigInterface $scopeConfig
*/
public function __construct(
CollectionFactory $collectionFactory,
StoreManagerInterface $storeManager,
- Config $config
+ Config $config,
+ private readonly Calculation $taxCalculation,
+ private readonly ScopeConfigInterface $scopeConfig
) {
$this->collectionFactory = $collectionFactory;
$this->storeManager = $storeManager;
@@ -50,16 +70,18 @@ public function __construct(
*/
public function decorate(Collection|PriceCollection $collection): void
{
+ $store = $collection->getStore();
$websiteId = $collection->getStore()->getWebsiteId();
- $priceSelect = $this->createPriceSelect($collection->getIds(), $websiteId);
+ $priceSelect = $this->createPriceSelect($collection->getIds(), $websiteId);
$priceQuery = $priceSelect->getSelect()->query();
- $currency = $collection->getStore()->getCurrentCurrency();
- $exchangeRate = 1;
+ $currency = $collection->getStore()->getCurrentCurrency();
if ($collection->getStore()->getCurrentCurrencyRate() > 0.00001) {
$exchangeRate = (float)$collection->getStore()->getCurrentCurrencyRate();
}
+ $this->exchangeRate = $exchangeRate ?? 1.0;
+ $this->roundingMethod = $this->scopeConfig->getValue(self::XML_PATH_ROUNDING_METHOD, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store->getCode());
$priceFields = $this->config->getPriceFields($collection->getStore()->getId());
@@ -68,15 +90,86 @@ public function decorate(Collection|PriceCollection $collection): void
$row['currency'] = $currency->getCurrencyCode();
$row['price'] = $this->getPriceValue($row, $priceFields);
- //do all prices * exchange rate
- foreach ($priceFields as $priceField) {
- $row[$priceField] = (float) ($row[$priceField] * $exchangeRate);
+ $product = $this->collectionFactory->create()->getItemById($entityId);
+ $taxClassId = $this->getTaxClassId($collection->get($entityId));
+
+ if ($this->config->calculateCombinedPrices($store) && $this->isGroupedProduct($product)) {
+ $row['price'] = $this->calculateGroupedProductPrice($entityId, $store, $taxClassId);
+ } elseif ($this->config->calculateCombinedPrices($store) && $this->isBundleProduct($product)) {
+ $row['price'] = $this->calculateBundleProductPrice($entityId, $store, $taxClassId);
+ } else {
+ foreach ($priceFields as $priceField) {
+ $row[$priceField] = $this->calculatePrice((float)$row[$priceField], $taxClassId, $store);
+ }
}
$collection->get($entityId)->setFromArray($row);
}
}
+ /**
+ * @param float $value
+ * @return float
+ */
+ private function applyRoundingMethod(float $value): float
+ {
+ return match ($this->roundingMethod) {
+ 'ceil' => ceil($value),
+ 'floor' => floor($value),
+ default => round($value, 2),
+ };
+ }
+
+ /**
+ * @param float $price
+ * @param int|null $taxClassId
+ * @param Store $store
+ * @return float
+ */
+ private function calculatePrice(float $price, ?int $taxClassId, $store): float
+ {
+ if ($this->config->addVat($store)) {
+ $price = $this->addVat($price, $taxClassId, $store);
+ }
+
+ $price = $this->calculateExchangeRate($price);
+
+ return $price;
+ }
+
+ /**
+ * @param float $price
+ * @param int|null $taxClassId
+ * @param Store $store
+ * @return float
+ */
+ private function addVat(float $price, ?int $taxClassId, $store): float
+ {
+ $rateRequest = $this->taxCalculation->getRateRequest(null, null, null, $store);
+ $taxRate = $this->taxCalculation->getRate($rateRequest->setProductClassId($taxClassId));
+
+ $price = $this->applyRoundingMethod(
+ $price * (1 + $taxRate / 100),
+ $this->roundingMethod
+ );
+
+ return $price;
+ }
+
+ /**
+ * @param float $price
+ * @return float
+ */
+ private function calculateExchangeRate(float $price): float
+ {
+ return $this->applyRoundingMethod(
+ $price * $this->exchangeRate,
+ $this->roundingMethod
+ );
+
+ return $price;
+ }
+
/**
* @param array $ids
* @param int $websiteId
@@ -120,4 +213,98 @@ protected function getPriceValue(array $priceData, array $priceFields): float
return 0;
}
+
+ /**
+ * @param ProductInterface $product
+ * @return int|null
+ */
+ protected function getTaxClassId($product): ?int
+ {
+ try {
+ if (isset($product->getAttribute('tax_class_id')[0])) {
+ return $product->getAttribute('tax_class_id')[0];
+ }
+ return null;
+ } catch (InvalidArgumentException) {
+ return null;
+ }
+ }
+
+ /**
+ * @param ProductInterface $product
+ * @return bool
+ */
+ protected function isGroupedProduct(ProductInterface $product): bool
+ {
+ return $product?->getTypeId() === \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE;
+ }
+
+ /**
+ * @param ProductInterface $product
+ * @return bool
+ */
+ protected function isBundleProduct($product): bool
+ {
+ return $product?->getTypeId() === \Magento\Bundle\Model\Product\Type::TYPE_CODE;
+ }
+
+ /**
+ * @param int $entityId
+ * @param callable $getAssociatedItems
+ * @param Store $store
+ * @param int|null $taxClassId
+ * @return float
+ */
+ protected function calculateProductPrice(int $entityId, callable $getAssociatedItems, $store, ?int $taxClassId): float
+ {
+ $product = $this->collectionFactory->create()->getItemById($entityId);
+ $associatedItems = $getAssociatedItems($product);
+
+ // Convert collection to array if necessary
+ if ($associatedItems instanceof \Magento\Framework\Data\Collection) {
+ $associatedItems = $associatedItems->getItems();
+ }
+
+ return array_reduce($associatedItems, function ($total, $item) use ($store, $taxClassId) {
+ $basePrice = $item->getPrice();
+ $price = $this->calculatePrice(
+ $basePrice,
+ $taxClassId,
+ $store
+ );
+
+ return $total + ($price * $item->getQty());
+ }, 0);
+ }
+
+ /**
+ * @param int $entityId
+ * @param Store $store
+ * @param int|null $taxClassId
+ * @return float
+ */
+ protected function calculateGroupedProductPrice(int $entityId, $store, ?int $taxClassId): float
+ {
+ return $this->calculateProductPrice($entityId, function ($product) {
+
+ return $product->getTypeInstance()->getAssociatedProducts($product);
+ }, $store, $taxClassId);
+ }
+
+ /**
+ * @param int $entityId
+ * @param Store $store
+ * @param int|null $taxClassId
+ * @return float
+ */
+ protected function calculateBundleProductPrice(int $entityId, $store, ?int $taxClassId): float
+ {
+ return $this->calculateProductPrice($entityId, function ($product) {
+
+ return $product->getTypeInstance()->getSelectionsCollection(
+ $product->getTypeInstance()->getOptionsIds($product),
+ $product
+ );
+ }, $store, $taxClassId);
+ }
}
diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml
index 4147335..090584f 100644
--- a/etc/adminhtml/system.xml
+++ b/etc/adminhtml/system.xml
@@ -111,6 +111,24 @@
+
+
+ Would you like to export the combined price of an grouped/bundle product?
+ Magento\Config\Model\Config\Source\Yesno
+
+ 1
+
+
+
+
+
+ Add the vat to the prices exported. Only enable this if the prices exported to Tweakwise are missing the vat
+ Magento\Config\Model\Config\Source\Yesno
+
+ 1
+
+
+
5000
5000
5000
+ 0
+ 0
0
From 24a707feb268db8c2caa615c4336dff1bb2bf116 Mon Sep 17 00:00:00 2001
From: ah-net <103565001+ah-net@users.noreply.github.com>
Date: Tue, 15 Apr 2025 11:06:57 +0200
Subject: [PATCH 2/4] Style fixes
---
Model/Config.php | 8 ++++++++
.../Products/CollectionDecorator/Price.php | 19 ++++++++++++-------
2 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/Model/Config.php b/Model/Config.php
index 23770d6..c55f74e 100644
--- a/Model/Config.php
+++ b/Model/Config.php
@@ -308,11 +308,19 @@ public function getBatchSizeProductsChildren(): int
return (int) $this->config->getValue(self::BATCH_SIZE_PRODUCTS_CHILDREN);
}
+ /**
+ * @param Store|int|string|null $store
+ * @return bool
+ */
public function calculateCombinedPrices($store = null): bool
{
return (bool) $this->config->isSetFlag(self::CALCULATE_COMPOSITE_PRICES, ScopeInterface::SCOPE_STORE, $store);
}
+ /**
+ * @param Store|int|string|null $store
+ * @return bool
+ */
public function addVat($store = null): bool
{
return (bool) $this->config->isSetFlag(self::ADD_TAX_TO_PRICES, ScopeInterface::SCOPE_STORE, $store);
diff --git a/Model/Write/Products/CollectionDecorator/Price.php b/Model/Write/Products/CollectionDecorator/Price.php
index 6a4fbf7..8a73f83 100644
--- a/Model/Write/Products/CollectionDecorator/Price.php
+++ b/Model/Write/Products/CollectionDecorator/Price.php
@@ -19,6 +19,7 @@
class Price implements DecoratorInterface
{
private const XML_PATH_ROUNDING_METHOD = 'tax/calculation/rounding_method';
+
/**
* @var CollectionFactory
*/
@@ -80,8 +81,13 @@ public function decorate(Collection|PriceCollection $collection): void
if ($collection->getStore()->getCurrentCurrencyRate() > 0.00001) {
$exchangeRate = (float)$collection->getStore()->getCurrentCurrencyRate();
}
+
$this->exchangeRate = $exchangeRate ?? 1.0;
- $this->roundingMethod = $this->scopeConfig->getValue(self::XML_PATH_ROUNDING_METHOD, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store->getCode());
+ $this->roundingMethod = $this->scopeConfig->getValue(
+ self::XML_PATH_ROUNDING_METHOD,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $store->getCode()
+ );
$priceFields = $this->config->getPriceFields($collection->getStore()->getId());
@@ -166,8 +172,6 @@ private function calculateExchangeRate(float $price): float
$price * $this->exchangeRate,
$this->roundingMethod
);
-
- return $price;
}
/**
@@ -224,6 +228,7 @@ protected function getTaxClassId($product): ?int
if (isset($product->getAttribute('tax_class_id')[0])) {
return $product->getAttribute('tax_class_id')[0];
}
+
return null;
} catch (InvalidArgumentException) {
return null;
@@ -286,9 +291,9 @@ protected function calculateProductPrice(int $entityId, callable $getAssociatedI
protected function calculateGroupedProductPrice(int $entityId, $store, ?int $taxClassId): float
{
return $this->calculateProductPrice($entityId, function ($product) {
-
return $product->getTypeInstance()->getAssociatedProducts($product);
- }, $store, $taxClassId);
+ }
+ , $store, $taxClassId);
}
/**
@@ -300,11 +305,11 @@ protected function calculateGroupedProductPrice(int $entityId, $store, ?int $tax
protected function calculateBundleProductPrice(int $entityId, $store, ?int $taxClassId): float
{
return $this->calculateProductPrice($entityId, function ($product) {
-
return $product->getTypeInstance()->getSelectionsCollection(
$product->getTypeInstance()->getOptionsIds($product),
$product
);
- }, $store, $taxClassId);
+ }
+ , $store, $taxClassId);
}
}
From 2f07b1dc6b8ea5fa0fc36dc47c64947a5331f274 Mon Sep 17 00:00:00 2001
From: ah-net <103565001+ah-net@users.noreply.github.com>
Date: Tue, 15 Apr 2025 11:16:20 +0200
Subject: [PATCH 3/4] Style fixes
---
.../Products/CollectionDecorator/Price.php | 60 ++++++++++++-------
1 file changed, 38 insertions(+), 22 deletions(-)
diff --git a/Model/Write/Products/CollectionDecorator/Price.php b/Model/Write/Products/CollectionDecorator/Price.php
index 8a73f83..1bc7cbc 100644
--- a/Model/Write/Products/CollectionDecorator/Price.php
+++ b/Model/Write/Products/CollectionDecorator/Price.php
@@ -260,7 +260,12 @@ protected function isBundleProduct($product): bool
* @param int|null $taxClassId
* @return float
*/
- protected function calculateProductPrice(int $entityId, callable $getAssociatedItems, $store, ?int $taxClassId): float
+ protected function calculateProductPrice(
+ int $entityId,
+ callable $getAssociatedItems,
+ $store,
+ ?int $taxClassId
+ ): float
{
$product = $this->collectionFactory->create()->getItemById($entityId);
$associatedItems = $getAssociatedItems($product);
@@ -270,16 +275,19 @@ protected function calculateProductPrice(int $entityId, callable $getAssociatedI
$associatedItems = $associatedItems->getItems();
}
- return array_reduce($associatedItems, function ($total, $item) use ($store, $taxClassId) {
- $basePrice = $item->getPrice();
- $price = $this->calculatePrice(
- $basePrice,
- $taxClassId,
- $store
- );
-
- return $total + ($price * $item->getQty());
- }, 0);
+ return array_reduce(
+ $associatedItems,
+ function ($total, $item) use ($store, $taxClassId) {
+ $basePrice = $item->getPrice();
+ $price = $this->calculatePrice(
+ $basePrice,
+ $taxClassId,
+ $store
+ );
+ return $total + ($price * $item->getQty());
+ },
+ 0
+ );
}
/**
@@ -290,10 +298,14 @@ protected function calculateProductPrice(int $entityId, callable $getAssociatedI
*/
protected function calculateGroupedProductPrice(int $entityId, $store, ?int $taxClassId): float
{
- return $this->calculateProductPrice($entityId, function ($product) {
- return $product->getTypeInstance()->getAssociatedProducts($product);
- }
- , $store, $taxClassId);
+ return $this->calculateProductPrice(
+ $entityId,
+ function ($product) {
+ return $product->getTypeInstance()->getAssociatedProducts($product);
+ },
+ $store,
+ $taxClassId
+ );
}
/**
@@ -304,12 +316,16 @@ protected function calculateGroupedProductPrice(int $entityId, $store, ?int $tax
*/
protected function calculateBundleProductPrice(int $entityId, $store, ?int $taxClassId): float
{
- return $this->calculateProductPrice($entityId, function ($product) {
- return $product->getTypeInstance()->getSelectionsCollection(
- $product->getTypeInstance()->getOptionsIds($product),
- $product
- );
- }
- , $store, $taxClassId);
+ return $this->calculateProductPrice(
+ $entityId,
+ function ($product) {
+ return $product->getTypeInstance()->getSelectionsCollection(
+ $product->getTypeInstance()->getOptionsIds($product),
+ $product
+ );
+ },
+ $store,
+ $taxClassId
+ );
}
}
From 7711fccfb99c734b7f7794e81cb5e48ca229906b Mon Sep 17 00:00:00 2001
From: ah-net <103565001+ah-net@users.noreply.github.com>
Date: Tue, 15 Apr 2025 11:18:12 +0200
Subject: [PATCH 4/4] Style fixes
---
Model/Write/Products/CollectionDecorator/Price.php | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Model/Write/Products/CollectionDecorator/Price.php b/Model/Write/Products/CollectionDecorator/Price.php
index 1bc7cbc..7905616 100644
--- a/Model/Write/Products/CollectionDecorator/Price.php
+++ b/Model/Write/Products/CollectionDecorator/Price.php
@@ -265,8 +265,7 @@ protected function calculateProductPrice(
callable $getAssociatedItems,
$store,
?int $taxClassId
- ): float
- {
+ ): float {
$product = $this->collectionFactory->create()->getItemById($entityId);
$associatedItems = $getAssociatedItems($product);