Repository: saleor/saleor

Issue:
Bug: Stock update webhook is not triggered
### What are you trying to achieve?

Get notified when variant is out of stock

### Steps to reproduce the problem

1. Create a new variant with quantity of 0
OUT_OF_STOCK webhook was sent
2. Add a quantity of 5
BACK_IN_STOCK webhook was sent
5. I've made two orders (with 3 quantity and 2 quantity) (stocks at this moment - allocated 5, quantity 5)
6. OUT_OF_STOCK webhook was sent
7. I added another 10 items to the stock (allocated 5, quantity 15)

### What did you expect to happen?

I expect to BACK_IN_STOCK webhook to be sent, but it's not. The only workaround I've found is to set quantity to 0 and then set it to 15 in step 7.


### Logs

_No response_

### Environment

dashboard v3.7.9
core v3.7.27


Discussion:
Also, when I tried to add (through dashboard) a new variant, and after creating variant I assigned new warehouse with quantity 0 - it triggered `back_in_stock` event, not `out_of_stock` like was mentioned. That should also be taken into consideration when fixing this bug. 

Resolving patch:
diff --git a/saleor/graphql/product/bulk_mutations/products.py b/saleor/graphql/product/bulk_mutations/products.py
--- a/saleor/graphql/product/bulk_mutations/products.py
+++ b/saleor/graphql/product/bulk_mutations/products.py
@@ -890,13 +890,18 @@ def update_or_create_variant_stocks(cls, variant, stocks_data, warehouses, manag
             stock, is_created = warehouse_models.Stock.objects.get_or_create(
                 product_variant=variant, warehouse=warehouse
             )
-
-            if is_created or (stock.quantity <= 0 and stock_data["quantity"] > 0):
+            if is_created or (
+                (stock.quantity - stock.quantity_allocated)
+                <= 0
+                < stock_data["quantity"]
+            ):
                 transaction.on_commit(
                     lambda: manager.product_variant_back_in_stock(stock)
                 )
 
-            if stock_data["quantity"] <= 0:
+            if stock_data["quantity"] <= 0 or (
+                stock_data["quantity"] - stock.quantity_allocated <= 0
+            ):
                 transaction.on_commit(
                     lambda: manager.product_variant_out_of_stock(stock)
                 )


Test patch:
diff --git a/saleor/graphql/product/tests/mutations/test_product_variant_stocks_update.py b/saleor/graphql/product/tests/mutations/test_product_variant_stocks_update.py
--- a/saleor/graphql/product/tests/mutations/test_product_variant_stocks_update.py
+++ b/saleor/graphql/product/tests/mutations/test_product_variant_stocks_update.py
@@ -360,6 +360,72 @@ def test_update_or_create_variant_with_back_in_stock_webhooks_only_failed(
     )
 
 
+@patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
+@patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
+def test_update_or_create_variant_with_back_in_stock_webhooks_with_allocations(
+    product_variant_stock_out_of_stock_webhook,
+    product_variant_back_in_stock_webhook,
+    settings,
+    variant,
+    warehouse,
+):
+    stock_quantity = 4
+    stock = Stock.objects.create(
+        warehouse=warehouse, product_variant=variant, quantity=stock_quantity
+    )
+
+    # given
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
+    plugins = get_plugins_manager()
+    stock.quantity_allocated = stock_quantity
+    stock.save(update_fields=["quantity_allocated"])
+    stocks_data = [
+        {"quantity": stock.quantity_allocated + 1},
+    ]
+
+    # when
+    ProductVariantStocksUpdate.update_or_create_variant_stocks(
+        variant, stocks_data, [warehouse], plugins
+    )
+    # then
+    flush_post_commit_hooks()
+    product_variant_back_in_stock_webhook.assert_called_once_with(stock)
+    product_variant_stock_out_of_stock_webhook.assert_not_called()
+
+
+@patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
+@patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
+def test_update_or_create_variant_with_out_of_stock_webhooks_with_allocations(
+    product_variant_stock_out_of_stock_webhook,
+    product_variant_back_in_stock_webhook,
+    settings,
+    variant,
+    warehouse,
+):
+    stock_quantity = 4
+    stock = Stock.objects.create(
+        warehouse=warehouse, product_variant=variant, quantity=stock_quantity
+    )
+
+    # given
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
+    plugins = get_plugins_manager()
+    stock.quantity_allocated = stock_quantity - 1
+    stock.save(update_fields=["quantity_allocated"])
+    stocks_data = [
+        {"quantity": stock.quantity_allocated},
+    ]
+
+    # when
+    ProductVariantStocksUpdate.update_or_create_variant_stocks(
+        variant, stocks_data, [warehouse], plugins
+    )
+    # then
+    flush_post_commit_hooks()
+    product_variant_stock_out_of_stock_webhook.assert_called_once_with(stock)
+    product_variant_back_in_stock_webhook.assert_not_called()
+
+
 @patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
 def test_update_or_create_variant_stocks_with_out_of_stock_webhook_only(