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.3] Streamline update purchasable stock level #3801

Merged
merged 12 commits into from
Dec 11, 2024
16 changes: 13 additions & 3 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@

### Store Management

- Added support for `to`, `bcc`, and `cc` email fields to support environment variables. ([#3738](https://github.com/craftcms/commerce/issues/3738))
- Added an `originalCart` value to the `commerce/update-cart` failed ajax response. ([#430](https://github.com/craftcms/commerce/issues/430))
- Order conditions can now have a “Payment Gateway” rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722))
- Variant conditions can now have a “Product” rule.

### Administration

- Added support for `to`, `bcc`, and `cc` email fields to support environment variables. ([#3738](https://github.com/craftcms/commerce/issues/3738))

### Development

- Added an `originalCart` value to the `commerce/update-cart` failed ajax response. ([#430](https://github.com/craftcms/commerce/issues/430))

### Extensibility

- Added `craft\commerce\elements\conditions\variants\ProductConditionRule`.
- Added `craft\commerce\base\InventoryItemTrait`.
- Added `craft\commerce\base\InventoryLocationTrait`.
- Added `craft\commerce\elements\conditions\variants\ProductConditionRule`.
- Added `craft\commerce\services\Inventory::updateInventoryLevel()`.
- Added `craft\commerce\services\Inventory::updatePurchasableInventoryLevel()`.
61 changes: 61 additions & 0 deletions src/base/InventoryItemTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\commerce\base;

use craft\commerce\models\InventoryItem;
use craft\commerce\Plugin;

/**
* Inventory Item Trait
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 5.3.0
*/
trait InventoryItemTrait
{
/**
* @var int|null The inventory item ID
*/
public ?int $inventoryItemId = null;

/**
* @var InventoryItem|null The inventory item
* @see getInventoryItem()
* @see setInventoryItem()
*/
private ?InventoryItem $_inventoryItem = null;

/**
* @param InventoryItem|null $inventoryItem
* @return void
*/
public function setInventoryItem(?InventoryItem $inventoryItem): void
{
$this->_inventoryItem = $inventoryItem;
$this->inventoryItemId = $inventoryItem?->id ?? null;
}

/**
* @return InventoryItem|null
* @throws \yii\base\InvalidConfigException
*/
public function getInventoryItem(): ?InventoryItem
{
if (isset($this->_inventoryItem)) {
return $this->_inventoryItem;
}

if ($this->inventoryItemId) {
$this->_inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($this->inventoryItemId);

return $this->_inventoryItem;
}

return null;
}
}
61 changes: 61 additions & 0 deletions src/base/InventoryLocationTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\commerce\base;

use craft\commerce\models\InventoryLocation;
use craft\commerce\Plugin;

/**
* Inventory Location Trait
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 5.3.0
*/
trait InventoryLocationTrait
{
/**
* @var int|null The inventory item ID
*/
public ?int $inventoryLocationId = null;

/**
* @var InventoryLocation|null The inventory item
* @see getInventoryLocation()
* @see setInventoryLocation()
*/
private ?InventoryLocation $_inventoryLocation = null;

/**
* @param InventoryLocation|null $inventoryLocation
* @return void
*/
public function setInventoryLocation(?InventoryLocation $inventoryLocation): void
{
$this->_inventoryLocation = $inventoryLocation;
$this->inventoryLocationId = $inventoryLocation?->id ?? null;
}

/**
* @return InventoryLocation|null
* @throws \yii\base\InvalidConfigException
*/
public function getInventoryLocation(): ?InventoryLocation
{
if (isset($this->_inventoryLocation)) {
return $this->_inventoryLocation;
}

if ($this->inventoryLocationId) {
$this->_inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($this->inventoryLocationId);

return $this->_inventoryLocation;
}

return null;
}
}
24 changes: 12 additions & 12 deletions src/base/InventoryMovement.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
namespace craft\commerce\base;

use craft\commerce\enums\InventoryTransactionType;
use craft\commerce\models\InventoryItem;
use craft\commerce\models\InventoryLocation;

/**
Expand All @@ -18,10 +17,7 @@
*/
abstract class InventoryMovement extends Model implements InventoryMovementInterface
{
/**
* @var InventoryItem The inventory item
*/
public InventoryItem $inventoryItem;
use InventoryItemTrait;

/**
* @var InventoryLocation
Expand Down Expand Up @@ -90,27 +86,31 @@ public function init(): void
}

/**
* @inheritDoc
* @return array
*/
public function isValid(): bool
protected function defineRules(): array
{
return $this->validate();
$rules = parent::defineRules();

$rules[] = [['inventoryItemId'], 'safe'];

return $rules;
}

/**
* @inheritDoc
*/
public function getInventoryMovementHash(): string
public function isValid(): bool
{
return $this->_inventoryMovementHash;
return $this->validate();
}

/**
* @inheritDoc
*/
public function getInventoryItem(): InventoryItem
public function getInventoryMovementHash(): string
{
return $this->inventoryItem;
return $this->_inventoryMovementHash;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/base/InventoryMovementInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface InventoryMovementInterface
/**
* @return InventoryItem
*/
public function getInventoryItem(): InventoryItem;
public function getInventoryItem(): ?InventoryItem;

/**
* @return InventoryLocation
Expand Down
15 changes: 14 additions & 1 deletion src/base/Purchasable.php
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,20 @@ private function _getTeller(): Teller
return Plugin::getInstance()->getCurrencies()->getTeller($this->getStore()->getCurrency());
}

/**
* @inheritdoc
*/
public function __unset($name)
{
// Allow clearing of specific memoized properties
if (in_array($name, ['stock', 'shippingCategory', 'taxCategory'])) {
$this->{'_' . $name} = null;
return;
}

parent::__unset($name);
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -949,7 +963,6 @@ public function getStock(): int
return $this->_stock;
}


/**
* Returns the total stock across all locations this purchasable is tracked in.
* @return Collection<InventoryLevel>
Expand Down
5 changes: 5 additions & 0 deletions src/base/StoreTrait.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\commerce\base;

Expand Down
2 changes: 1 addition & 1 deletion src/collections/UpdateInventoryLevelCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static function make($items = [])
public function getPurchasables(): array
{
return $this->map(function(UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel) {
return $updateInventoryLevel->inventoryItem->getPurchasable();
return $updateInventoryLevel->getInventoryItem()->getPurchasable();
})->all();
}
}
77 changes: 32 additions & 45 deletions src/controllers/InventoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,6 @@ public function actionUpdateLevels(): Response
$note = Craft::$app->getRequest()->getRequiredParam('note');
$inventoryLocationId = (int)Craft::$app->getRequest()->getRequiredParam('inventoryLocationId');
$inventoryItemIds = Craft::$app->getRequest()->getRequiredParam('ids');
$inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($inventoryLocationId);
$type = Craft::$app->getRequest()->getRequiredParam('type');

// We don't add zero amounts as transactions movements
Expand All @@ -514,17 +513,16 @@ public function actionUpdateLevels(): Response
$errors = [];
$updateInventoryLevels = UpdateInventoryLevelCollection::make();
foreach ($inventoryItemIds as $inventoryItemId) {
$inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId);

$updateInventoryLevels->push(new UpdateInventoryLevel([
'type' => $type,
'updateAction' => $updateAction,
'inventoryItem' => $inventoryItem,
'inventoryLocation' => $inventoryLocation,
'quantity' => $quantity,
'note' => $note,
])
);
// Verbosely set property to show usages
$updateInventoryLevel = new UpdateInventoryLevel();
$updateInventoryLevel->type = $type;
$updateInventoryLevel->updateAction = $updateAction;
$updateInventoryLevel->inventoryItemId = $inventoryItemId;
$updateInventoryLevel->inventoryLocationId = $inventoryLocationId;
$updateInventoryLevel->quantity = $quantity;
$updateInventoryLevel->note = $note;

$updateInventoryLevels->push($updateInventoryLevel);
}


Expand All @@ -540,7 +538,8 @@ public function actionUpdateLevels(): Response

$resultingInventoryLevels = [];
foreach ($updateInventoryLevels as $updateInventoryLevel) {
$resultingInventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel($updateInventoryLevel->inventoryItem, $updateInventoryLevel->inventoryLocation);
/** @var UpdateInventoryLevel $updateInventoryLevel */
$resultingInventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel($updateInventoryLevel->inventoryItemId, $updateInventoryLevel->inventoryLocationId);
}

return $this->asSuccess(Craft::t('commerce', 'Inventory updated.'), [
Expand All @@ -565,12 +564,9 @@ public function actionEditUpdateLevelsModal(): Response
$quantity = (int)$this->request->getParam('quantity', 0);
$type = $this->request->getRequiredParam('type');

$inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($inventoryLocationId);

$inventoryLevels = [];
foreach ($inventoryItemIds as $inventoryItemId) {
$item = Plugin::getInstance()->getInventory()->getInventoryItemById((int)$inventoryItemId);
$inventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel($item, $inventoryLocation);
$inventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel((int)$inventoryItemId, $inventoryLocationId);
}

$params = [
Expand Down Expand Up @@ -614,17 +610,14 @@ public function actionSaveInventoryMovement(): Response
return $this->asSuccess(Craft::t('commerce', 'No inventory movements made.'));
}

$inventoryMovement = new InventoryManualMovement(
[
'inventoryItem' => Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId),
'fromInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId),
'toInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId),
'fromInventoryTransactionType' => InventoryTransactionType::from($fromInventoryTransactionType),
'toInventoryTransactionType' => InventoryTransactionType::from($toInventoryTransactionType),
'quantity' => $quantity,
'note' => $note,
]
);
$inventoryMovement = new InventoryManualMovement();
$inventoryMovement->inventoryItemId = $inventoryItemId;
$inventoryMovement->fromInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId);
$inventoryMovement->toInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId);
$inventoryMovement->fromInventoryTransactionType = InventoryTransactionType::from($fromInventoryTransactionType);
$inventoryMovement->toInventoryTransactionType = InventoryTransactionType::from($toInventoryTransactionType);
$inventoryMovement->quantity = $quantity;
$inventoryMovement->note = $note;

if ($inventoryMovement->validate()) {
/** @var InventoryMovementCollection $inventoryMovementCollection */
Expand Down Expand Up @@ -665,19 +658,16 @@ public function actionEditMovementModal(): Response
$toInventoryTransactionType = $toInventoryTransactionType->value;
}

$inventoryMovement = new InventoryManualMovement(
[
'inventoryItem' => Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId),
'fromInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId),
'toInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId),
'fromInventoryTransactionType' => InventoryTransactionType::from($fromInventoryTransactionType),
'toInventoryTransactionType' => InventoryTransactionType::from($toInventoryTransactionType),
'quantity' => $quantity,
'note' => $note,
]
);

$fromLevel = Plugin::getInstance()->getInventory()->getInventoryLevel($inventoryMovement->inventoryItem, $inventoryMovement->fromInventoryLocation);
$inventoryMovement = new InventoryManualMovement();
$inventoryMovement->inventoryItemId = $inventoryItemId;
$inventoryMovement->fromInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId);
$inventoryMovement->toInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId);
$inventoryMovement->fromInventoryTransactionType = InventoryTransactionType::from($fromInventoryTransactionType);
$inventoryMovement->toInventoryTransactionType = InventoryTransactionType::from($toInventoryTransactionType);
$inventoryMovement->quantity = $quantity;
$inventoryMovement->note = $note;

$fromLevel = Plugin::getInstance()->getInventory()->getInventoryLevel($inventoryMovement->inventoryItemId, $inventoryMovement->fromInventoryLocation);
$fromTotal = $fromLevel->{$fromInventoryTransactionType . 'Total'};

$movableTo = $movableTo->toArray();
Expand Down Expand Up @@ -705,10 +695,7 @@ public function actionUnfulfilledOrders(): Response
$inventoryLocationId = Craft::$app->getRequest()->getParam('inventoryLocationId');
$inventoryItemId = Craft::$app->getRequest()->getParam('inventoryItemId');

$inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($inventoryLocationId);
$inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId);

$orders = Plugin::getInstance()->getInventory()->getUnfulfilledOrders($inventoryItem, $inventoryLocation);
$orders = Plugin::getInstance()->getInventory()->getUnfulfilledOrders($inventoryItemId, $inventoryLocationId);

$title = Craft::t('commerce', '{count} Unfulfilled Orders', [
'count' => count($orders),
Expand Down
Loading
Loading