Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Fix product/variant revisions #3750

Merged
merged 18 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Release Notes for Craft Commerce

## Unreleased
- Fixed a bug where product revisions weren’t storing variant relations.

## 5.2.1 - 2024-10-23

- Fixed a bug where the Commerce subnav could be missing the “Product” nav item. ([#3735](https://github.com/craftcms/commerce/issues/3735))
Expand Down
2 changes: 1 addition & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ public static function editions(): array
/**
* @inheritDoc
*/
public string $schemaVersion = '5.2.0.4';
public string $schemaVersion = '5.2.0.5';

/**
* @inheritdoc
Expand Down
68 changes: 48 additions & 20 deletions src/base/Purchasable.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use Craft;
use craft\base\Element;
use craft\base\NestedElementInterface;
use craft\commerce\db\Table;
use craft\commerce\elements\Order;
use craft\commerce\errors\StoreNotFoundException;
use craft\commerce\helpers\Currency;
Expand All @@ -28,6 +27,7 @@
use craft\commerce\records\InventoryItem as InventoryItemRecord;
use craft\commerce\records\Purchasable as PurchasableRecord;
use craft\commerce\records\PurchasableStore;
use craft\db\ActiveQuery;
use craft\errors\DeprecationException;
use craft\errors\SiteNotFoundException;
use craft\helpers\ArrayHelper;
Expand Down Expand Up @@ -818,6 +818,10 @@ protected function defineRules(): array
UniqueValidator::class,
'targetClass' => PurchasableRecord::class,
'caseInsensitive' => true,
'filter' => function(ActiveQuery $query) {
$query->leftJoin(\craft\db\Table::ELEMENTS . ' elements', '[[elements.id]] = [[commerce_purchasables.id]]');
$query->andWhere(['elements.revisionId' => null, 'elements.draftId' => null]);
},
'on' => self::SCENARIO_LIVE,
],
[['basePrice'], 'number'],
Expand Down Expand Up @@ -947,20 +951,33 @@ public function getInventoryLevels(): Collection
*/
public function afterSave(bool $isNew): void
{
$purchasableId = $this->getCanonicalId();
$canonicalPurchasableId = $this->getCanonicalId();
$purchasableId = $this->id;

if (!$this->propagating) {
$isOwnerDraftApplying = false;
$isOwnerRevisionApplying = false;

// If this is a nested element, check if the owner is a draft and is being applied
if ($this instanceof NestedElementInterface) {
$owner = $this->getOwner();
$isOwnerDraftApplying = $owner && $owner->getIsCanonical() && $owner->duplicateOf !== null && $owner->duplicateOf->getIsDraft();
$isOwnerDraftApplying = $owner
&& $owner->getIsCanonical()
&& $owner->duplicateOf !== null
&& $owner->duplicateOf->getIsDraft();

$isOwnerRevisionApplying = $owner
&& $owner->duplicateOf !== null
&& $owner->duplicateOf->getIsRevision();
}

if ($this->duplicateOf !== null && !$this->getIsRevision() && !$isOwnerDraftApplying) {
$this->sku = PurchasableHelper::tempSku();
// Nullify inventory item so a new one is created
$this->inventoryItemId = null;
if (!$this->getIsRevision()) {
// Reset the purchasable's SKU when it is explicitly being duplicating
if ($this->duplicateOf !== null && !$isOwnerDraftApplying && !$isOwnerRevisionApplying) {
$this->sku = PurchasableHelper::tempSku();
// Nullify inventory item so a new one is created
$this->inventoryItemId = null;
}
}

$purchasable = PurchasableRecord::findOne($purchasableId);
Expand All @@ -986,20 +1003,29 @@ public function afterSave(bool $isNew): void

// Always create the inventory item even if it's a temporary draft (in the slide) since we want to allow stock to be
// added to inventory before it is saved as a permanent variant.
if ($purchasableId) {
// Set the inventory item data
/** @var InventoryItem|null $inventoryItem */
$inventoryItem = InventoryItemRecord::find()->where(['purchasableId' => $purchasableId])->one();
if (!$inventoryItem) {
$inventoryItem = new InventoryItemRecord();
$inventoryItem->purchasableId = $purchasableId;
$inventoryItem->countryCodeOfOrigin = '';
$inventoryItem->administrativeAreaCodeOfOrigin = '';
$inventoryItem->harmonizedSystemCode = '';
$inventoryItem->save();
if ($canonicalPurchasableId) {
if ($isOwnerDraftApplying && $this->duplicateOf !== null) {
/** @var InventoryItemRecord|null $inventoryItem */
$inventoryItem = InventoryItemRecord::find()->where(['purchasableId' => $this->duplicateOf->id])->one();
if ($inventoryItem) {
$inventoryItem->purchasableId = $canonicalPurchasableId;
$inventoryItem->save();
$this->inventoryItemId = $inventoryItem->id;
}
} else {
// Set the inventory item data
/** @var InventoryItemRecord|null $inventoryItem */
$inventoryItem = InventoryItemRecord::find()->where(['purchasableId' => $canonicalPurchasableId])->one();
if (!$inventoryItem) {
$inventoryItem = new InventoryItemRecord();
$inventoryItem->purchasableId = $canonicalPurchasableId;
$inventoryItem->countryCodeOfOrigin = '';
$inventoryItem->administrativeAreaCodeOfOrigin = '';
$inventoryItem->harmonizedSystemCode = '';
$inventoryItem->save();
$this->inventoryItemId = $inventoryItem->id;
}
}

$this->inventoryItemId = $inventoryItem->id;
}
}

Expand Down Expand Up @@ -1181,6 +1207,7 @@ protected function shippingCategoryFieldHtml(bool $static): string
'label' => Craft::t('commerce', 'Shipping Category'),
'options' => $options,
'value' => $this->shippingCategoryId,
'disabled' => $static,
]);
}

Expand Down Expand Up @@ -1210,6 +1237,7 @@ protected function taxCategoryFieldHtml(bool $static): string
'label' => Craft::t('commerce', 'Tax Category'),
'options' => $options,
'value' => $this->taxCategoryId,
'disabled' => $static,
]);
}

