Repository: saleor/saleor

Issue:
Automatically fulfilling digital order lines causes AttributeError in API 
I'm trying to use automatic fulfillment for digital products that have associated digital content. As they are digital download products with unlimited availability, Track Inventory for these product variants is disabled. It seems that I must have stock assigned in warehouses for this digital product to work otherwise the checkout will return an insufficient stock error.

But when I successfully purchase a digital download product during the checkout process the API raises the following error:
AttributeError: 'NoneType' object has no attribute 'stock' 
at the following line:
https://github.com/saleor/saleor/blob/8e4caaae1d6ffa17fbf0b15985ebb5e2a266f658/saleor/order/actions.py#L604

This stops the automatic fulfillment of the digital order lines. I can workaround this issue by enabling Track Inventory with some stock allocated to a warehouse for these digital product variants and the error doesn't occur which allows the digital products to be automatically fulfilled. However when I do this I also notice that the Fulfillment status for this order doesn't get updated to Fulfilled only the order line is set to Fulfilled even though it is a single item order.

### Steps to reproduce the problem
<!-- Adding logs from the console, as well as query/response help us fix the bug faster -->
1. Create a digital product with variant that has Track Inventory disabled but has stock assigned to a warehouse
2. Add a digital content file to that product variant with automatic fulfillment enabled
3. Successfully purchase that digital product variant through the checkout

Here is a Traceback of the error from Saleor v3.3.11:

```
ERROR saleor.graphql.errors.unhandled A query failed unexpectedly [PID:23:Thread-13]
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/graphql/execution/executor.py", line 452, in resolve_or_error
    return executor.execute(resolve_fn, source, info, **args)
  File "/usr/local/lib/python3.9/site-packages/graphql/execution/executors/sync.py", line 16, in execute
    return fn(*args, **kwargs)
  File "/app/saleor/graphql/core/mutations.py", line 374, in mutate
    response = cls.perform_mutation(root, info, **data)
  File "/app/saleor/graphql/checkout/mutations/checkout_complete.py", line 170, in perform_mutation
    order, action_required, action_data = complete_checkout(
  File "/usr/local/lib/python3.9/contextlib.py", line 126, in __exit__
    next(self.gen)
  File "/app/saleor/core/transactions.py", line 18, in transaction_with_commit_on_errors
    error = e
  File "/usr/local/lib/python3.9/contextlib.py", line 126, in __exit__
    next(self.gen)
  File "/app/saleor/core/tracing.py", line 28, in traced_atomic_transaction
    yield
  File "/usr/local/lib/python3.9/site-packages/django/db/transaction.py", line 290, in __exit__
    connection.set_autocommit(True)
  File "/usr/local/lib/python3.9/site-packages/django/db/backends/base/base.py", line 420, in set_autocommit
    self.run_and_clear_commit_hooks()
  File "/usr/local/lib/python3.9/site-packages/django/db/backends/base/base.py", line 653, in run_and_clear_commit_hooks
    func()
  File "/app/saleor/checkout/complete_checkout.py", line 552, in <lambda>
    lambda: order_created(
  File "/app/saleor/order/actions.py", line 93, in order_created
    order_captured(
  File "/app/saleor/order/actions.py", line 308, in order_captured
    handle_fully_paid_order(manager, order_info, user, app)
  File "/app/saleor/order/actions.py", line 143, in handle_fully_paid_order
    automatically_fulfill_digital_lines(order_info, manager)
  File "/usr/local/lib/python3.9/contextlib.py", line 79, in inner
    return func(*args, **kwds)
  File "/app/saleor/order/actions.py", line 597, in automatically_fulfill_digital_lines
    line_data.warehouse_pk = allocation.stock.warehouse.pk  # type: ignore
AttributeError: 'NoneType' object has no attribute 'stock'
```

**System information**
Saleor version: v3.3.11

Operating system: Docker Desktop v4.8.2 on MacOS v10.14.6



Discussion:
+1

noticed same on Saleor 3.2.x
Hi, I'm working with digital products, but the fulfillment is not automatic, there is a special configuration for that? Thanks!
> Hi, I'm working with digital products, but the fulfillment is not automatic, there is a special configuration for that? Thanks!

Unfortunately automatic digital product fulfillment isn't supported in the dashboard. It's only something that can be set up from the API. In general Saleor doesn't have proper support for digital products and there doesn't seem to be anything in the development roadmap to address it either.

If you want good support for digital products and automatic fulfillment, it may be best to look at using another ecommerce solution instead.
@simon-online we will add high priority to fix this, hopefully, we can deliver it in 3.5
Hi @simon-online did you find a way to do that in the API? Thanks!

Resolving patch:
diff --git a/saleor/order/actions.py b/saleor/order/actions.py
--- a/saleor/order/actions.py
+++ b/saleor/order/actions.py
@@ -617,7 +617,15 @@ def automatically_fulfill_digital_lines(
             FulfillmentLine(fulfillment=fulfillment, order_line=line, quantity=quantity)
         )
         allocation = line.allocations.first()
-        line_data.warehouse_pk = allocation.stock.warehouse.pk  # type: ignore
+        if allocation:
+            line_data.warehouse_pk = allocation.stock.warehouse.pk
+        else:
+            # allocation is not created when track inventory for given product
+            # is turned off so it doesn't matter which warehouse we'll use
+            line_data.warehouse_pk = (
+                line_data.variant.stocks.first().warehouse  # type: ignore
+            )
+
         lines_info.append(line_data)
 
     FulfillmentLine.objects.bulk_create(fulfillments)


Test patch:
diff --git a/saleor/order/tests/test_order_actions.py b/saleor/order/tests/test_order_actions.py
--- a/saleor/order/tests/test_order_actions.py
+++ b/saleor/order/tests/test_order_actions.py
@@ -576,3 +576,49 @@ def test_fulfill_digital_lines(
     assert fulfillment_lines.count() == 1
     assert line.digital_content_url
     assert mock_email_fulfillment.called
+
+
+@patch("saleor.order.actions.send_fulfillment_confirmation_to_customer")
+@patch("saleor.order.utils.get_default_digital_content_settings")
+def test_fulfill_digital_lines_no_allocation(
+    mock_digital_settings, mock_email_fulfillment, order_with_lines, media_root
+):
+    # given
+    mock_digital_settings.return_value = {"automatic_fulfillment": True}
+    line = order_with_lines.lines.all()[0]
+
+    image_file, image_name = create_image()
+    variant = line.variant
+
+    product_type = variant.product.product_type
+    product_type.is_digital = True
+    product_type.is_shipping_required = False
+    product_type.save(update_fields=["is_digital", "is_shipping_required"])
+
+    digital_content = DigitalContent.objects.create(
+        content_file=image_file, product_variant=variant, use_default_settings=True
+    )
+
+    variant.digital_content = digital_content
+    variant.track_inventory = False
+    variant.save()
+
+    line.is_shipping_required = False
+    line.allocations.all().delete()
+    line.save()
+
+    order_with_lines.refresh_from_db()
+    order_info = fetch_order_info(order_with_lines)
+    manager = get_plugins_manager()
+
+    # when
+    automatically_fulfill_digital_lines(order_info, manager)
+
+    # then
+    line.refresh_from_db()
+    fulfillment = Fulfillment.objects.get(order=order_with_lines)
+    fulfillment_lines = fulfillment.lines.all()
+
+    assert fulfillment_lines.count() == 1
+    assert line.digital_content_url
+    assert mock_email_fulfillment.called