details:   https://code.tryton.org/tryton/commit/3e58474b6aed
branch:    default
user:      Cédric Krier <[email protected]>
date:      Sun Mar 29 20:05:46 2026 +0200
description:
        Replace ShopifyAPI by shopifyapp

        Closes #14243
diffstat:

 modules/web_shop_shopify/CHANGELOG                                          |  
  1 +
 modules/web_shop_shopify/__init__.py                                        |  
  6 +-
 modules/web_shop_shopify/common.py                                          |  
  5 +-
 modules/web_shop_shopify/exceptions.py                                      |  
  8 +-
 modules/web_shop_shopify/graphql.py                                         |  
 18 +-
 modules/web_shop_shopify/product.py                                         |  
 15 +-
 modules/web_shop_shopify/pyproject.toml                                     |  
  3 +-
 modules/web_shop_shopify/routes.py                                          |  
 27 +-
 modules/web_shop_shopify/sale.py                                            |  
171 +--
 modules/web_shop_shopify/shopify_retry.py                                   |  
 87 --
 modules/web_shop_shopify/stock.py                                           |  
 21 +-
 modules/web_shop_shopify/tests/scenario_web_shop_shopify.rst                |  
 49 +-
 modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst    |  
 39 +-
 modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst |  
 31 +-
 modules/web_shop_shopify/tests/test_scenario.py                             |  
  4 +-
 modules/web_shop_shopify/tests/tools.py                                     |  
126 +-
 modules/web_shop_shopify/view/shop_form.xml                                 |  
 15 +-
 modules/web_shop_shopify/web.py                                             |  
420 +++++----
 18 files changed, 490 insertions(+), 556 deletions(-)

diffs (1932 lines):

diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/CHANGELOG
--- a/modules/web_shop_shopify/CHANGELOG        Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/CHANGELOG        Sun Mar 29 20:05:46 2026 +0200
@@ -1,3 +1,4 @@
+* Replace ShopifyAPI by shopifyapp
 * Add support for pick-up delivery method
 * Log the update of sales
 * Add support for gift card products
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/__init__.py
--- a/modules/web_shop_shopify/__init__.py      Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/__init__.py      Sun Mar 29 20:05:46 2026 +0200
@@ -1,8 +1,8 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
 
-from . import routes, shopify_retry
+from . import routes
+
+SHOPIFY_VERSION = '2024-10'
 
 __all__ = [routes]
-
-shopify_retry.patch()
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/common.py
--- a/modules/web_shop_shopify/common.py        Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/common.py        Sun Mar 29 20:05:46 2026 +0200
@@ -68,8 +68,9 @@
                 and getattr(self, 'web_shop', None)
                 and self.shopify_identifier_char):
             return urljoin(
-                self.web_shop.shopify_url + '/admin/',
-                f'{self.shopify_resource}/{self.shopify_identifier_char}')
+                self.web_shop.shopify_url,
+                f'admin/{self.shopify_resource}/{self.shopify_identifier_char}'
+                )
 
     @classmethod
     def copy(cls, records, default=None):
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/exceptions.py
--- a/modules/web_shop_shopify/exceptions.py    Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/exceptions.py    Sun Mar 29 20:05:46 2026 +0200
@@ -1,6 +1,12 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
-from trytond.exceptions import UserError, UserWarning
+from trytond.exceptions import TrytonException, UserError, UserWarning
+
+
+class GraphQLException(TrytonException):
+    def __init__(self, message):
+        super().__init__(message)
+        self.message = message
 
 
 class ShopifyError(UserError):
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/graphql.py
--- a/modules/web_shop_shopify/graphql.py       Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/graphql.py       Sun Mar 29 20:05:46 2026 +0200
@@ -1,10 +1,6 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
 
-import shopify
-
-from .shopify_retry import GraphQLException
-
 
 def deep_merge(d1, d2):
     "Merge 2 fields dictionary"
@@ -27,7 +23,7 @@
     return _format('', fields).strip()
 
 
-def iterate(query, params, query_name, path=None, data=None):
+def iterate(shop, query, params, query_name, path=None, data=None):
     def getter(data):
         if path:
             for name in path.split('.'):
@@ -35,13 +31,11 @@
         return data
 
     if data is None:
-        data = shopify.GraphQL().execute(
+        data = shop.shopify_request(
             query, {
                 **params,
                 'cursor': None,
-                })['data'][query_name]
-        if errors := data.get('userErrors'):
-            raise GraphQLException({'errors': errors})
+                }).data[query_name]
 
     while True:
         lst = getter(data)
@@ -49,10 +43,8 @@
             yield item
         if not lst['pageInfo']['hasNextPage']:
             break
-        data = shopify.GraphQL().execute(
+        data = shop.shopify_request(
             query, {
                 **params,
                 'cursor': lst['pageInfo']['endCursor'],
-                })['data'][query_name]
-        if errors := data.get('userErrors'):
-            raise GraphQLException({'errors': errors})
+                }).data[query_name]
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/product.py
--- a/modules/web_shop_shopify/product.py       Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/product.py       Sun Mar 29 20:05:46 2026 +0200
@@ -4,7 +4,6 @@
 from decimal import Decimal
 from urllib.parse import urljoin
 
-import shopify
 from sql.conditionals import NullIf
 from sql.operators import Equal
 
@@ -47,12 +46,12 @@
         shopify_id = self.get_shopify_identifier(shop)
         if shopify_id:
             shopify_id = id2gid('Collection', shopify_id)
-            collection = shopify.GraphQL().execute(
+            collection = shop.shopify_request(
                 QUERY_COLLECTION % {
                     'fields': graphql.selection({
                             'id': None,
                             }),
-                    }, {'id': shopify_id})['data']['collection'] or {}
+                    }, {'id': shopify_id}).data['collection'] or {}
         else:
             collection = {}
         collection['title'] = self.name[:255]
@@ -144,13 +143,13 @@
         product = {}
         if shopify_id:
             shopify_id = id2gid('Product', shopify_id)
-            product = shopify.GraphQL().execute(
+            product = shop.shopify_request(
                 QUERY_PRODUCT % {
                     'fields': graphql.selection({
                             'id': None,
                             'status': None,
                             }),
-                    }, {'id': shopify_id})['data']['product'] or {}
+                    }, {'id': shopify_id}).data['product'] or {}
             if product.get('status') == 'ARCHIVED':
                 product['status'] = 'ACTIVE'
         product['title'] = self.name
@@ -298,12 +297,12 @@
         shopify_id = self.get_shopify_identifier(shop)
         if shopify_id:
             shopify_id = id2gid('ProductVariant', shopify_id)
