Django + Zoho CRM: как управлять данными без головной боли

Почти любой современный бизнес, когда приходит время выстраивать отношения с клиентами, полагается на CRM-системы (Customer Relationship Management). И сталкивается с тем, что ожидания не совпадают с реальностью: эффективность не такая высокая, да и управление данными кажется слишком сложным. Один из способов решить эти проблемы — интегрировать сервис со своим веб-приложением.

Привет! Меня зовут Денис, я Python-разработчик в Kokoc Group. Уже около года занимаюсь интеграцией одной из наших платформ на Django с Zoho CRM. Мы соединяем пользователей с широким спектром услуг, требования к обработке заявок и управлению партнерскими и пользовательскими профилями высокие, поэтому Zoho CRM стала ключевым элементом.

В статье хочу показать, как провести ее и сделать процессы проще и эффективнее. Мы рассмотрим все этапы: от подготовки среды до настройки вебхуков и асинхронной обработки данных с помощью Celery. Советую почитать всем разработчикам и менеджерам, так или иначе связанным с работой в CRM-системах.

О чем поговорим:

  • Как подготовить CRM и Django-приложение к интеграции.

  • Как взаимодействовать с API Zoho CRM через Python.

  • Как настроить правила рабочего процесса в Zoho CRM для отправки данных в Django-приложение.

  • Как обработать данные, полученные от вебхука Zoho CRM, и обновить базу данных Django.

  • Как использовать Celery для асинхронной обработки данных из Zoho CRM.

Немного о Zoho CRM

Скрытый текст

Zoho CRM — это комплексная система управления отношениями с клиентами (CRM) от компании Zoho. В ней есть инструменты для управления лидами, контактной информацией, сделками, маркетинговыми кампаниями, обслуживанием клиентов и аналитикой — реализованные, как набор модулей:

  • Лиды (Leads) — модуль для сбора и управления информацией о потенциальных клиентах.

  • Контакты (Contacts) — модуль для хранения контактной информации о клиентах как физических лицах.

  • Аккаунты (Accounts) — модуль для хранения информации о компаниях, с которыми вы взаимодействуете.

  • Продукты (Products) — модуль для хранения информации о продуктах компании.

  • Сделки (Deals) — Модуль для отслеживания прогресса продаж и управления сделками.

Стандартные модули можно редактировать, как и добавлять новые под свои бизнес-процессы. Еще один плюс — интеграция с другими продуктами Zoho: Zoho Mail, Zoho Campaigns, Zoho Desk, Zoho Books — из которых можно собрать полноценную платформу для управления бизнесом.

Готовим CRM-систему

Первым делом нужно зарегистрироваться с в системе и создать песочницу (дальше работать будем уже в ней).

  1. Заводим аккаунт и переходим в настройки:

    7f5395884390df88ea81a3c863ef53d6.png
  2. Выбираем Sandbox:

    5e18e9e42a2238df1d948c47a8fd1799.png
  3. Нажимаем Create New Sanbox, заполняем форму, нажимаем Create:

    75677fa18d1f6c947d6add1bb6151f7e.png
  4. Нажимаем Go to sunbox и оказываемся внутри нашей песочницы (о чем говорит флаг на скриншоте):

d5010974a065d8cdfa9d43d88ae55108.png

Получаем ключи API

Для взаимодействия понадобятся ключи. Идем сюда: https://api-console.zoho.eu/:

fbe538ce52fa7a1343af1d8197353dd9.png

  1. Выбираем Self Client, нажимаем Create:

39b287eea7d701092c99693e4c477a01.png
  1. Записываем вот эти значения — они еще понадобятся:

425c5a04e39890c246201bcc17291b57.png

Указываем Scope

