Repository: saleor/saleor

Issue:
Bug: Integrity error in FulfillmentReturnProducts mutation
### What are you trying to achieve?

After migrating from version 3.1 to 3.9, I am unable to perform a refund for an order that was created before the migration.

### Steps to reproduce the problem

1. Create an order on Saleor 3.1 (With old id's, Orders from populatedb also work)
2. Migrate Saleor to 3.9
3. Try to run `FulfillmentReturnProducts` for pre-migration orders. 

### What did you expect to happen?

I should be able to return the product. 

### Logs

```
UniqueViolation: duplicate key value violates unique constraint "order_orderline_old_id_1da97079_uniq"
DETAIL:  Key (old_id)=(9029517) already exists.

  File "django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "ddtrace/contrib/dbapi/__init__.py", line 112, in execute
    return self._trace_method(self.__wrapped__.execute, self._self_datadog_name, query, {}, query, *args, **kwargs)
  File "ddtrace/contrib/psycopg/patch.py", line 58, in _trace_method
    return super(Psycopg2TracedCursor, self)._trace_method(method, name, resource, extra_tags, *args, **kwargs)
  File "ddtrace/contrib/dbapi/__init__.py", line 75, in _trace_method
    return method(*args, **kwargs)
IntegrityError: duplicate key value violates unique constraint "order_orderline_old_id_1da97079_uniq"
DETAIL:  Key (old_id)=(9029517) already exists.

  File "graphql/execution/executor.py", line 452, in resolve_or_error
    return executor.execute(resolve_fn, source, info, **args)
  File "graphql/execution/executors/sync.py", line 16, in execute
    return fn(*args, **kwargs)
  File "saleor/graphql/core/mutations.py", line 392, in mutate
    response = cls.perform_mutation(root, info, **data)
  File "saleor/graphql/order/mutations/fulfillment_return_products.py", line 162, in perform_mutation
    response = create_fulfillments_for_returned_products(
  File "saleor/order/actions.py", line 1426, in create_fulfillments_for_returned_products
    replace_fulfillment, new_order = process_replace(
  File "saleor/order/actions.py", line 1342, in process_replace
    new_order = create_replace_order(
  File "saleor/order/actions.py", line 1174, in create_replace_order
    OrderLine.objects.bulk_create(lines_to_create)
  File "django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "django/db/models/query.py", line 502, in bulk_create
    returned_columns = self._batched_insert(
  File "django/db/models/query.py", line 1287, in _batched_insert
    inserted_rows.extend(self._insert(
  File "django/db/models/query.py", line 1270, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "django/db/models/sql/compiler.py", line 1416, in execute_sql
    cursor.execute(sql, params)
  File "ddtrace/contrib/dbapi/__init__.py", line 112, in execute
    return self._trace_method(self.__wrapped__.execute, self._self_datadog_name, query, {}, query, *args, **kwargs)
  File "ddtrace/contrib/psycopg/patch.py", line 58, in _trace_method
    return super(Psycopg2TracedCursor, self)._trace_method(method, name, resource, extra_tags, *args, **kwargs)
  File "ddtrace/contrib/dbapi/__init__.py", line 75, in _trace_method
    return method(*args, **kwargs)
  File "django/db/backends/utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "saleor/graphql/views.py", line 54, in tracing_wrapper
    return execute(sql, params, many, context)
  File "django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "ddtrace/contrib/dbapi/__init__.py", line 112, in execute
    return self._trace_method(self.__wrapped__.execute, self._self_datadog_name, query, {}, query, *args, **kwargs)
  File "ddtrace/contrib/psycopg/patch.py", line 58, in _trace_method
    return super(Psycopg2TracedCursor, self)._trace_method(method, name, resource, extra_tags, *args, **kwargs)
  File "ddtrace/contrib/dbapi/__init__.py", line 75, in _trace_method
    return method(*args, **kwargs)
```

### Environment

Saleor version: 3.9
OS and version: Saleor Cloudl



Discussion:
Sentry issue: [SALEOR-CORE-VG](https://saleor.sentry.io/issues/3809978681/?referrer=github_integration)
Reported in: https://saleorcommerce.slack.com/archives/C017L12TRJS/p1677580612574009?thread_ts=1677580555.771109&cid=C017L12TRJS

Resolving patch:
diff --git a/saleor/order/actions.py b/saleor/order/actions.py
--- a/saleor/order/actions.py
+++ b/saleor/order/actions.py
@@ -1139,6 +1139,7 @@ def create_replace_order(
             order_line.order = replace_order
             order_line.quantity = line_data.quantity
             order_line.quantity_fulfilled = 0
+            order_line.old_id = None
             # we set order_line_id as a key to use it for iterating over fulfillment
             # items
             order_line_to_create[order_line_id] = order_line
@@ -1165,6 +1166,7 @@ def create_replace_order(
             order_line = order_line_from_fulfillment  # type: ignore
             order_line_id = order_line.pk
             order_line.pk = None
+            order_line.old_id = None
             order_line.order = replace_order
             order_line.quantity = fulfillment_line_data.quantity
             order_line.quantity_fulfilled = 0


Test patch:
diff --git a/saleor/order/tests/test_order_actions_create_fulfillments_for_returned_products.py b/saleor/order/tests/test_order_actions_create_fulfillments_for_returned_products.py
--- a/saleor/order/tests/test_order_actions_create_fulfillments_for_returned_products.py
+++ b/saleor/order/tests/test_order_actions_create_fulfillments_for_returned_products.py
@@ -644,3 +644,80 @@ def test_create_return_fulfillment_with_lines_already_refunded(
     assert returned_and_refunded_fulfillment.shipping_refund_amount is None
 
     mocked_order_updated.assert_called_once_with(fulfilled_order)
+
+
+@patch("saleor.plugins.manager.PluginsManager.order_updated")
+@patch("saleor.order.actions.gateway.refund")
+def test_create_return_fulfillment_only_order_lines_with_old_ids(
+    mocked_refund,
+    mocked_order_updated,
+    order_with_lines,
+    payment_dummy_fully_charged,
+    staff_user,
+):
+    order_with_lines.payments.add(payment_dummy_fully_charged)
+    payment = order_with_lines.get_last_payment()
+
+    order_lines_to_return = order_with_lines.lines.all()
+    order_lines_to_return[0].old_id = 16
+    order_lines_to_return[1].old_id = 12
+    original_quantity = {
+        line.id: line.quantity_unfulfilled for line in order_with_lines.lines.all()
+    }
+    order_line_ids = order_lines_to_return.values_list("id", flat=True)
+    original_allocations = list(
+        Allocation.objects.filter(order_line_id__in=order_line_ids)
+    )
+    lines_count = order_with_lines.lines.count()
+
+    response = create_fulfillments_for_returned_products(
+        user=staff_user,
+        app=None,
+        order=order_with_lines,
+        payment=payment,
+        transactions=[],
+        order_lines=[
+            OrderLineInfo(line=line, quantity=2, replace=False)
+            for line in order_lines_to_return
+        ],
+        fulfillment_lines=[],
+        manager=get_plugins_manager(),
+    )
+    returned_fulfillment, replaced_fulfillment, replace_order = response
+
+    returned_fulfillment_lines = returned_fulfillment.lines.all()
+    assert returned_fulfillment.status == FulfillmentStatus.RETURNED
+    assert len(returned_fulfillment_lines) == lines_count
+    for fulfillment_line in returned_fulfillment_lines:
+        assert fulfillment_line.quantity == 2
+        assert fulfillment_line.order_line_id in order_line_ids
+    for line in order_lines_to_return:
+        assert line.quantity_unfulfilled == original_quantity.get(line.pk) - 2
+
+    current_allocations = Allocation.objects.in_bulk(
+        [allocation.pk for allocation in original_allocations]
+    )
+    for original_allocation in original_allocations:
+        current_allocation = current_allocations.get(original_allocation.pk)
+        assert (
+            original_allocation.quantity_allocated - 2
+            == current_allocation.quantity_allocated
+        )
+    assert not mocked_refund.called
+    assert not replace_order
+
+    # check if we have correct events
+    flush_post_commit_hooks()
+    events = order_with_lines.events.all()
+    assert events.count() == 1
+    returned_event = events[0]
+    assert returned_event.type == OrderEvents.FULFILLMENT_RETURNED
+    assert len(returned_event.parameters["lines"]) == 2
+    event_lines = returned_event.parameters["lines"]
+    assert order_lines_to_return.filter(id=event_lines[0]["line_pk"]).exists()
+    assert event_lines[0]["quantity"] == 2
+
+    assert order_lines_to_return.filter(id=event_lines[1]["line_pk"]).exists()
+    assert event_lines[1]["quantity"] == 2
+
+    mocked_order_updated.assert_called_once_with(order_with_lines)