-            variant = shopify.GraphQL().execute(
+            variant = shop.shopify_request(
                 QUERY_VARIANT % {
                     'fields': graphql.selection({
                             'id': None,
                             }),
-                    }, {'id': shopify_id})['data']['productVariant'] or {}
+                    }, {'id': shopify_id}).data['productVariant'] or {}
         else:
             variant = {}
         sale_price = self.shopify_price(
@@ -405,7 +404,7 @@
         url = super().get_url(name)
         if (self.shop.type == 'shopify'
                 and (handle := self.product.template.shopify_handle)):
-            url = urljoin(self.shop.shopify_url + '/', f'products/{handle}')
+            url = urljoin(self.shop.shopify_url, f'products/{handle}')
         return url
 
 
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/pyproject.toml
--- a/modules/web_shop_shopify/pyproject.toml   Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/pyproject.toml   Sun Mar 29 20:05:46 2026 +0200
@@ -52,10 +52,9 @@
 
 [tool.hatch.metadata.hooks.tryton]
 dependencies = [
-    'ShopifyAPI',
-    'pyactiveresource',
     'python-dateutil',
     'python-sql',
+    'shopifyapp',
     ]
 copyright = 'COPYRIGHT'
 readme = 'README.rst'
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/routes.py
--- a/modules/web_shop_shopify/routes.py        Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/routes.py        Sun Mar 29 20:05:46 2026 +0200
@@ -1,10 +1,10 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
-import base64
-import hashlib
-import hmac
+
 import logging
 
+from shopify_app import RequestInput, verify_webhook_req
+
 from trytond.protocols.wrappers import (
     HTTPStatus, Response, abort, redirect, with_pool, with_transaction)
 from trytond.wsgi import app
@@ -12,10 +12,12 @@
 logger = logging.getLogger(__name__)
 
 
-def verify_webhook(data, hmac_header, secret):
-    digest = hmac.new(secret, data, hashlib.sha256).digest()
-    computed_hmac = base64.b64encode(digest)
-    return hmac.compare_digest(computed_hmac, hmac_header.encode('utf-8'))
+def request_to_shopify_req(request):
+    return RequestInput(
+        method=request.method,
+        headers=request.headers,
+        url=request.url,
+        body=request.get_data())
 
 
 @app.route(
@@ -26,12 +28,11 @@
     Sale = pool.get('sale.sale')
     Shop = pool.get('web.shop')
     shop = Shop.get(shop)
-    data = request.get_data()
-    verified = verify_webhook(
-        data, request.headers.get('X-Shopify-Hmac-SHA256'),
-        shop.shopify_webhook_shared_secret.encode('utf-8'))
-    if not verified:
-        abort(HTTPStatus.UNAUTHORIZED)
+
+    result = verify_webhook_req(request_to_shopify_req(request))
+    if not result.ok:
+        logger.debug("unauthorized %s", result)
+        abort(result.response.status, result.response.body)
 
     topic = request.headers.get('X-Shopify-Topic')
     order = request.get_json()
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/sale.py
--- a/modules/web_shop_shopify/sale.py  Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/sale.py  Sun Mar 29 20:05:46 2026 +0200
@@ -5,7 +5,6 @@
 from itertools import zip_longest
 
 import dateutil
-import shopify
 
 from trytond.i18n import gettext
 from trytond.model import ModelView, Unique, fields
@@ -18,8 +17,7 @@
 
 from . import graphql
 from .common import IdentifierMixin, gid2id, id2gid, setattr_changed
-from .exceptions import ShopifyError
-from .shopify_retry import GraphQLException
+from .exceptions import GraphQLException, ShopifyError
 
 QUERY_ORDER = '''\
 query GetOrder($id: ID!) {
@@ -315,7 +313,7 @@
             sale.shopify_identifier = gid2id(order['id'])
 
             shopify_fulfillments = graphql.iterate(
-                QUERY_ORDER_CURSOR % {
+                shop, QUERY_ORDER_CURSOR % {
                     'fields': graphql.selection({
                             'fulfillmentOrders(first: 10, after: $cursor)': (
                                 shopify_fields[
@@ -376,7 +374,7 @@
         refund_line_items = defaultdict(list)
         for refund in order['refunds']:
             shopify_refund_line_items = graphql.iterate(
-                QUERY_REFUND_CURSOR % {
+                shop, QUERY_REFUND_CURSOR % {
                     'fields': graphql.selection({
                             'refundLineItems(first: 10, after: $cursor)': (
                                 shopify_fields['refunds'][
@@ -391,7 +389,7 @@
 
         line2warehouses = defaultdict(set)
         shopify_fulfillment_orders = graphql.iterate(
-            QUERY_ORDER_CURSOR % {
+            shop, QUERY_ORDER_CURSOR % {
                 'fields': graphql.selection({
                         'fulfillmentOrders(first: 10, after: $cursor)': (
                             shopify_fields[
@@ -414,7 +412,7 @@
                 if method_type == 'PICK_UP':
                     shipment_address = sale.warehouse.address
             shopify_line_items = graphql.iterate(
-                QUERY_FULFILLMENT_ORDER % {
+                shop, QUERY_FULFILLMENT_ORDER % {
                     'fields': graphql.selection({
                             'lineItems(first: 100, after: $cursor)': (
                                 shopify_fields[
@@ -451,7 +449,7 @@
             l.shopify_identifier]
         lines = []
         shopify_line_items = graphql.iterate(
-            QUERY_ORDER_CURSOR % {
+            shop, QUERY_ORDER_CURSOR % {
                 'fields': graphql.selection({
                         'lineItems(first: 100, after: $cursor)': (
                             shopify_fields['lineItems(first: 100)']),
@@ -481,7 +479,7 @@
             lines.append(Line.get_from_shopify(
                     sale, line_item, quantity, warehouse=warehouse, line=line))
         shopify_shipping_lines = graphql.iterate(
-            QUERY_ORDER_CURSOR % {
+            shop, QUERY_ORDER_CURSOR % {
                 'fields': graphql.selection({
                         'shippingLines(first: 10, after: $cursor)': (
                             shopify_fields['shippingLines(first: 10)']),
@@ -594,96 +592,85 @@
         pool = Pool()
         Payment = pool.get('account.payment')
         self.lock()
-        with self.web_shop.shopify_session():
-            for shipment in self.shipments:
-                fulfillment = shipment.get_shopify(self)
-                if fulfillment:
-                    try:
-                        result = shopify.GraphQL().execute(
-                            MUTATION_FULFILLMENT_CREATE,
-                            {'fulfillment': fulfillment}
-                            )['data']['fulfillmentCreate']
-                        if errors := result.get('userErrors'):
-                            raise GraphQLException({'errors': errors})
-                        fulfillment = result['fulfillment']
-                    except GraphQLException as e:
-                        raise ShopifyError(gettext(
-                                'web_shop_shopify.msg_fulfillment_fail',
-                                sale=self.rec_name,
-                                error="\n".join(
-                                    err['message'] for err in e.errors))
-                            ) from e
-                    shipment.set_shopify_identifier(
-                        self, gid2id(fulfillment['id']))
-                    Transaction().commit()
-                    # Start a new transaction as commit release the lock
-                    self.__class__.__queue__._process_shopify(self)
-                    return
-                elif shipment.state == 'cancelled':
-                    fulfillment_id = shipment.get_shopify_identifier(self)
-                    if fulfillment_id:
-                        fulfillment_id = id2gid('Fulfillment', fulfillment_id)
-                        result = shopify.GraphQL().execute(
-                            MUTATION_FULFILLMENT_CANCEL,
-                            {'id': fulfillment_id}
-                            )['data']['fulfillmentCancel']
-                        if errors := result.get('userErrors'):
-                            raise GraphQLException({'errors': errors})
-
-            # TODO: manage drop shipment
+        shopify_request = self.web_shop.shopify_request
+        for shipment in self.shipments:
+            fulfillment = shipment.get_shopify(self)
+            if fulfillment:
+                try:
+                    fulfillment = shopify_request(
+                        MUTATION_FULFILLMENT_CREATE,
+                        {'fulfillment': fulfillment},
+                        user_errors='fulfillmentCreate.userErrors',
+                        ).data['fulfillmentCreate']['fulfillment']
+                except GraphQLException as e:
+                    raise ShopifyError(gettext(
+                            'web_shop_shopify.msg_fulfillment_fail',
+                            sale=self.rec_name,
+                            error=e.message)) from e
+                shipment.set_shopify_identifier(
+                    self, gid2id(fulfillment['id']))
+                Transaction().commit()
+                # Start a new transaction as commit release the lock
+                self.__class__.__queue__._process_shopify(self)
+                return
+            elif shipment.state == 'cancelled':
+                fulfillment_id = shipment.get_shopify_identifier(self)
+                if fulfillment_id:
+                    fulfillment_id = id2gid('Fulfillment', fulfillment_id)
+                    self.web_shop.shopify_request(
+                        MUTATION_FULFILLMENT_CANCEL,
+                        {'id': fulfillment_id},
+                        user_errors='fulfillmentCancel.userErrors')
 
-            shopify_id = id2gid('Order', self.shopify_identifier)
-            if self.shipment_state == 'sent' or self.state == 'done':
-                # TODO: manage shopping refund
-                refund = self.get_shopify_refund(
-                    shipping=self.shipment_state == 'none')
-                if refund:
-                    try:
-                        result = shopify.GraphQL().execute(
-                            MUTATION_REFUND_CREATE,
-                            {'input': refund})['data']['refundCreate']
-                        if errors := result.get('userErrors'):
-                            raise GraphQLException({'errors': errors})
-                    except GraphQLException as e:
-                        raise ShopifyError(gettext(
-                                'web_shop_shopify.msg_refund_fail',
-                                sale=self.rec_name,
-                                error="\n".join(
-                                    err['message'] for err in e.errors))
-                            ) from e
-                    order = shopify.GraphQL().execute(
-                        QUERY_ORDER % {
-                            'fields': graphql.selection(self.shopify_fields()),
-                            }, {'id': shopify_id})['data']['order']
-                    Payment.get_from_shopify(self, order)
+        # TODO: manage drop shipment
 
-            shopify_id = id2gid('Order', self.shopify_identifier)
-            order = shopify.GraphQL().execute(
-                QUERY_ORDER_CLOSED, {'id': shopify_id})['data']['order']
-            if self.state == 'done':
-                if not order['closed']:
-                    result = shopify.GraphQL().execute(
-                        MUTATION_ORDER_CLOSE, {
-                            'input': {
-                                'id': shopify_id,
-                                },
-                            })['data']['orderClose']
-                    if errors := result.get('userErrors'):
-                        raise GraphQLException({'errors': errors})
-            elif order['closed']:
-                result = shopify.GraphQL().execute(
-                    MUTATION_ORDER_OPEN, {
+        shopify_id = id2gid('Order', self.shopify_identifier)
+        if self.shipment_state == 'sent' or self.state == 'done':
+            # TODO: manage shopping refund
+            refund = self.get_shopify_refund(
+                shipping=self.shipment_state == 'none')
+            if refund:
+                try:
+                    shopify_request(
+                        MUTATION_REFUND_CREATE,
+                        {'input': refund},
+                        user_errors='refundCreate.userErrors')
+                except GraphQLException as e:
+                    raise ShopifyError(gettext(
+                            'web_shop_shopify.msg_refund_fail',
+                            sale=self.rec_name,
+                            error=e.message)) from e
+                order = shopify_request(
+                    QUERY_ORDER % {
+                        'fields': graphql.selection(self.shopify_fields()),
+                        }, {'id': shopify_id}).data['order']
+                Payment.get_from_shopify(self, order)
+
+        shopify_id = id2gid('Order', self.shopify_identifier)
+        order = shopify_request(
+            QUERY_ORDER_CLOSED, {'id': shopify_id}).data['order']
+        if self.state == 'done':
+            if not order['closed']:
+                shopify_request(
+                    MUTATION_ORDER_CLOSE, {
                         'input': {
                             'id': shopify_id,
                             },
-                        })['data']['orderOpen']
-                if errors := result.get('userErrors'):
-                    raise GraphQLException({'errors': errors})
+                        },
+                    user_errors='orderClose.userErrors')
+        elif order['closed']:
+            shopify_request(
+                MUTATION_ORDER_OPEN, {
+                    'input': {
+                        'id': shopify_id,
+                        },
+                    },
+                user_errors='orderOpen.userErrors')
 
     def get_shopify_refund(self, shipping=False):
         order_id = id2gid('Order', self.shopify_identifier)
         shopify_line_items = graphql.iterate(
-            QUERY_ORDER_FULFILLABLE_QUANTITIES,
+            self.web_shop, QUERY_ORDER_FULFILLABLE_QUANTITIES,
             {'id': order_id}, 'order', 'lineItems')
         fulfillable_quantities = {
             gid2id(l['id']): l['fulfillableQuantity']
@@ -693,11 +680,11 @@
         if not refund_line_items:
             return
 
-        order = shopify.GraphQL().execute(
+        order = self.web_shop.shopify_request(
             QUERY_ORDER_SUGGESTED_REFUND, {
                 'id': order_id,
                 'refundShipping': shipping,
-                'refundLineItems': refund_line_items})['data']['order']
+                'refundLineItems': refund_line_items}).data['order']
         currencies = set()
         transactions = []
         for transaction in order['suggestedRefund']['suggestedTransactions']:
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/shopify_retry.py
--- a/modules/web_shop_shopify/shopify_retry.py Wed Mar 25 22:11:59 2026 +0100
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-# This file is part of Tryton.  The COPYRIGHT file at the top level of
-# this repository contains the full copyright notices and license terms.
-import json
-import logging
-import time
-import urllib
-
-from pyactiveresource.connection import ClientError
-from shopify import Limits
-from shopify.base import ShopifyConnection
-from shopify.resources.graphql import GraphQL
-
-try:
-    from shopify.resources.graphql import GraphQLException
-except ImportError:
-    class GraphQLException(Exception):
-        def __init__(self, response):
-            self._response = response
-
-        @property
-        def errors(self):
-            return self._response['errors']
-
-from trytond.protocols.wrappers import HTTPStatus
-from trytond.tools.logging import format_args
-
-logger = logging.getLogger(__name__)
-
-
-def patch():
-    def _open(*args, **kwargs):
-        while True:
-            try:
-                return open_func(*args, **kwargs)
-            except ClientError as e:
-                if e.response.code == HTTPStatus.TOO_MANY_REQUESTS:
-                    retry_after = float(
-                        e.response.headers.get('Retry-After', 2))
-                    logger.debug(
-                        "Shopify connection retry after %ss", retry_after)
-                    time.sleep(retry_after)
-                else:
-                    raise
-            else:
-                try:
-                    if Limits.credit_maxed():
-                        logger.debug("Shopify connection credit maxed")
-                        time.sleep(0.5)
-                except Exception:
-                    pass
-
-    if ShopifyConnection._open != _open:
-        open_func = ShopifyConnection._open
-        ShopifyConnection._open = _open
-
-    def graphql_execute(self, *args, **kwargs):
-        log_message = "GraphQL execute %s"
-        log_args = (
-            format_args(args, kwargs, logger.isEnabledFor(logging.DEBUG)),)
-        while True:
-            try:
-                result = graphql_execute_func(self, *args, **kwargs)
-            except urllib.error.HTTPError as e:
-                if e.code == HTTPStatus.TOO_MANY_REQUESTS:
-                    retry_after = float(e.headers.get('Retry-After', 2))
-                    logger.debug("GraphQL retry after %ss", retry_after)
-                    time.sleep(retry_after)
-                else:
-                    logger.exception(log_message, *log_args)
-                    raise GraphQLException(json.load(e.fp))
-            if isinstance(result, str):
-                result = json.loads(result)
-            if result.get('errors'):
-                for error in result['errors']:
-                    if error.get('extensions', {}).get('code') == 'THROTTLED':
-                        logger.debug("GraphQL throttled")
-                        time.sleep(0.5)
-                        continue
-                logger.exception(log_message, *log_args)
-                raise GraphQLException(result)
-            logger.info(log_message, *log_args)
-            logger.debug("GraphQL Result: %r", result)
-            return result
-
-    if GraphQL.execute != graphql_execute:
-        graphql_execute_func = GraphQL.execute
-        GraphQL.execute = graphql_execute
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/stock.py
--- a/modules/web_shop_shopify/stock.py Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/stock.py Sun Mar 29 20:05:46 2026 +0200
@@ -3,8 +3,6 @@
 
 from collections import defaultdict
 
-import shopify
-
 from trytond.i18n import gettext, lazy_gettext
 from trytond.model import ModelSQL, ModelView, Unique, Workflow, fields
 from trytond.model.exceptions import AccessError
@@ -12,8 +10,7 @@
 
 from . import graphql
 from .common import IdentifierMixin, id2gid
-from .exceptions import ShopifyError
-from .shopify_retry import GraphQLException
+from .exceptions import GraphQLException, ShopifyError
 
 QUERY_FULFILLMENT_ORDERS = '''\
 query FulfillmentOrders($orderId: ID!) {
@@ -78,10 +75,10 @@
                     },
                 })
         order_id = id2gid('Order', sale.shopify_identifier)