Expand Down
2 changes: 2 additions & 0 deletions src/controllers/OrdersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,8 @@ public function actionPurchasablesTable(): Response
->innerJoin(Table::PURCHASABLES_STORES . ' pstores', '[[purchasables.id]] = [[pstores.purchasableId]]')
->where(['elements.enabled' => true])
->andWhere(['pstores.storeId' => $store->id])
->andWhere(['elements.revisionId' => null])
->andWhere(['elements.draftId' => null])
->from(['purchasables' => Table::PURCHASABLES]);

// Are they searching for a SKU or purchasable description?
Expand Down
8 changes: 7 additions & 1 deletion src/elements/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -1749,10 +1749,16 @@ protected static function defineSearchableAttributes(): array
*/
private static function createVariantQuery(Product $product): VariantQuery
{
return Variant::find()
$query = Variant::find()
->productId($product->id)
->siteId($product->siteId)
->orderBy(['sortOrder' => SORT_ASC]);

if ($product->getIsRevision()) {
$query->revisions(null)->trashed(null);
}

return $query;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/elements/Variant.php
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,14 @@ public function canDuplicate(User $user): bool
*/
public function getIsAvailable(): bool
{
if ($this->getIsRevision()) {
return false;
}

if ($this->getIsDraft()) {
return false;
}

if ($this->getPrimaryOwner()->getIsDraft()) {
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions src/elements/db/PurchasableQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ protected function afterPrepare(): bool
$this->subQuery->leftJoin(['purchasables_stores' => Table::PURCHASABLES_STORES], '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]');

$this->subQuery->leftJoin(['catalogprices' => $catalogPricesQuery], '[[catalogprices.purchasableId]] = [[commerce_purchasables.id]] AND [[catalogprices.storeId]] = [[sitestores.storeId]]');
$this->subQuery->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]');
$this->subQuery->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]] OR [[inventoryitems.purchasableId]] = [[elements.canonicalId]]');

return parent::afterPrepare();
}
Expand Down Expand Up @@ -705,7 +705,7 @@ protected function beforePrepare(): bool

