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

Fix stock import for Product Variants #1490

Merged
merged 9 commits into from
Sep 26, 2024
100 changes: 75 additions & 25 deletions src/elements/CommerceProduct.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Craft;
use craft\base\ElementInterface;
use craft\commerce\collections\UpdateInventoryLevelCollection;
use craft\commerce\elements\Product;
use craft\commerce\elements\Product as ProductElement;
use craft\commerce\elements\Variant as VariantElement;
use craft\commerce\models\inventory\UpdateInventoryLevel;
Expand All @@ -21,6 +22,7 @@
use craft\feedme\services\Process;
use craft\fields\Matrix;
use craft\fields\Table;
use craft\helpers\ArrayHelper;
use craft\helpers\Json;
use DateTime;
use Exception;
Expand Down Expand Up @@ -87,15 +89,26 @@ public function init(): void
parent::init();

// Hook into the process service on each step - we need to re-arrange the feed mapping
Event::on(Process::class, Process::EVENT_STEP_BEFORE_PARSE_CONTENT, function(FeedProcessEvent $event) {
Event::on(Process::class, Process::EVENT_STEP_BEFORE_ELEMENT_MATCH, function(FeedProcessEvent $event) {
if ($event->feed['elementType'] === ProductElement::class) {
$this->_preParseVariants($event);
$this->_checkForVariantMatches($event);
}
});

Event::on(Process::class, Process::EVENT_STEP_BEFORE_ELEMENT_MATCH, function(FeedProcessEvent $event) {
Event::on(Process::class, Process::EVENT_STEP_BEFORE_PARSE_CONTENT, function(FeedProcessEvent $event) {
if ($event->feed['elementType'] === ProductElement::class) {
$this->_checkForVariantMatches($event);
// at this point we've matched existing elements;
// if $event->element->id is null then we haven't found a match so create an unsaved draft of the product
// so that the variants can get saved right
if (!$event->element->id) {
$originalScenario = $event->element->getScenario();
$event->element->setScenario(\craft\base\Element::SCENARIO_ESSENTIALS);
if (!Craft::$app->getDrafts()->saveElementAsDraft($event->element, null, null, null, false)) {
throw new Exception('Unable to create product element as unsaved');
}
$event->element->setScenario($originalScenario);
}
$this->_preParseVariants($event);
}
});

Expand Down Expand Up @@ -164,6 +177,11 @@ public function save($element, $settings): bool
{
$this->beforeSave($element, $settings);

if ($this->element->getIsDraft()) {
$this->element->setDirtyAttributes(['variants']);
$this->element = Craft::$app->getDrafts()->applyDraft($this->element);
}

if (!Craft::$app->getElements()->saveElement($this->element, true, true, Hash::get($this->feed, 'updateSearchIndexes'))) {
$errors = [$this->element->getErrors()];

Expand Down Expand Up @@ -274,6 +292,7 @@ private function _parseVariants($event): void
$feed = $event->feed;
$feedData = $event->feedData;
$contentData = $event->contentData;
/** @var Product $element */
$element = $event->element;

$variantMapping = Hash::get($feed, 'fieldMapping.variants');
Expand Down Expand Up @@ -431,8 +450,6 @@ private function _parseVariants($event): void
$variants[$sku] = new VariantElement();
}

$variants[$sku]->product = $element;

// We are going to handle stock after the product and variants save
$stock = null;
if (isset($attributeData['stock'])) {
Expand Down Expand Up @@ -480,31 +497,64 @@ private function _parseVariants($event): void

private function _inventoryUpdate($event): void
{
/** @var Product $product */
$product = $event->element;

// Index variants by SKU for lookup:
$variantsBySku = ArrayHelper::index($event->contentData['variants'], 'sku');

/** @var Commerce $commercePlugin */
$commercePlugin = Commerce::getInstance();
$variants = $event->element->getVariants();
$variants = $product->getVariants();

// Queue up a changeset:
$updateInventoryLevels = UpdateInventoryLevelCollection::make();

foreach ($variants as $variant) {
if ($inventoryItem = $commercePlugin->getInventory()->getInventoryItemByPurchasable($variant)) {
/** @var InventoryLevel $firstInventoryLevel */
$firstInventoryLevel = $commercePlugin->getInventory()->getInventoryLevelsForPurchasable($variant)->first();
if ($firstInventoryLevel && $firstInventoryLevel->getInventoryLocation()) {
$feedData = $event->feedData;
$data = Json::decodeIfJson($event->feedData, true);
$stock = $data['stock'] ?? 0;
$updateInventoryLevels->push(new UpdateInventoryLevel([
'type' => \craft\commerce\enums\InventoryTransactionType::AVAILABLE->value,
'updateAction' => \craft\commerce\enums\InventoryUpdateQuantityType::SET,
'inventoryItem' => $inventoryItem,
'inventoryLocation' => $firstInventoryLevel->getInventoryLocation(),
'quantity' => $stock,
'note' => '',
])
);
}
// Is this SKU even present in our import data?
if (!isset($variantsBySku[$variant->sku])) {
continue;
}

if (!$variant->inventoryTracked) {
Plugin::info(sprintf('Variant %s is not configured to track stock.', $variant->sku));

continue;
}

$stock = $variantsBySku[$variant->sku]['stock'] ?? null;

// What if the `stock` key wasn't in the import data?
if (is_null($stock)) {
Plugin::error(sprintf('No stock value was present in the import data for %s.', $variant->sku));

continue;
}

// Load InventoryItem model:
$inventoryItem = $commercePlugin->getInventory()->getInventoryItemByPurchasable($variant);

/** @var InventoryLevel $firstInventoryLevel */
$level = $commercePlugin->getInventory()->getInventoryLevelsForPurchasable($variant)->first();
$location = $level->getInventoryLocation();

if (!$level || !$location) {
// Again, looks like there's nothing to track…
continue;
}

$update = new UpdateInventoryLevel([
'type' => \craft\commerce\enums\InventoryTransactionType::AVAILABLE->value,
'updateAction' => \craft\commerce\enums\InventoryUpdateQuantityType::SET,
'inventoryItem' => $inventoryItem,
'inventoryLocation' => $location,
'quantity' => $stock,
'note' => sprintf('Imported via feed ID #%s', $event->feed['id']),
]);

$updateInventoryLevels->push($update);

Plugin::info(sprintf('Updating stock for the default inventory location for %s to %s.', $variant->sku, $stock));
}

if ($updateInventoryLevels->count() > 0) {
Expand Down
Loading