-        fulfillment_orders = shopify.GraphQL().execute(
+        fulfillment_orders = sale.web_shop.shopify_request(
             QUERY_FULFILLMENT_ORDERS % {
                 'fields': graphql.selection(fulfillment_order_fields),
-                }, {'orderId': order_id})['data']['order']['fulfillmentOrders']
+                }, {'orderId': order_id}).data['order']['fulfillmentOrders']
         line_items = defaultdict(list)
         for move in self.outgoing_moves:
             if move.sale == sale:
@@ -170,6 +167,7 @@
     @classmethod
     def _shopify_prepared_for_pickup(cls, shipments):
         mutation = MUTATION_FULFILLMENT_ORDER_LINE_ITEMS_PREPARED_FOR_PICKUP
+        output = 'fulfillmentOrderLineItemsPreparedForPickup'
         fullfilments = defaultdict(set)
         for shipment in shipments:
             if shipment.state == 'packed':
@@ -181,13 +179,11 @@
                         id2gid('FulfillmentOrder', r.shopify_identifier)
                         for r in records})
                 try:
-                    result = shopify.GraphQL().execute(
+                    shop.shopify_request(
                         mutation, {
                             'input': list(fullfilment_ids),
-                            }
-                        )['data']['fulfillmentOrderLineItemsPreparedForPickup']
-                    if errors := result.get('userErrors'):
-                        raise GraphQLException({'errors': errors})
+                            },
+                        user_errors=f'{output}.userErrors')
                 except GraphQLException as e:
                     shipments = ", ".join(
                         r.shipment.rec_name for r in records[:5])
@@ -200,8 +196,7 @@
                             '.msg_fulfillment_prepared_for_pickup_fail',
                             shipments=shipments,
                             sales=sales,
-                            error="\n".join(
-                                err['message'] for err in e.errors))) from e
+                            error=e.message)) from e
 
 
 class ShipmentShopifyIdentifier(IdentifierMixin, ModelSQL, ModelView):
diff -r f859e3843468 -r 3e58474b6aed 
modules/web_shop_shopify/tests/scenario_web_shop_shopify.rst
--- a/modules/web_shop_shopify/tests/scenario_web_shop_shopify.rst      Wed Mar 
25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/tests/scenario_web_shop_shopify.rst      Sun Mar 
29 20:05:46 2026 +0200
@@ -13,9 +13,6 @@
     >>> from itertools import cycle
     >>> from unittest.mock import patch
 
-    >>> import shopify
-    >>> from shopify.api_version import ApiVersion
-
     >>> from proteus import Model
     >>> from trytond.modules.account.tests.tools import (
     ...     create_chart, create_fiscalyear, create_tax, get_accounts)
@@ -137,21 +134,15 @@
 
     >>> web_shop = WebShop(name="Web Shop")
     >>> web_shop.type = 'shopify'
-    >>> web_shop.shopify_url = os.getenv('SHOPIFY_URL')
-    >>> web_shop.shopify_password = os.getenv('SHOPIFY_PASSWORD')
-    >>> web_shop.shopify_version = sorted(ApiVersion.versions, reverse=True)[1]
+    >>> web_shop.shopify_shop_name = os.getenv('SHOPIFY_SHOP')
+    >>> web_shop.shopify_access_token = os.getenv('SHOPIFY_ACCESS_TOKEN')
     >>> shop_warehouse = web_shop.shopify_warehouses.new()
     >>> shop_warehouse.warehouse, = Location.find([('type', '=', 'warehouse')])
     >>> shopify_payment_journal = web_shop.shopify_payment_journals.new()
     >>> shopify_payment_journal.journal = payment_journal
     >>> web_shop.save()
 
-    >>> shopify.ShopifyResource.activate_session(shopify.Session(
-    ...         web_shop.shopify_url,
-    ...         web_shop.shopify_version,
-    ...         web_shop.shopify_password))
-
-    >>> location = tools.get_location()
+    >>> location = tools.get_location(web_shop)
 
     >>> shop_warehouse, = web_shop.shopify_warehouses
     >>> shop_warehouse.shopify_id = str(gid2id(location['id']))
@@ -389,7 +380,7 @@
     >>> inventory_item_ids = [i.shopify_identifier
     ...     for inv in inventory_items for i in inv.shopify_identifiers]
     >>> for _ in range(MAX_SLEEP):