$this->query->leftJoin(Table::SITESTORES . ' sitestores', '[[elements_sites.siteId]] = [[sitestores.siteId]]');
$this->query->leftJoin(Table::PURCHASABLES_STORES . ' purchasables_stores', '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]');
$this->query->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]');
$this->query->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]] OR [[inventoryitems.purchasableId]] = [[elements.canonicalId]]');

$this->subQuery->addSelect([
'catalogprices.price',
Expand Down
2 changes: 2 additions & 0 deletions src/fieldlayoutelements/PurchasableAllowedQtyField.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
'value' => $element->minQty,
'placeholder' => Craft::t('commerce', 'Any'),
'title' => Craft::t('commerce', 'Minimum allowed quantity'),
'disabled' => $static,
]) .
Html::endTag('div') .
Html::tag('div', Craft::t('commerce', 'to'), ['class' => 'label light']) .
Expand All @@ -70,6 +71,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
'value' => $element->maxQty,
'placeholder' => Craft::t('commerce', 'Any'),
'title' => Craft::t('commerce', 'Maximum allowed quantity'),
'disabled' => $static,
]) .
Html::endTag('div') .
Html::endTag('div');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.');
}

return PurchasableHelper::availableForPurchaseInputHtml($element->availableForPurchase);
return PurchasableHelper::availableForPurchaseInputHtml($element->availableForPurchase, [
'disabled' => $static,
]);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/fieldlayoutelements/PurchasableDimensionsField.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
'class' => 'text',
'size' => 10,
'unit' => Plugin::getInstance()->getSettings()->dimensionUnits,
'disabled' => $static,
]), ['id' => 'length', 'label' => Craft::t('commerce', 'Length')]) .
Cp::fieldHtml(Cp::textHtml([
'id' => 'width',
Expand All @@ -85,6 +86,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
'class' => 'text',
'size' => 10,
'unit' => Plugin::getInstance()->getSettings()->dimensionUnits,
'disabled' => $static,
]), ['id' => 'width', 'label' => Craft::t('commerce', 'Width')]) .
Cp::fieldHtml(Cp::textHtml([
'id' => 'height',
Expand All @@ -93,6 +95,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
'class' => 'text',
'size' => 10,
'unit' => Plugin::getInstance()->getSettings()->dimensionUnits,
'disabled' => $static,
]), ['id' => 'height', 'label' => Craft::t('commerce', 'Height')]) .
Html::endTag('div');
}
Expand Down
1 change: 1 addition & 0 deletions src/fieldlayoutelements/PurchasableFreeShippingField.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
'name' => 'freeShipping',
'small' => true,
'on' => $element->freeShipping,
'disabled' => $static,
]);
}

Expand Down
4 changes: 4 additions & 0 deletions src/fieldlayoutelements/PurchasablePriceField.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
}
}

$toggleContent = $static ? null : $toggleContent;

$currency = $element->getStore()->getCurrency();

return Html::beginTag('div', [
Expand All @@ -161,6 +163,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
'currencyLabel' => $currency->getCode(),
'required' => true,
'errors' => $element->getErrors('basePrice'),
'disabled' => $static,
]), [
'id' => 'base-price',
'label' => Craft::t('commerce', 'Price'),
Expand All @@ -174,6 +177,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
'currency' => $currency->getCode(),
'currencyLabel' => $currency->getCode(),
'errors' => $element->getErrors('basePromotionalPrice'),
'disabled' => $static,
]), [
'id' => 'promotional-price',
'label' => Craft::t('commerce', 'Promotional Price'),
Expand Down
1 change: 1 addition & 0 deletions src/fieldlayoutelements/PurchasablePromotableField.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
'name' => 'promotable',
'small' => true,
'on' => $element->promotable,
'disabled' => $static,
]);
}

Expand Down
4 changes: 3 additions & 1 deletion src/fieldlayoutelements/PurchasableSkuField.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ public function inputHtml(ElementInterface $element = null, bool $static = false
if ($element instanceof Variant && $element->getOwner()->getType()->skuFormat !== null && !$element->id) {
$html .= Html::hiddenInput('sku', '');
} else {
$html .= PurchasableHelper::skuInputHtml($element->getSkuAsText());
$html .= PurchasableHelper::skuInputHtml($element->getSkuAsText(), [
'disabled' => $static,
]);
}

return $html;
Expand Down
Loading
Loading