В API Zoho, он определяет разрешения, которые приложение запрашивает для доступа к различным ресурсам (подробнее советую почитать тут: https://www.zoho.com/crm/developer/docs/api/v6/scopes.html). 

Мы возьмем «ZohoCRM.modules.ALL», который дает доступ ко всем модулям в Zoho CRM. То есть приложение сможет выполнять операции (чтение, создание, обновление, удаление) с любыми модулями: контактами, сделками, компаниями и прочим.

  1. Нажимаем Create и выбираем кабинет, для которого создается ключ:

b047396e85092f41cb64469c287867f9.png

  1. Выбираем, для какой версии CRM создать токен: для производственной или песочницы. Фича удобная: достаточно взять однажды все токены и больше не возвращаться к этому вопросу.

Получаем токены

Отправляем POST-запрос на этот url со следующими параметрами. Пример:
Заменить токены на условные
https://accounts.zoho.eu/oauth/v2/token? client_id=&client_secredt=&code=&grant_type=authorizationcode  
В ответ нам придет access_token и refresh_token. Сохраняем и идем дальше.

Готовим Django-приложение

Первым делом устанавливаем пакеты, которые понадобятся для работы:

  • Django — основной фреймворк для разработки веб-приложений.

  • djangorestframework — пакет для создания API.

  • requests — библиотека для отправки HTTP-запросов.

  • Celery — система для создания и обработки асинхронных задач.

Далее прописываем модели, я для примера буду использовать эти:

  1. Расширенная модель юзера:

class User(AbstractBaseUser):
    first_name = models.CharField(_('first name'), max_length=30, 
blank=True, null=True)
    last_name = models.CharField(_('last name'), max_length=30, 
blank=True, null=True)
    phone_number = models.CharField(_('phone number'), max_length=20, 
unique=True)
    email = models.EmailField(_('email address'), unique=True)

    USERNAME_FIELD = 'email'
    objects = UserManager()

    class Meta:
        verbose_name = _('User')
        verbose_name_plural = _('Users')
  1. Модель продукта:

class Product(models.Model):
    name = models.CharField(verbose_name=_("Product name"),
max_length=200)
    description = models.TextField(verbose_name=_("Product description"),
blank=True, null=True)
    price = models.DecimalField(verbose_name=_("Price"),
decimal_places=2, max_digits=10)
    zoho_product_id = models.CharField(verbose_name=_("Zoho product ID"), 
max_length=50, blank=True, null=True, unique=True)

    def __str__(self):
        return self.name
  1. Модель ExternalResource. Она нужна для хранения данных, полученных при настройке Zoho CRM (Client ID, Client Secret, access_token, refresh_token):

class ExternalResource(models.Model):
    name = models.CharField(verbose_name=_("Name"), max_length=200, 
unique=True)
    client_id = models.CharField(verbose_name=_("Client ID"), 
max_length=200, null=True, blank=True)
    client_secret = models.CharField(verbose_name=_("Client Secret"), 
max_length=200, null=True, blank=True)
    token = models.CharField(verbose_name='Token', max_length=256, 
null=True, blank=True, default=None)
    refresh_token = models.CharField(verbose_name='Refresh token', 
max_length=256, null=True, blank=True, default=None)

    class Meta:
        verbose_name = _('Third-party service')
        verbose_name_plural = _('Third-party services')

    @classmethod
    def get_zoho(cls):
        return cls.objects.get(name='ZohoAPI')

Регистрируем ExternalResource в админке. И создаем объект модели с полученными данными:

a47b246acdb8ae3dac14ac00f331d302.png

Настраиваем взаимодействие с Zoho API. Понадобятся следующие классы:

  1. ExternalResourceManager — базовый класс для работы с API внешних сервисов:

class ExternalResourceManager(ABC):
    """
        Базовый класс для работы с API внешних сервисов, таких как ZohoCRM.
    """

    @abstractmethod
    def db_object(self):
        """
            Метод реализует получение объекта модели ExternalResource
        """
        pass

    @abstractmethod
    def send_request(self, method, url, data, **kwargs):
        """
            Метод реализует отправку запроса к внешнему ресурсу
        """
        pass
  1. ZohoBaseManager — класс для работы с ZohoCRM API, реализующий методы отправки данных в ZohoCRM и обновления токенов:

class ZohoBaseManager(ExternalResourceManager):

    @cached_property
    def db_object(self):
        return ExternalResource.get_zoho()

    def send_request(self, method, url, data=None, auth=False, **kwargs):
        """
        Отправка запроса к ZohoCRM API.
        """
        if auth:
            kwargs.update({"Authorization": f"Zoho-oauthtoken {self.db_object.token}"})

        try:
            response = requests.request(method=method, url=url, data=data, headers=kwargs)
        except RequestException as e:
            pass
            # как то обрабатываем исключение...
        return response

    def handle_response(self, response):
        """
        Обработка ответа от API.
        Проверяем на наличие ошибок и возвращаем необходимые данные.
        """
        if response is None:
            raise ValueError("Ответ от API отсутствует")

        try:
            response_json = response.json()
        except ValueError:
            raise ValueError("Невозможно преобразовать ответ в JSON")

        if response.status_code != 200:
            error_message = response_json.get('error', 'Неизвестная ошибка')
            raise Exception(f"Ошибка API: {error_message} (код {response.status_code})")

        return response_json


    def update_token(self):
        """
            Отправка запроса к Zoho API для  обновления token
        """
        url = 'https://accounts.zoho.eu/oauth/v2/token'

        data = {
            'client_id': self.db_object.client_id,
            'client_secret': self.db_object.client_secret,
            'refresh_token': self.db_object.refresh_token,
            'grant_type': 'refresh_token',
        }

        response = self.send_request(
            method='post',
            url=url,
            data=data
        )

        try:
            response_json = self.handle_response(response)
        except Exception as e:
            # как то обрабатываем исключение...
            return

        new_token = response_json.get('access_token')
        self.db_object.token = new_token
        self.db_object.save()
  1. RegistrationDataManager — класс для управления данными при регистрации пользователя.

class RegistrationDataManager(ZohoBaseManager):

    @staticmethod
    def get_registration_data(user):
        data = {
            "data": [
                {
                    'First_Name': user.first_name,
                    'Last_Name': user.last_name,
                    'Email': user.email,
                    'Mobile': user.phone_number,
                }
            ]
        }
        return data

    def create_lead(self, user):
        """
        Отправляет данные для создания лида в ZohoCRM.
        """
        data = self.get_registration_data(user)
        json_data = json.dumps(data)
        url = "https://www.zohoapis.eu/crm/v2/Leads"
        method = 'post'
        response = self.send_request(method, url, json_data, auth=True)
        json_response = self.handle_response(response)
        # Обрабатываем ответ от ZohoCRM (проверяем статус, 
получаем ID лида и т.д.)