-    ...     inventory_levels = tools.get_inventory_levels(location)
+    ...     inventory_levels = tools.get_inventory_levels(web_shop, location)
     ...     if inventory_levels and len(inventory_levels) == 2:
     ...         break
     ...     time.sleep(FETCH_SLEEP)
@@ -448,7 +439,7 @@
     >>> len(product2.shopify_identifiers)
     0
     >>> identifier, = product2.template.shopify_identifiers
-    >>> tools.get_product(identifier.shopify_identifier)['status']
+    >>> tools.get_product(web_shop, identifier.shopify_identifier)['status']
     'ARCHIVED'
     >>> variant1.reload()
     >>> len(variant1.shopify_identifiers)
@@ -470,7 +461,7 @@
     ...     ''.join(random.choice(string.digits) for _ in range(3)))
     >>> customer_address_phone = '+32-495-555-' + (
     ...     ''.join(random.choice(string.digits) for _ in range(3)))
-    >>> customer = tools.create_customer({
+    >>> customer = tools.create_customer(web_shop, {
     ...         'lastName': "Customer",
     ...         'email': (''.join(
     ...                 random.choice(string.ascii_letters) for _ in range(10))
@@ -479,7 +470,7 @@
     ...         'locale': 'en-CA',
     ...         })
 
-    >>> order = tools.create_order({
+    >>> order = tools.create_order(web_shop, {
     ...         'customer': {
     ...             'toAssociate': {
     ...                 'id': customer['id'],
@@ -596,7 +587,7 @@
 Capture full amount::
 
     >>> transaction = tools.capture_order(
-    ...     order['id'], 258.98, order['transactions'][0]['id'])
+    ...     web_shop, order['id'], 258.98, order['transactions'][0]['id'])
 
     >>> with config.set_context(shopify_orders=[gid2id(order['id'])]):
     ...     cron_update_order, = Cron.find([
@@ -633,7 +624,7 @@
     >>> len(sale.invoices)
     0
 
-    >>> order = tools.get_order(order['id'])
+    >>> order = tools.get_order(web_shop, order['id'])
     >>> order['displayFulfillmentStatus']
     'PARTIALLY_FULFILLED'
     >>> len(order['fulfillments'])
@@ -651,7 +642,7 @@
     >>> shipment.state
     'shipped'
 
-    >>> order = tools.get_order(order['id'])
+    >>> order = tools.get_order(web_shop, order['id'])
     >>> order['displayFulfillmentStatus']
     'FULFILLED'
     >>> len(order['fulfillments'])
@@ -661,7 +652,7 @@
     >>> shipment.state
     'cancelled'
 
-    >>> order = tools.get_order(order['id'])
+    >>> order = tools.get_order(web_shop, order['id'])
     >>> order['displayFulfillmentStatus']
     'PARTIALLY_FULFILLED'
     >>> len(order['fulfillments'])
@@ -685,7 +676,7 @@
     >>> len(sale.invoices)
     0
 
-    >>> order = tools.get_order(order['id'])
+    >>> order = tools.get_order(web_shop, order['id'])
     >>> order['displayFulfillmentStatus']
     'PARTIALLY_FULFILLED'
     >>> len(order['fulfillments'])
@@ -700,7 +691,7 @@
     ...     shipment_exception.form.ignore_moves.find())
     >>> shipment_exception.execute('handle')
 
-    >>> order = tools.get_order(order['id'])
+    >>> order = tools.get_order(web_shop, order['id'])
     >>> order['displayFulfillmentStatus']
     'FULFILLED'
     >>> len(order['fulfillments'])
@@ -731,25 +722,25 @@
     >>> sale.reload()
     >>> sale.state
     'done'
-    >>> order = tools.get_order(order['id'])
+    >>> order = tools.get_order(web_shop, order['id'])
     >>> bool(order['closed'])
     True
 
 Clean up::
 
-    >>> tools.delete_order(order['id'])
+    >>> tools.delete_order(web_shop, order['id'])
     >>> for product in ShopifyIdentifier.find(
     ...         [('record', 'like', 'product.template,%')]):
-    ...     tools.delete_product(id2gid('Product', product.shopify_identifier))
+    ...     tools.delete_product(
+    ...         web_shop, id2gid('Product', product.shopify_identifier))
     >>> for category in ShopifyIdentifier.find(
     ...         [('record', 'like', 'product.category,%')]):
-    ...     tools.delete_collection(id2gid('Collection', 
category.shopify_identifier))
+    ...     tools.delete_collection(
+    ...         web_shop, id2gid('Collection', category.shopify_identifier))
     >>> for _ in range(MAX_SLEEP):
     ...     try:
-    ...         tools.delete_customer(customer['id'])
+    ...         tools.delete_customer(web_shop, customer['id'])
     ...     except Exception:
     ...         time.sleep(FETCH_SLEEP)
     ...     else:
     ...         break
-
-    >>> shopify.ShopifyResource.clear_session()
diff -r f859e3843468 -r 3e58474b6aed 
modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst
--- a/modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst  
Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst  
Sun Mar 29 20:05:46 2026 +0200
@@ -10,9 +10,6 @@
     >>> import time
     >>> from decimal import Decimal
 
-    >>> import shopify
-    >>> from shopify.api_version import ApiVersion
-
     >>> from proteus import Model
     >>> from trytond.modules.account.tests.tools import (
     ...     create_chart, create_fiscalyear, get_accounts)
@@ -81,21 +78,15 @@
 
     >>> web_shop = WebShop(name="Web Shop")
     >>> web_shop.type = 'shopify'
-    >>> web_shop.shopify_url = os.getenv('SHOPIFY_URL')
-    >>> web_shop.shopify_password = os.getenv('SHOPIFY_PASSWORD')
-    >>> web_shop.shopify_version = sorted(ApiVersion.versions, reverse=True)[1]
+    >>> web_shop.shopify_shop_name = os.getenv('SHOPIFY_SHOP')
+    >>> web_shop.shopify_access_token = os.getenv('SHOPIFY_ACCESS_TOKEN')
     >>> shop_warehouse = web_shop.shopify_warehouses.new()
     >>> shop_warehouse.warehouse, = Location.find([('type', '=', 'warehouse')])
     >>> shopify_payment_journal = web_shop.shopify_payment_journals.new()
     >>> shopify_payment_journal.journal = payment_journal
     >>> web_shop.save()
 
-    >>> shopify.ShopifyResource.activate_session(shopify.Session(
-    ...         web_shop.shopify_url,
-    ...         web_shop.shopify_version,
-    ...         web_shop.shopify_password))
-
-    >>> location = tools.get_location()
+    >>> location = tools.get_location(web_shop)
 
     >>> shop_warehouse, = web_shop.shopify_warehouses
     >>> shop_warehouse.shopify_id = str(gid2id(location['id']))
@@ -163,14 +154,14 @@
 
 Create an order on Shopify::
 
-    >>> customer = tools.create_customer({
+    >>> customer = tools.create_customer(web_shop, {
     ...         'lastName': "Customer",
     ...         'email': (''.join(
     ...                 random.choice(string.ascii_letters) for _ in range(10))
     ...             + '@example.com')
     ...         })
 
-    >>> order = tools.create_order({
+    >>> order = tools.create_order(web_shop, {
     ...         'customerId': customer['id'],
     ...         'shippingAddress': {
     ...                 'lastName': "Customer",
@@ -203,7 +194,7 @@
     'AUTHORIZED'
 
     >>> transaction = tools.capture_order(
-    ...     order['id'], 300, order['transactions'][0]['id'])
+    ...     web_shop, order['id'], 300, order['transactions'][0]['id'])
 
 Run fetch order::
 
@@ -239,7 +230,7 @@
     >>> shipment.state
     'done'
 
-    >>> order = tools.get_order(order['id'])
+    >>> order = tools.get_order(web_shop, order['id'])
     >>> order['displayFulfillmentStatus']
     'PARTIALLY_FULFILLED'
     >>> fulfillment, = order['fulfillments']
@@ -263,7 +254,7 @@
     >>> shipment.state
     'done'
 
-    >>> order = tools.get_order(order['id'])
+    >>> order = tools.get_order(web_shop, order['id'])
     >>> order['displayFulfillmentStatus']
     'PARTIALLY_FULFILLED'
     >>> fulfillment, = order['fulfillments']
@@ -282,25 +273,25 @@
     >>> shipment.state
     'done'
 
-    >>> order = tools.get_order(order['id'])
+    >>> order = tools.get_order(web_shop, order['id'])
     >>> order['displayFulfillmentStatus']
     'FULFILLED'
 
 Clean up::
 
-    >>> tools.delete_order(order['id'])
+    >>> tools.delete_order(web_shop, order['id'])
     >>> for product in ShopifyIdentifier.find(
     ...         [('record', 'like', 'product.template,%')]):
-    ...     tools.delete_product(id2gid('Product', product.shopify_identifier))
+    ...     tools.delete_product(
+    ...         web_shop, id2gid('Product', product.shopify_identifier))
     >>> for category in ShopifyIdentifier.find(
     ...         [('record', 'like', 'product.category,%')]):
-    ...     tools.delete_collection(id2gid('Collection', 
category.shopify_identifier))
+    ...     tools.delete_collection(
+    ...         web_shop, id2gid('Collection', category.shopify_identifier))
     >>> for _ in range(MAX_SLEEP):
     ...     try:
-    ...         tools.delete_customer(customer['id'])
+    ...         tools.delete_customer(web_shop, customer['id'])
     ...     except Exception:
     ...         time.sleep(FETCH_SLEEP)
     ...     else:
     ...         break
-
-    >>> shopify.ShopifyResource.clear_session()
diff -r f859e3843468 -r 3e58474b6aed 
modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst
--- 
a/modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst   
    Wed Mar 25 22:11:59 2026 +0100
+++ 
b/modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst   
    Sun Mar 29 20:05:46 2026 +0200
@@ -10,9 +10,6 @@
     >>> import time
     >>> from decimal import Decimal
 
-    >>> import shopify
-    >>> from shopify.api_version import ApiVersion
-
     >>> from proteus import Model
     >>> from trytond.modules.account.tests.tools import (
     ...     create_chart, create_fiscalyear, get_accounts)
@@ -81,21 +78,15 @@
 
     >>> web_shop = WebShop(name="Web Shop")
     >>> web_shop.type = 'shopify'
-    >>> web_shop.shopify_url = os.getenv('SHOPIFY_URL')
-    >>> web_shop.shopify_password = os.getenv('SHOPIFY_PASSWORD')
-    >>> web_shop.shopify_version = sorted(ApiVersion.versions, reverse=True)[1]
+    >>> web_shop.shopify_shop_name = os.getenv('SHOPIFY_SHOP')
+    >>> web_shop.shopify_access_token = os.getenv('SHOPIFY_ACCESS_TOKEN')
     >>> shop_warehouse = web_shop.shopify_warehouses.new()
     >>> shop_warehouse.warehouse, = Location.find([('type', '=', 'warehouse')])
     >>> shopify_payment_journal = web_shop.shopify_payment_journals.new()
     >>> shopify_payment_journal.journal = payment_journal
     >>> web_shop.save()
 
-    >>> shopify.ShopifyResource.activate_session(shopify.Session(
-    ...         web_shop.shopify_url,
-    ...         web_shop.shopify_version,
-    ...         web_shop.shopify_password))
-
-    >>> location = tools.get_location()
+    >>> location = tools.get_location(web_shop)
 
     >>> shop_warehouse, = web_shop.shopify_warehouses
     >>> shop_warehouse.shopify_id = str(gid2id(location['id']))
@@ -151,14 +142,14 @@
 
 Create an order on Shopify::
 
-    >>> customer = tools.create_customer({
+    >>> customer = tools.create_customer(web_shop, {
     ...         'lastName': "Customer",
     ...         'email': (''.join(
     ...                 random.choice(string.ascii_letters) for _ in range(10))
     ...             + '@example.com')
     ...         })
 
-    >>> order = tools.create_order({
+    >>> order = tools.create_order(web_shop, {
     ...         'customerId': customer['id'],
     ...         'shippingAddress': {
     ...                 'lastName': "Customer",
@@ -231,19 +222,19 @@
 
 Clean up::
 
-    >>> tools.delete_order(order['id'])
+    >>> tools.delete_order(web_shop, order['id'])
     >>> for product in ShopifyIdentifier.find(
     ...         [('record', 'like', 'product.template,%')]):
-    ...     tools.delete_product(id2gid('Product', product.shopify_identifier))
+    ...     tools.delete_product(
+    ...         web_shop, id2gid('Product', product.shopify_identifier))
     >>> for category in ShopifyIdentifier.find(
     ...         [('record', 'like', 'product.category,%')]):
-    ...     tools.delete_collection(id2gid('Collection', 
category.shopify_identifier))
+    ...     tools.delete_collection(
+    ...         web_shop, id2gid('Collection', category.shopify_identifier))
     >>> for _ in range(MAX_SLEEP):
     ...     try:
-    ...         tools.delete_customer(customer['id'])
+    ...         tools.delete_customer(web_shop, customer['id'])
     ...     except Exception:
     ...         time.sleep(FETCH_SLEEP)
     ...     else:
     ...         break
-
-    >>> shopify.ShopifyResource.clear_session()
diff -r f859e3843468 -r 3e58474b6aed 
modules/web_shop_shopify/tests/test_scenario.py
--- a/modules/web_shop_shopify/tests/test_scenario.py   Wed Mar 25 22:11:59 
2026 +0100
+++ b/modules/web_shop_shopify/tests/test_scenario.py   Sun Mar 29 20:05:46 
2026 +0200
@@ -8,8 +8,8 @@
 
 def load_tests(*args, **kwargs):
     if (not TEST_NETWORK
-            or not (os.getenv('SHOPIFY_PASSWORD')
-                and os.getenv('SHOPIFY_URL'))):
+            or not (os.getenv('SHOPIFY_ACCESS_TOKEN')
+                and os.getenv('SHOPIFY_SHOP'))):
         kwargs.setdefault('skips', set()).update({
                 'scenario_web_shop_shopify.rst',
                 'scenario_web_shop_shopify_secondary_unit.rst',
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/tests/tools.py
--- a/modules/web_shop_shopify/tests/tools.py   Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/tests/tools.py   Sun Mar 29 20:05:46 2026 +0200
@@ -1,24 +1,53 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
 
-import shopify
+from shopify_app import admin_graphql_request
 
+from trytond.modules.web_shop_shopify import SHOPIFY_VERSION
 from trytond.modules.web_shop_shopify.common import id2gid
-from trytond.modules.web_shop_shopify.shopify_retry import GraphQLException
+from trytond.modules.web_shop_shopify.exceptions import GraphQLException
 
 
-def get_location():
-    return shopify.GraphQL().execute('''{
+def shopify_request(shop, query, variables=None, user_errors=None):
+    result = admin_graphql_request(
+        query=query,
+        variables=variables,
+        shop=shop.shopify_shop_name,
+        access_token=shop.shopify_access_token,
+        api_version=SHOPIFY_VERSION)
+    if not result.ok:
+        msg = []
+        if result.log and result.log.detail:
+            msg.append(result.log.detail)
+        for log in result.http_logs:
+            if log.detail:
+                msg.append(log.detail)
+        raise GraphQLException("\n".join(msg))
+    if user_errors:
+        names = user_errors.split('.')
+        errors = result.data
+        for name in names:
+            errors = getattr(errors, name, None)
+            if errors is None:
+                break
+        if errors:
+            raise GraphQLException("\n".join(
+                    f"{e['field']}: {e['message']}" for e in errors))
+    return result
+
+
+def get_location(shop):
+    return shopify_request(shop, '''{
         locations(first:1) {
             nodes {
                 id
             }
         }
-    }''')['data']['locations']['nodes'][0]
+    }''').data['locations']['nodes'][0]
 
 
-def get_inventory_levels(location):
-    return shopify.GraphQL().execute('''query InventoryLevels($id: ID!) {
+def get_inventory_levels(shop, location):
+    return shopify_request(shop, '''query InventoryLevels($id: ID!) {
         location(id: $id) {
             inventoryLevels(first: 250) {
                 nodes {
@@ -34,21 +63,21 @@
         }
     }''', {
             'id': location['id'],
-            })['data']['location']['inventoryLevels']['nodes']
+            }).data['location']['inventoryLevels']['nodes']
 
 
-def get_product(id):
-    return shopify.GraphQL().execute('''query Product($id: ID!) {
+def get_product(shop, id):
+    return shopify_request(shop, '''query Product($id: ID!) {
         product(id: $id) {
             status
         }
     }''', {
             'id': id2gid('Product', id),
-            })['data']['product']
+            }).data['product']
 
 
-def delete_product(id):
-    result = shopify.GraphQL().execute('''mutation productDelete($id: ID!) {
+def delete_product(shop, id):
+    shopify_request(shop, '''mutation productDelete($id: ID!) {
         productDelete(input: {id: $id}) {
             userErrors {
                 field
@@ -57,13 +86,12 @@
         }
     }''', {
             'id': id,
-            })['data']['productDelete']
-    if errors := result.get('userErrors'):
-        raise GraphQLException({'errors': errors})
+            },
+        user_errors='productDelete.userErrors')
 
 
-def delete_collection(id):
-    result = shopify.GraphQL().execute('''mutation collectionDelete($id: ID!) {
+def delete_collection(shop, id):
+    shopify_request(shop, '''mutation collectionDelete($id: ID!) {
         collectionDelete(input: {id: $id}) {
             userErrors {
                 field
@@ -72,13 +100,12 @@
         }
     }''', {
             'id': id,
-            })['data']['collectionDelete']
-    if errors := result.get('userErrors'):
-        raise GraphQLException({'errors': errors})
+            },
+        user_errors='collectionDelete.userErrors')
 
 
-def create_customer(customer):
-    result = shopify.GraphQL().execute('''mutation customerCreate(
+def create_customer(shop, customer):
+    return shopify_request(shop, '''mutation customerCreate(
     $input: CustomerInput!) {
         customerCreate(input: $input) {
             customer {
@@ -91,14 +118,13 @@
         }
     }''', {
             'input': customer,
-            })['data']['customerCreate']
-    if errors := result.get('userErrors'):
-        raise GraphQLException({'errors': errors})
-    return result['customer']
+            },
+        user_errors='customerCreate.userErrors',
+        ).data['customerCreate']['customer']
 
 
-def delete_customer(id):
-    result = shopify.GraphQL().execute('''mutation customerDelete($id: ID!) {
+def delete_customer(shop, id):
+    shopify_request(shop, '''mutation customerDelete($id: ID!) {
         customerDelete(input: {id: $id}) {
             userErrors {
                 field
@@ -107,13 +133,12 @@
         }
     }''', {
             'id': id,
-            })['data']['customerDelete']
-    if errors := result.get('userErrors'):
-        raise GraphQLException({'errors': errors})
+            },
+        user_errors='customerDelete.userErrors')
 
 
-def create_order(order):
-    result = shopify.GraphQL().execute('''mutation orderCreate(
+def create_order(shop, order):
+    return shopify_request(shop, '''mutation orderCreate(
     $order: OrderCreateOrderInput!) {
         orderCreate(order: $order) {
             order {
@@ -141,14 +166,13 @@
         }
     }''', {
             'order': order,
-            })['data']['orderCreate']
-    if errors := result.get('userErrors'):
-        raise GraphQLException({'errors': errors})
-    return result['order']
+            },
+        user_errors='orderCreate.userErrors',
+        ).data['orderCreate']['order']
 
 
-def get_order(id):
-    return shopify.GraphQL().execute('''query Order($id: ID!) {
+def get_order(shop, id):
+    return shopify_request(shop, '''query Order($id: ID!) {
         order(id: $id) {
             id
             totalPriceSet {
@@ -174,11 +198,11 @@
         }
     }''', {
             'id': id,
-            })['data']['order']
+            }).data['order']
 
 
-def capture_order(id, amount, parent_transaction_id):
-    result = shopify.GraphQL().execute('''mutation orderCapture(
+def capture_order(shop, id, amount, parent_transaction_id):
+    return shopify_request(shop, '''mutation orderCapture(
     $input: OrderCaptureInput!) {
         orderCapture(input: $input) {
             transaction {
@@ -195,14 +219,13 @@
                 'id': id,
                 'parentTransactionId': parent_transaction_id,
                 },
-            })['data']['orderCapture']
-    if errors := result.get('userErrors'):
-        raise GraphQLException({'errors': errors})
-    return result['transaction']
+            },
+        user_errors='orderCapture.userErrors',
+        ).data['orderCapture']['transaction']
 
 
-def delete_order(id):
-    result = shopify.GraphQL().execute('''mutation orderDelete($orderId: ID!) {
+def delete_order(shop, id):
+    shopify_request(shop, '''mutation orderDelete($orderId: ID!) {
         orderDelete(orderId: $orderId) {
             userErrors {
                 field
@@ -211,6 +234,5 @@
         }
     }''', {
             'orderId': id,
-            })['data']['orderDelete']
-    if errors := result.get('userErrors'):
-        raise GraphQLException({'errors': errors})
+            },
+        user_errors='orderDelete.userErrors')
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/view/shop_form.xml
--- a/modules/web_shop_shopify/view/shop_form.xml       Wed Mar 25 22:11:59 
2026 +0100
+++ b/modules/web_shop_shopify/view/shop_form.xml       Sun Mar 29 20:05:46 
2026 +0200
@@ -8,22 +8,21 @@
         </page>
     </xpath>
     <xpath expr="//page[@id='products']" position="after">
-        <page string="Shopify" id="shopify" col="6">
-            <label name="shopify_url"/>
-            <field name="shopify_url"/>
-            <label name="shopify_password"/>
-            <field name="shopify_password" widget="password"/>
-            <label name="shopify_version"/>
-            <field name="shopify_version"/>
+        <page string="Shopify" id="shopify">
+            <label name="shopify_shop_name"/>
+            <field name="shopify_shop_name"/>
+            <label name="shopify_access_token"/>
+            <field name="shopify_access_token" widget="password"/>
 
             <label name="shopify_webhook_shared_secret"/>
             <field name="shopify_webhook_shared_secret"/>
             <label name="shopify_webhook_endpoint_order"/>
             <field name="shopify_webhook_endpoint_order"/>
+
             <label name="shopify_fulfillment_notify_customer"/>
             <field name="shopify_fulfillment_notify_customer"/>
 
-            <field name="shopify_payment_journals" colspan="6"/>
+            <field name="shopify_payment_journals" colspan="4"/>
         </page>
     </xpath>
 </data>
diff -r f859e3843468 -r 3e58474b6aed modules/web_shop_shopify/web.py
--- a/modules/web_shop_shopify/web.py   Wed Mar 25 22:11:59 2026 +0100
+++ b/modules/web_shop_shopify/web.py   Sun Mar 29 20:05:46 2026 +0200
@@ -1,14 +1,15 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
 import datetime as dt
+import logging
 import urllib.parse
 from collections import defaultdict
 from decimal import Decimal
 from itertools import groupby
 from operator import attrgetter
 
-import shopify
-from shopify.api_version import ApiVersion
+from shopify_app import admin_graphql_request
+from sql.functions import Position, Substring
 
 import trytond.config as config
 from trytond.cache import Cache
@@ -18,14 +19,17 @@
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Eval
 from trytond.tools import grouped_slice
+from trytond.tools.logging import format_args
 from trytond.transaction import Transaction
 from trytond.url import http_host
 
-from . import graphql
+from . import SHOPIFY_VERSION, graphql
 from .common import IdentifierMixin, IdentifiersMixin, gid2id, id2gid
-from .exceptions import ShopifyCredentialWarning, ShopifyError
+from .exceptions import (
+    GraphQLException, ShopifyCredentialWarning, ShopifyError)
 from .product import QUERY_PRODUCT
-from .shopify_retry import GraphQLException
+
+logger = logging.getLogger(__name__)
 
 EDIT_ORDER_DELAY = dt.timedelta(days=60 + 1)
 
@@ -166,10 +170,12 @@
         'invisible': Eval('type') != 'shopify',
         }
 
-    shopify_url = fields.Char("Shop URL", states=_states)
-    shopify_version = fields.Selection(
-        'get_shopify_versions', "Version", states=_states)
-    shopify_password = fields.Char("Access Token", states=_states, strip=False)
+    shopify_shop_name = fields.Char("Shop Name", states=_states)
+    shopify_url = fields.Function(
+        fields.Char("Shop URL"),
+        'on_change_with_shopify_url')
+    shopify_access_token = fields.Char(
+        "Access Token", states=_states, strip=False)
     shopify_webhook_shared_secret = fields.Char(
         "Webhook Shared Secret", strip=False,
         states={
@@ -205,9 +211,31 @@
                 field.states['invisible'] = invisible
 
     @classmethod
-    def get_shopify_versions(cls):
-        return [(None, "")] + sorted(
-            ((v, v) for v in ApiVersion.versions), reverse=True)
+    def __register__(cls, module):
+        table_h = cls.__table_handler__(module)
+        table = cls.__table__()
+        cursor = Transaction().connection.cursor()
+
+        super().__register__(module)
+
+        # Migration from 7.8: replace url by shop_name
+        if table_h.column_exist('shopify_url'):
+            cursor.execute(*table.update(
+                    [table.shopify_shop_name],
+                    [Substring(
+                            table.shopify_url,
+                            Position('//', table.shopify_url) + 2,
+                            Position('.', table.shopify_url)
+                            - Position('//', table.shopify_url) - 2)]))
+            table_h.drop_column('shopify_url')
+
+        # Migration from 7.8: rename password into access_token
+        table_h.column_rename('shopify_password', 'shopify_access_token')
+
+    @fields.depends('shopify_shop_name')
+    def on_change_with_shopify_url(self, name=None):
+        if self.shopify_shop_name:
+            return f"https://{self.shopify_shop_name}.myshopify.com/";
 
     @fields.depends('name')
     def on_change_with_shopify_webhook_endpoint_order(self, name=None):
@@ -273,20 +301,56 @@
         if self.type == 'shopify':
             self._shopify_update_order(self, sales)
 
-    def shopify_session(self):
-        return shopify.Session.temp(
-            self.shopify_url, self.shopify_version, self.shopify_password)
+    def shopify_request(
+            self, query, variables=None, headers=None, max_retries=2,
+            user_errors=None):
+        result = admin_graphql_request(
+            query=query,
+            shop=self.shopify_shop_name,
+            access_token=self.shopify_access_token,
+            api_version=SHOPIFY_VERSION,
+            variables=variables,
+            headers=headers,
+            max_retries=max_retries)
+        if not result.ok:
+            logger.exception(
+                "shopify request %s", format_args(
+                    query, {
+                        'variables': variables,
+                        'headers': headers,
+                        'max_retries': max_retries,
+                        }, logger.isEnabledFor(logging.DEBUG)))
+            msg = []
+            if result.log and result.log.detail:
+                msg.append(result.log.detail)
+            for log in result.http_logs:
+                if log.detail:
+                    msg.append(log.detail)
+            raise GraphQLException("\n".join(msg))
+        if user_errors:
+            names = user_errors.split('.')
+            errors = result.data
+            for name in names:
+                errors = getattr(errors, name, None)
+                if errors is None:
+                    break
+            if errors:
+                raise GraphQLException("\n".join(
+                        f"{e['field']}: {e['message']}" for e in errors))
+        return result
 
     def shopify_shop(self, fields):
-        return shopify.GraphQL().execute(QUERY_SHOP % {
+        result = self.shopify_request(QUERY_SHOP % {
                 'fields': graphql.selection(fields),
-                })['data']['shop']
+                })
+        return result.data['shop']
 
     def shopify_shop_locales(self, fields):
-        return shopify.GraphQL().execute(
+        result = self.shopify_request(
             QUERY_SHOP_LOCALES % {
                 'fields': graphql.selection(fields),
-                })['data']['shopLocales']
+                })
+        return result.data['shopLocales']
 
     def get_payment_journal(self, currency_code, pattern):
         for payment_journal in self.shopify_payment_journals:
@@ -321,75 +385,74 @@
                 'primary': None,
                 })
         for shop in shops:
-            with shop.shopify_session():
-                shopify_shop = shop.shopify_shop(shop_fields)
-                shop_language = (
-                    shop.language.code if shop.language
-                    else transaction.language)
-                categories = shop.get_categories()
-                products, prices, taxes = shop.get_products(
-                    key=lambda p: p.template.id)
-                sale_prices, sale_taxes = prices, taxes
+            shopify_shop = shop.shopify_shop(shop_fields)
+            shop_language = (
+                shop.language.code if shop.language
+                else transaction.language)
+            categories = shop.get_categories()
+            products, prices, taxes = shop.get_products(
+                key=lambda p: p.template.id)
+            sale_prices, sale_taxes = prices, taxes
 
-                context = shop.get_context()
-                with Transaction().set_context(_non_sale_price=True):
-                    sale_context = shop.get_context()
-                    if context != sale_context:
-                        _, prices, taxes = shop.get_products()
+            context = shop.get_context()
+            with Transaction().set_context(_non_sale_price=True):
+                sale_context = shop.get_context()
+                if context != sale_context:
+                    _, prices, taxes = shop.get_products()
 
-                if shopify_shop['currencyCode'] != shop.currency.code:
-                    raise ShopifyError(gettext(
-                            'web_shop_shopify.msg_shop_currency_different',
-                            shop=shop.rec_name,
-                            shop_currency=shop.currency.code,
-                            shopify_currency=shopify_shop['currencyCode']))
-                shop_locales = shop.shopify_shop_locales(shop_locales_fields)
-                primary_locale = next(
-                    filter(lambda l: l['primary'], shop_locales))
-                if primary_locale['locale'] != shop_language:
-                    raise ShopifyError(gettext(
-                            'web_shop_shopify.msg_shop_locale_different',
-                            shop=shop.rec_name,
-                            shop_language=shop_language,
-                            shopify_primary_locale=primary_locale['locale'],
-                            ))
+            if shopify_shop['currencyCode'] != shop.currency.code:
+                raise ShopifyError(gettext(
+                        'web_shop_shopify.msg_shop_currency_different',
+                        shop=shop.rec_name,
+                        shop_currency=shop.currency.code,
+                        shopify_currency=shopify_shop['currencyCode']))
+            shop_locales = shop.shopify_shop_locales(shop_locales_fields)
+            primary_locale = next(
+                filter(lambda l: l['primary'], shop_locales))
+            if primary_locale['locale'] != shop_language:
+                raise ShopifyError(gettext(
+                        'web_shop_shopify.msg_shop_locale_different',
+                        shop=shop.rec_name,
+                        shop_language=shop_language,
+                        shopify_primary_locale=primary_locale['locale'],
+                        ))
 
-                for category in categories:
-                    shop._shopify_update_collection(category)
+            for category in categories:
+                shop._shopify_update_collection(category)
 
-                categories = set(categories)
-                inventory_items = dict(
-                    zip(products, InventoryItem.browse(products)))
-                for template, t_products in groupby(
-                        products, key=lambda p: p.template):
-                    t_products = sorted(
-                        t_products, key=lambda p: p.position or 0)
-                    p_inventory_items = [
-                        inventory_items[p] for p in t_products]
-                    p_sale_prices = [sale_prices[p.id] for p in t_products]
-                    p_sale_taxes = [sale_taxes[p.id] for p in t_products]
-                    p_prices = [prices[p.id] for p in t_products]
-                    p_taxes = [taxes[p.id] for p in t_products]
-                    if shop._shopify_product_is_to_update(
-                            template, t_products, p_sale_prices, p_sale_taxes,
-                            p_prices, p_taxes):
-                        shop._shopify_update_product(
-                            shopify_shop, categories, template, t_products,
-                            p_inventory_items, p_sale_prices, p_sale_taxes,
-                            p_prices, p_taxes)
-                        Transaction().commit()
+            categories = set(categories)
+            inventory_items = dict(
+                zip(products, InventoryItem.browse(products)))
+            for template, t_products in groupby(
+                    products, key=lambda p: p.template):
+                t_products = sorted(
+                    t_products, key=lambda p: p.position or 0)
+                p_inventory_items = [
+                    inventory_items[p] for p in t_products]
+                p_sale_prices = [sale_prices[p.id] for p in t_products]
+                p_sale_taxes = [sale_taxes[p.id] for p in t_products]
+                p_prices = [prices[p.id] for p in t_products]
+                p_taxes = [taxes[p.id] for p in t_products]
+                if shop._shopify_product_is_to_update(
+                        template, t_products, p_sale_prices, p_sale_taxes,
+                        p_prices, p_taxes):
+                    shop._shopify_update_product(
+                        shopify_shop, categories, template, t_products,
+                        p_inventory_items, p_sale_prices, p_sale_taxes,
+                        p_prices, p_taxes)
+                    Transaction().commit()
 
-                for category in shop.categories_removed:
-                    shop._shopify_remove_collection(category)
-                shop.categories_removed = []
+            for category in shop.categories_removed:
+                shop._shopify_remove_collection(category)
+            shop.categories_removed = []
 
-                products = set(products)
-                for product in shop.products_removed:
-                    template = product.template
-                    if set(template.products).isdisjoint(products):
-                        shop._shopify_remove_product(template)
-                    product.set_shopify_identifier(shop)
-                shop.products_removed = []
+            products = set(products)
+            for product in shop.products_removed:
+                template = product.template
+                if set(template.products).isdisjoint(products):
+                    shop._shopify_remove_product(template)
+                product.set_shopify_identifier(shop)
+            shop.products_removed = []
         cls.save(shops)
 
     def _shopify_update_collection(self, category, collection_fields=None):
@@ -406,21 +469,19 @@
             MUTATION = MUTATION_COLLECTION_CREATE
             output = 'collectionCreate'
         try:
-            result = shopify.GraphQL().execute(
+            result = self.shopify_request(
                 MUTATION % {
                     'fields': graphql.selection(collection_fields),
                     }, {
                     'input': collection,
-                    })['data'][output]
-            if errors := result.get('userErrors'):
-                raise GraphQLException({'errors': errors})
-            collection = result['collection']
+                    },
+                user_errors=f'{output}.userErrors')
+            collection = result.data[output]['collection']
         except GraphQLException as e:
             raise ShopifyError(gettext(
                     'web_shop_shopify.msg_custom_collection_fail',
                     category=category.rec_name,
-                    error="\n".join(
-                        err['message'] for err in e.errors))) from e
+                    error=e.message)) from e
         identifier = category.set_shopify_identifier(
             self, gid2id(collection['id']))
         if identifier.to_update:
@@ -434,14 +495,13 @@
         if shopify_id:
             shopify_id = id2gid('Collection', shopify_id)
             try:
-                result = shopify.GraphQL().execute(
+                self.shopify_request(
                     MUTATION_COLLECTION_DELETE, {
                         'input': {
                             'id': shopify_id,
                             }
-                        })['data']['collectionDelete']
-                if errors := result.get('userErrors'):
-                    raise GraphQLException({'errors': errors})
+                        },
+                    user_errors='collectionDelete.userErrors')
             except GraphQLException:
                 pass
             category.set_shopify_identifier(self)
@@ -522,13 +582,12 @@
             'input': shopify_product,
             }
         try:
-            result = shopify.GraphQL().execute(
+            result = self.shopify_request(
                 MUTATION_PRODUCT_SET % {
                     'fields': graphql.selection(product_fields),
-                    }, data)['data']['productSet']
-            if errors := result.get('userErrors'):
-                raise GraphQLException({'errors': errors})
-            shopify_product = result['product']
+                    }, data,
+                user_errors='productSet.userErrors')
+            shopify_product = result.data['productSet']['product']
 
             identifiers = []
             identifier = template.set_shopify_identifier(
@@ -538,7 +597,7 @@
                 identifiers.append(identifier)
 
             shopify_variants = graphql.iterate(
-                QUERY_PRODUCT_CURSOR % {
+                self, QUERY_PRODUCT_CURSOR % {
                     'fields': graphql.selection({
                             'variants(first: 250, after: $cursor)': (
                                 product_fields['variants(first: 250)']),
@@ -579,34 +638,31 @@
             raise ShopifyError(gettext(
                     'web_shop_shopify.msg_product_fail',
                     template=template.rec_name,
-                    error="\n".join(
-                        err['message'] for err in e.errors))) from e
+                    error=e.message)) from e
 
     def _shopify_remove_product(self, template):
         shopify_id = template.get_shopify_identifier(self)
         if shopify_id:
             shopify_id = id2gid('Product', shopify_id)
-            product = shopify.GraphQL().execute(
+            product = self.shopify_request(
                 QUERY_PRODUCT % {
                     'fields': graphql.selection({
                             'id': None,
                             }),
-                    }, {'id': shopify_id})['data']['product']
+                    }, {'id': shopify_id}).data['product']
             if product:
                 try:
-                    result = shopify.GraphQL().execute(
+                    self.shopify_request(
                         MUTATION_PRODUCT_CHANGE_STATUS, {
                             'productId': shopify_id,
                             'status': 'ARCHIVED',
-                            })['data']['productChangeStatus']
-                    if errors := result.get('userErrors'):
-                        raise GraphQLException({'errors': errors})
+                            },
+                        user_errors='productChangeStatus.userErrors')
                 except GraphQLException as e:
                     raise ShopifyError(gettext(
                             'web_shop_shopify.msg_product_fail',
                             template=template.rec_name,
-                            error="\n".join(
-                                err['message'] for err in e.errors))) from e
+                            error=e.message)) from e
 
     @classmethod
     def shopify_update_inventory(cls, shops=None):
@@ -627,8 +683,7 @@
                         **shop_warehouse.get_shopify_inventory_context()):
                     products = Product.browse([
                             p for p in shop.products if p.shopify_uom])
-                    with shop.shopify_session():
-                        shop._shopify_update_inventory(products, location_id)
+                    shop._shopify_update_inventory(products, location_id)
 
     def _shopify_update_inventory(self, products, location_id):
         pool = Pool()
@@ -645,24 +700,21 @@
         def set_quantities():
             try:
                 for quantity in quantities:
-                    result = shopify.GraphQL().execute(
+                    self.shopify_request(
                         MUTATION_INVENTORY_ACTIVATE, {
                             'inventoryItemId': quantity['inventoryItemId'],
                             'locationId': quantity['locationId'],
-                            })['data']['inventoryActivate']
-                    if errors := result.get('userErrors'):
-                        raise GraphQLException({'errors': errors})
-                result = shopify.GraphQL().execute(
+                            },
+                        user_errors='inventoryActivate.userErrors')
+                self.shopify_request(
                     MUTATION_INVENTORY_SET_QUANTITIES, {
                         'input': input,
-                        })['data']['inventorySetQuantities']
-                if errors := result.get('userErrors'):
-                    raise GraphQLException({'errors': errors})
+                        },
+                    user_errors='inventorySetQuantities.userErrors')
             except GraphQLException as e:
                 raise ShopifyError(gettext(
                         'web_shop_shopify.msg_inventory_set_fail',
-                        error="\n".join(
-                            err['message'] for err in e.errors))) from e
+                        error=e.message)) from e
             quantities.clear()
 
         for product, inventory_item in zip(products, inventory_items):
@@ -702,38 +754,37 @@
                 last_order_id = last_sale.shopify_identifier
             else:
                 last_order_id = ''
-            with shop.shopify_session():
-                if pool.test and 'shopify_orders' in context:
-                    query = ' OR '.join(
-                        f'id:{id}' for id in context['shopify_orders'])
-                elif last_order_id:
-                    query = f'-status:cancelled AND id:>{last_order_id}'
-                else:
-                    query = '-status:cancelled'
-                orders = shopify.GraphQL().execute(
-                    QUERY_ORDERS % {
-                        'query': query,
-                        'fields': graphql.selection(fields),
-                        })['data']['orders']
-                sales = []
-                for order in orders['nodes']:
-                    sales.append(Sale.get_from_shopify(shop, order))
-                Sale.save(sales)
-                for sale, order in zip(sales, orders['nodes']):
-                    total_price = Decimal(
-                        order['currentTotalPriceSet']['presentmentMoney'][
-                            'amount'])
-                    sale.shopify_tax_adjustment = (
-                        total_price - sale.total_amount)
-                Sale.save(sales)
-                to_quote = [
-                    s for s in sales if s.party != s.web_shop.guest_party]
-                if to_quote:
-                    Sale.quote(to_quote)
-                for sale, order in zip(sales, orders['nodes']):
-                    if sale.state != 'draft':
-                        Payment.get_from_shopify(sale, order)
-                Sale.payment_confirm(sales)
+            if pool.test and 'shopify_orders' in context:
+                query = ' OR '.join(
+                    f'id:{id}' for id in context['shopify_orders'])
+            elif last_order_id:
+                query = f'-status:cancelled AND id:>{last_order_id}'
+            else:
+                query = '-status:cancelled'
+            orders = shop.shopify_request(
+                QUERY_ORDERS % {
+                    'query': query,
+                    'fields': graphql.selection(fields),
+                    }).data['orders']
+            sales = []
+            for order in orders['nodes']:
+                sales.append(Sale.get_from_shopify(shop, order))
+            Sale.save(sales)
+            for sale, order in zip(sales, orders['nodes']):
+                total_price = Decimal(
+                    order['currentTotalPriceSet']['presentmentMoney'][
+                        'amount'])
+                sale.shopify_tax_adjustment = (
+                    total_price - sale.total_amount)
+            Sale.save(sales)
+            to_quote = [
+                s for s in sales if s.party != s.web_shop.guest_party]
+            if to_quote:
+                Sale.quote(to_quote)
+            for sale, order in zip(sales, orders['nodes']):
+                if sale.state != 'draft':
+                    Payment.get_from_shopify(sale, order)
+            Sale.payment_confirm(sales)
 
     @classmethod
     def shopify_update_order(cls, shops=None):
@@ -768,15 +819,14 @@
         assert shop.type == 'shopify'
         assert all(s.web_shop == shop for s in sales)
         fields = {'nodes': Sale.shopify_fields()}
-        with shop.shopify_session():
-            query = ' OR '.join(
-                f'id:{s.shopify_identifier}' for s in sales)
-            orders = shopify.GraphQL().execute(
-                QUERY_ORDERS % {
-                    'query': query,
-                    'fields': graphql.selection(fields),
-                    })['data']['orders']
-            id2order = {gid2id(o['id']): o for o in orders['nodes']}
+        query = ' OR '.join(
+            f'id:{s.shopify_identifier}' for s in sales)
+        orders = shop.shopify_request(
+            QUERY_ORDERS % {
+                'query': query,
+                'fields': graphql.selection(fields),
+                }).data['orders']
+        id2order = {gid2id(o['id']): o for o in orders['nodes']}
 
         to_update = []
         orders = []
@@ -802,15 +852,14 @@
         for sale, order in zip(sales, orders):
             assert sale.shopify_identifier == gid2id(order['id'])
             shop = sale.web_shop
-            with shop.shopify_session():
-                sale = Sale.get_from_shopify(shop, order, sale=sale)
-                if sale._changed_values():
-                    sale.untaxed_amount_cache = None
-                    sale.tax_amount_cache = None
-                    sale.total_amount_cache = None
-                    sale.shopify_tax_adjustment = None
-                    to_update[sale] = order
-                    states_to_restore[sale.state].append(sale)
+            sale = Sale.get_from_shopify(shop, order, sale=sale)
+            if sale._changed_values():
+                sale.untaxed_amount_cache = None
+                sale.tax_amount_cache = None
+                sale.total_amount_cache = None
+                sale.shopify_tax_adjustment = None
+                to_update[sale] = order
+                states_to_restore[sale.state].append(sale)
         Sale.write(list(to_update.keys()), {'state': 'draft'})
         with Transaction().set_context(_log=True):
             Sale.save(to_update.keys())
@@ -841,9 +890,7 @@
 
         for sale, order in zip(sales, orders):
             if sale.state != 'draft':
-                shop = sale.web_shop
-                with shop.shopify_session():
-                    Payment.get_from_shopify(sale, order)
+                Payment.get_from_shopify(sale, order)
         Sale.payment_confirm(sales)
 
     @classmethod
@@ -855,7 +902,8 @@
         if (mode == 'write'
                 and external
                 and values.keys() & {
-                    'shopify_url', 'shopify_password',
+                    'shopify_shop_name',
+                    'shopify_access_token',
                     'shopify_webhook_shared_secret'}):
             warning_name = Warning.format('shopify_credential', shops)
             if Warning.check(warning_name):
@@ -904,7 +952,7 @@
 
         try:
             shopify_media = graphql.iterate(
-                QUERY_PRODUCT_CURSOR % {
+                self, QUERY_PRODUCT_CURSOR % {
                     'fields': graphql.selection({
                             'media(first: 250, after: $cursor)': (
                                 product_fields['media(first: 250)']),
@@ -929,8 +977,7 @@
             raise ShopifyError(gettext(
                     'web_shop_shopify.msg_product_fail',
                     template=template.rec_name,
-                    error="\n".join(
-                        err['message'] for err in e.errors))) from e
+                    error=e.message)) from e
 
 
 class ShopShopifyIdentifier(IdentifierMixin, ModelSQL, ModelView):
@@ -1019,22 +1066,21 @@
             ]
 
     @fields.depends(
-        'shop', '_parent_shop.shopify_url', '_parent_shop.shopify_version',
-        '_parent_shop.shopify_password')
+        'shop',
+        '_parent_shop.shopify_shop_name',
+        '_parent_shop.shopify_access_token')
     def get_shopify_locations(self):
         locations = [(None, "")]
-        session = attrgetter(
-            'shopify_url', 'shopify_version', 'shopify_password')
+        session = attrgetter('shopify_shop_name', 'shopify_access_token')
         if self.shop and all(session(self.shop)):
             locations_cache = self._shopify_locations_cache.get(self.shop.id)
             if locations_cache is not None:
                 return locations_cache
             try:
-                with self.shop.shopify_session():
-                    for location in graphql.iterate(
-                            QUERY_LOCATIONS, {}, 'locations'):
-                        locations.append(
-                            (str(gid2id(location['id'])), location['name']))
+                for location in graphql.iterate(
+                        self.shop, QUERY_LOCATIONS, {}, 'locations'):
+                    locations.append(
+                        (str(gid2id(location['id'])), location['name']))
                 locations = self._shopify_locations_cache.set(
                     self.shop.id, locations)
             except GraphQLException:

Reply via email to