Обеспечиваем бесперебойный доступ к API Zoho CRM

Чтобы доступ оставался постоянным, необходимо обновлять токен. Напишем задачу, которая будет делать это за нас каждые 10 минут.

  1. Создаем Celery-таск, и в нем — экземпляр ZohoBaseManager и вызываем метод update_access_token (), который отвечает за фактическое обновление токена.

@shared_task
def refresh_zoho_token():
    """Обновляет токен доступа для API Zoho CRM."""
    manager = ZohoBaseManager()
    manager.update_token token()
  1. Настраиваем расписание для выполнения задачи в файле settings.py (там будем определять, как часто вызывается задача). Создаем запись для задачи refresh_zoho_token в словаре CELERY_BEAT_SCHEDULE:

CELERY_BEAT_SCHEDULE = {
    'refresh_zoho_token': {
        'task': 'zoho_integration.tasks.refresh_zoho_token',
        'schedule': crontab(minute='*/10'),
        'args': (),
        'kwargs': {},
        'options': {
            'expires': 60.0,
        },
    },
}

Что именно прописали

Скрытый текст

  • task — указывает путь к нашей задаче.

  • schedule — использует crontab (minute='*/10'), то есть «выполнение задачи каждые 10 минут».

  • args и kwargs — пустые, так как задача не требует дополнительных параметров.

  • options — гарантирует, что если задача не будет выполнена в течение минуты, то станет считаться устаревшей и отменится.

Создаем лида в ZohoCRM при регистрации пользователя

Здесь все просто:

  1. Используем Django-сигналы, чтобы отслеживать создание пользователей:

@receiver(post_save, sender=User)
def send_user_data(sender, instance, created, **kwargs):
    if created:
        create_zoho_lead.delay(instance.id)
  1. Используем Celery-задачи для асинхронной отправки данных в Zoho CRM:

@shared_task
def create_zoho_lead(user_id: int) -> None:
    try:
        user = User.objects.get(pk=user_id)
    except User.DoesNotExist:
        return

    manager = RegistrationDataManager()
    manager.create_lead(user)

Вот и все, теперь при каждой регистрации данные о пользователе будут отправляться в ZohoCRM.

Проводим интеграцию с помощью Workflow Rules

Представим, что каждый раз, когда в Zoho создается или обновляется продукт, нам нужно забирать его в свою базу данных. Реализовать это можно как правила рабочего процесса и функций в Zoho CRM.

Подробнее о правила рабочего процесса

Скрытый текст

В Zoho CRM, Workflow Rules —- это по сути триггеры, которые автоматизируют задачи. Правила могут срабатывать автоматически в конкретное время или запускаются, когда:

  • создается новая запись;

  • обновляется существующая запись;

  • удаляется запись;

  • создаются, изменяются, удаляются примечания к записи.

Создадим правило, которое при создании или обновлении продукта будет присылать нам данные по нему и обновлять значение в базе.

  1. Переходим в настройки и нажимаем на вкладку Workflow Rules:

867a68e4397a1f352a4ecc5519add940.png
  1. Create Rule и заполняем форму:

cb978bab876599cfde5066ffd6074c67.png
  1. Нажимаем Next и попадаем на эту страницу:

dfa161692762f7651936210141c4955c.png

  1. В первом селекте выбираем Record Action. Во втором — Create or Edit и Repeat this workflow whenever a Product is edited. Эти настройки говорят о том, что правило будет срабатывать при каждом редактировании записи:

e13527e727898eeb3be3faf42f4feca1.png

  1. Нажимаем Next, выбираем All Products (так правило будет применяться ко всем записям):

885cfb2a792925fd97d68c6190f78212.png

  1. Выбираем, какое именно действие будет происходить. В нашем случае — Function:

5374d47b9fb1c1e1b8944935f583205c.png

  1. В появившемся окне выбираем Write your own:

5aca0d0cc90cb2cc653c8f7bd96a4287.png

  1. Заполняем форму и нажимаем Create:

308227ac702859213976df7470e328ca.png

  1. Попадаем на страницу создания функции:

59a4ec3cf0f7e3918066cbd217ab9b0d.png

Разберем типичную функцию в Zoho CRM

Что это вообще такое?

Скрытый текст

Это фрагменты кода, написанные на языке Deluge. Deluge разработан специально для Zoho CRM и позволяет выполнять операции более сложные, чем простое изменение полей или отправка писем. Вот пара примеров:

  • Обработка данных. Вы можете манипулировать данными из разных записей, использовать условные операторы и циклы для логического управления потоком выполнения.

  • Вызов API. Вы можете использовать функции для вызова внешних API, например, для получения данных из других сервисов или для отправки запросов в приложение.

В качестве примера возьмем функцию, которая будет отправлять данные о продукте из Zoho CRM в наше Django-приложение.

  1. Указываем, что функция принимает ID записи, и нажимаем Edit Arguments:

61a047c58a1b0b7569c1d6e18573c640.png
  1. В появившемся окне, в поле Argument Name задаем имя аргумента. В поле Argument Value вводим »#». Выбираем Product Id. Нажимаем Save:

b8c68d958bb2cc4ba3d54a0636e68b72.png

Теперь ID продукта будет подставляться автоматически при срабатывании правила и вызове функции. И мы сможем его использовать в коде функции, чтобы получить создаваемую или редактируемую запись.

Вот пример функции с пояснениями:

29542daa46175ab290f97b33f1eff2af.png

Напишем view, которая будет принимать запрос

View

class ZohoProductView(APIView)
    serializer_class = ZohoProductSerializer
    permission_classes = (CheckZohoTokenPermission,)

    def post(self, request):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)
        sync_product.delay(data=serializer.validated_data)
        return Response(serializer.data, status=status.HTTP_202_ACCEPTED

Serializer

class ZohoProductSerializer(serializers.Serializer):
    zoho_product_id = serializers.CharField()
    name = serializers.CharField()
    description = serializers.CharField()
    price = serializers.DecimalField(max_digits=10, decimal_places=2)

Permission

class CheckZohoTokenPermission(BasePermission):

    def has_permission(self, request, view):
        if request.META.get("HTTP_ZOHO_REQUEST_AUTH_TOKEN") == settings.ZOHO_REQUEST_AUTH_TOKEN:
            return True

Celery Task

@shared_task
def sync_product(data):
    zoho_product_id = data.get("zoho_product_id")
    product = Product.objects.filter(zoho_product_id=zoho_product_id)
    if product.exists():
        product.update(**data)
    else:
        Product.objects.create(**data)

Теперь, когда в Zoho CRM, в модуле Products будет создаваться или редактироваться запись, сработает правило и система вызовет нужную функцию. Функция в свою очередь отправит данные на наш Django API-endpoint, который принимает данные о продукте и передает их на обработку в Celery-задачу sync_product. Задача проверит наличие продукта в базе данных по zoho_product_id. 

Наконец, если продукт существует, данные обновятся. Если нет — появится новый объект Product с полученными данными. 

Такой подход обеспечивает синхронность Zoho CRM и Django-приложением, то есть все изменения в данных продуктов будут отражены в обеих системах.

Заключение

Я постарался максимально полно показать, как интегрировать Zoho CRM с Django-приложением для автоматизации работы с данными о клиентах и продуктах. Но важно помнить, что эта статья — лишь начало, и интеграция с Zoho CRM — это гибкий и мощный инструмент, который можно настроить под любые потребности. Экспериментируйте, чтобы сделать сделать свой бизнес еще эффективнее.

И пара советов напоследок:

  • Добавьте логирование, чтобы отслеживать ошибки и успешную обработку данных.

  • Документируйте код. Это поможет и вам, и вашей команде лучше ориентироваться в структуре интеграции и легко вносить изменения в будущем. 

  • Тестируйте. Это поможет убедиться, что интеграция работает корректно и изменения не нарушат ее работу. 

  • Изучите документацию Zoho CRM и Django. В них вы найдете много полезной информации, которая поможет реализовать более сложные сценарии.

© Habrahabr.ru