Картинка - Мобильное приложение 1С: работа с push-уведомлениями Firebase Cloud Messaging

Мобильное приложение 1С: работа с push-уведомлениями Firebase Cloud Messaging

Проект демонстрирует практическое использование системы доставки push-уведомлений, созданных средствами платформы 1С, до мобильного приложения. Возможно использование не только для мобильных приложений на базе 1С.

Арт.: 2767575

1. создание проекта и приложения в консоли.png
2. скачать файл services-google-json.png
3. файл закрытого ключа.png
7. кодирование sequence.png
8. проверка ключа.png
4. данные получателя уведомления.png
6. скриншот поступившего уведомления.jpg
обработка для отправки уведомлений.png
1. создание проекта и приложения в консоли.png
2. скачать файл services-google-json.png
3. файл закрытого ключа.png
7. кодирование sequence.png
8. проверка ключа.png
4. данные получателя уведомления.png
6. скриншот поступившего уведомления.jpg
обработка для отправки уведомлений.png
Лицензии

6 100 руб.

Техподдержка и обновления

3 660 руб.

3 000 руб.

Поясним как выглядит общая схема. Есть три актора - мобильное приложение (устройство) -получатель push-сообщения; приложение-сервер, которое является отправителем сообщений; FCM - сервис-провайдер.

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

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

Шаг 1.
Подготовка.

Регистрируемся в сервисе FCM по адресу https://console.firebase.google.com/.
Создаем свои проект и приложение. Важные данные на этом этапе: Project ID и Project number.

Затем надо скачать в разделе App файл google-service.json, он будет нужен нам на этапе сборки мобильного приложения.

Шаг 2.
Получение файла закрытого ключа.

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

Шаг 3.
Получение access-токена.

Перед отсылкой сообщения для рассылки в FCM, нам надо авторизоваться в сервисах Google, чтобы получить access-token. Этот токен будет использоваться при отсылке сообщений в сервис.
Авторизация происходит по OAuth2, для нее мы формируем JWT-токен, подписываем его ранее полученным закрытым ключом и отсылаем на адрес авторизации Google.

// формируем JWT-токен
ТокенJWT = Новый ТокенДоступа;
ТокенJWT.Эмитент = ДанныеЗакрытогоКлюча.client_email; // iss
ТокенJWT.ВремяСоздания = УниверсальноеВремя(ТекущаяДата()) - Дата(1970, 1, 1, 0, 0, 0); // iat
ТокенJWT.ВремяЖизни = 3600; // exp
ТокенJWT.ПолезнаяНагрузка.Вставить("scope", "https://www.googleapis.com/auth/firebase.messaging");
ТокенJWT.ПолезнаяНагрузка.Вставить("aud", "https://oauth2.googleapis.com/token");
ТокенJWT.ПолезнаяНагрузка.Вставить("sub", ДанныеЗакрытогоКлюча.client_email);
ТокенJWT.Подписать(АлгоритмПодписиТокенаДоступа.RS256, КлючОчищенный);

// отправляем готовый JWT в Google Auth для получения Access Token
Соединение = Новый HTTPСоединение("oauth2.googleapis.com",,,,,, Новый ЗащищенноеСоединениеOpenSSL());
Запрос = Новый HTTPЗапрос("/token");
ТелоЗапроса = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer"
        + "&assertion=" + Строка(ТокенJWT);

Запрос.Заголовки.Вставить("Content-Type", "application/x-www-form-urlencoded");
Запрос.Заголовки.Вставить("Content-Charset", "utf-8");
Запрос.УстановитьТелоИзСтроки(ТелоЗапроса, КодировкаТекста.UTF8);
Попытка
    Ответ = Соединение.ОтправитьДляОбработки(Запрос);
    Если Ответ.КодСостояния = 200 Тогда
        СтруктураОтвета = ПрочитатьЗначениеJSON(Ответ.ПолучитьТелоКакСтроку(КодировкаТекста.UTF8));
    Иначе
        ЗаписьЖурналаРегистрации("FCM.Ошибка", УровеньЖурналаРегистрации.Ошибка,,, Ответ.ПолучитьТелоКакСтроку());
        Возврат Неопределено;
    КонецЕсли;
Исключение
    ЗаписьЖурналаРегистрации("FCM.Ошибка", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
    Возврат Неопределено;
КонецПопытки;

Есть некоторые особенности, которые надо проговорить отдельно.

Шаг 3.1.
Подпись JWT-токена.

После создания токен подписывается приватным ключом по RS256 (RSASSA-PKCS1-v1_5 с SHA256). Тут есть нюанс: как понятно по наименованию алгоритма, взятому из документации платформы, метод Подписать() объекта ТокенДоступа просит на вход ключ в формате PKCS#1 v1.5., а FCM выдает закрытый ключ в более продвинутом стандарте PKCS#8. Если подавать его в метод в исходном виде, то получим сообщение "Ошибка создания подписи". Пришлось написать функцию-конвертер. Очень неудобно, собственно, именно на разбор схем кодирования и работу с бинарными данными ушло большое количество времени, надеюсь, этот пробел разработчики платформы устранят и метод подписи будет принимать ключ любого стандарта, "разбираясь" с ним внутри своей реализации. В самом методе шифрования, к счастью, разбираться не пришлось.

В стандарте на PKCS#8 (он сам не очень большой) в разделе 5 Private-Key Information Syntax написано, что нужный нам приватный ключ есть один из реквизитов некоторой структуры.

PrivateKeyInfo ::= SEQUENCE {
    version                 Version,
    privateKeyAlgorithm     PrivateKeyAlgorithmIdentifier,
    privateKey              PrivateKey,
    attributes          [0] IMPLICIT Attributes OPTIONAL }

Соответственно, нам надо прочитать этот реквизит, пропустив последовательно все байты, что встретятся до него - пропускаем заголовок SEQUENCE, реквизиты version и privateKeyAlgorithm. Здесь мы смотрим, как кодируются поля структуры и соответственно выполняем побайтовое чтение.

Функция КонвертироватьPKCS8_в_PKCS1(Знач СтрокаPKCS8) Экспорт
    // очищаем текст файла ключа от заголовков и переносов, чтобы получить чистый Base64
    ЧистыйBase64 = СтрЗаменить(СтрокаPKCS8, "-----BEGIN PRIVATE KEY-----", "");
    ЧистыйBase64 = СтрЗаменить(ЧистыйBase64, "-----END PRIVATE KEY-----", "");
    ЧистыйBase64 = СтрЗаменить(ЧистыйBase64, Символы.ВК, "");
    ЧистыйBase64 = СтрЗаменить(ЧистыйBase64, Символы.ПС, "");
    ЧистыйBase64 = СокрЛП(ЧистыйBase64);

    // декодируем Base64 в двоичные данные и читаем в буфер
    ДвоичныеДанныеPKCS8 = Base64Значение(ЧистыйBase64);
    Буфер = ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(ДвоичныеДанныеPKCS8);

    Указатель = 0;

    // проверяем корневой тег Sequence (0x30)
    ТегSequence = Буфер.Прочитать(Указатель, 1)[0]; Указатель = Указатель + 1;
    Если ТегSequence <> 48 Тогда // 0x30 это 48
        ВызватьИсключение "Неверный формат ASN.1: Ожидался Sequence (0x30)";
    КонецЕсли;

    // читаем поле Version (Integer, обычно 0x02 0x01 0x00)
    ТегVersion = Буфер.Прочитать(Указатель, 1)[0]; Указатель = Указатель + 1;
    ДлинаVersion = ПрочитатьДлинуASN1(Буфер, Указатель);
    Указатель = Указатель + ДлинаVersion; // пропускаем значение версии

    // читаем поле AlgorithmIdentifier (Sequence, описывающий RSA алгоритм)
    ТегAlgId = Буфер.Прочитать(Указатель, 1)[0]; Указатель = Указатель + 1;
    ДлинаAlgId = ПрочитатьДлинуASN1(Буфер, Указатель);
    Указатель = Указатель + ДлинаAlgId; // пропускаем блок идентификатора алгоритма

    // читаем поле PrivateKey (тег Octet String = 0x04)
    ТегPrivateKey = Буфер.Прочитать(Указатель, 1)[0]; Указатель = Указатель + 1;
    Если ТегPrivateKey <> 4 Тогда // 0x04 = 4
        ВызватьИсключение "Неверный формат PKCS#8: Ожидался Octet String (0x04)";
    КонецЕсли;

    ДлинаPrivateKey = ПрочитатьДлинуASN1(Буфер, Указатель);

    // содержимое этого Octet String и есть наш PKCS#1 ключ (включая его собственный Sequence)
    БуферPKCS1 = Буфер.Прочитать(Указатель, ДлинаPrivateKey);

    // превращаем обратно в двоичные данные 1С
    ДвоичныеДанныеPKCS1 = ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(БуферPKCS1);
    Base64PKCS1 = Base64Строка(ДвоичныеДанныеPKCS1);

    Возврат "-----BEGIN RSA PRIVATE KEY-----" + Символы.ПС + Base64PKCS1 + Символы.ПС + "-----END RSA PRIVATE KEY-----";
КонецФункции

Функция ПрочитатьДлинуASN1(Буфер, Указатель)
    ПервыйБайт = Буфер.Получить(Указатель);
    Указатель = Указатель + 1;

    // если старший бит не установлен (длина < 128), это и есть готовая длина
    Если ПервыйБайт < 128 Тогда
        Возврат ПервыйБайт;
    КонецЕсли;

    // в противном случае младшие 7 бит задают количество последующих байт
    КоличествоБайтДлины = ПервыйБайт - 128;
    РезультатДлина = 0;
    Для Счетчик = 1 По КоличествоБайтДлины Цикл
        РезультатДлина = (РезультатДлина * 256) + Буфер.Получить(Указатель);
        Указатель = Указатель + 1;
    КонецЦикла;

    Возврат РезультатДлина;
КонецФункции

Проще всего проверить полученный на выходе функции преобразования новый ключ при помощи утилиты openssl. Сохраните ключ в текстовый файл, и выполните команду в консоли:

openssl rsa --in text.key -check -noout

Если все в порядке, утилита так и напишет. Ключ check - проверка, noout - не показывать проверяемые ключ на экране. Если нет - косяк при реализации функции (да, было, получилось не сразу)).

Шаг 3.2.

Токен готов и подписан. Обращаю внимание на параметры scope и aud. В scope надо указать правильную область действия, в aud - адрес сервера, для которого предназначен формируемый токен. При ошибке в этих полях токен не будет принят.

Что может пойти не так на этом этапе, если все сделали вроде бы корректно?

  1. Ответ сервера без реквизита access_token.
    Если вы получаете структуру только с одним реквизитом id_token, то это некорректный ответ. Сервер просто выдал вам токен идентификации. Надо проверить поле запроса уровней доступа JWT-токена, в нем должно быть указано конкретное значение https://www.googleapis.com/auth/firebase.messaging вместо обобщенного https://googleapis.com.
  2. Ошибка 400.
    {
      "error": {
        "code": 400,
        "message": "The registration token is not a valid FCM registration token",
        "status": "INVALID_ARGUMENT",
        "details": [
          {
            "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError",
            "errorCode": "INVALID_ARGUMENT"
          },
          {
            "@type": "type.googleapis.com/google.rpc.BadRequest",
            "fieldViolations": [
              {
                "field": "message.token",
                "description": "The registration token is not a valid FCM registration token"
              }
            ]
          }
        ]
      }
    }

    Ваша авторизация, генерация JWT и подпись закрытым ключом работают нормально. Подвела генерация токена (идентификатора) получателя, этого мы еще коснемся.

  3. Ошибка 404.
    {
      "error": {
        "code": 404,
        "message": "NotRegistered",
        "status": "NOT_FOUND",
        "details": [
          {
            "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError",
            "errorCode": "UNREGISTERED"
          }
        ]
      }
    }

    Эта ошибка означает, что токен устройства, на который вы отправляете пуш-уведомление, больше недействителен на серверах Google. Сервис FCM считает это устройство отписанным от уведомлений. Надо получить новый идентификатор устройства.

Шаг 4.
Получение идентификатора мобильного устройства.

В мобильном приложении нам надо получить идентификатор устройства, по которому устройство будет определяться провайдером как получатель уведомлений. Как написано в документации, идентификатор может периодически меняться, поэтому его надо получать регулярно и пересылать приложению-серверу.

Этот фрагмент кода исполняется каждый раз при старте приложения на мобильном устройстве. Он получает все идентификаторы и отправляет их на http-сервис. Также выполняется подключение обработчика push-уведомлений.

Оповещение = Новый ОписаниеОповещения("ОбработчикУведомлений", ЭтотОбъект);
ДоставляемыеУведомления.ПодключитьОбработчикУведомлений(Оповещение);

ИдентификаторПодписчикаДоставляемыхУведомлений = ДоставляемыеУведомления.ПолучитьИдентификаторПодписчикаУведомлений(НомерПриложенияВСервисеFCM);;

ПараметрыПодключения = ОбменДаннымиВызовСервера.ДанныеСоединения();
Сервер = ПараметрыПодключения.Сервер;
Порт = ПараметрыПодключения.Порт;
Если Не ЗначениеЗаполнено(Сервер) Или Не ЗначениеЗаполнено(Порт) Тогда
    Возврат;
КонецЕсли;

ДанныеЗапроса = Новый Структура("ИдентификаторИнформационнойБазы, ИдентификаторПриложения, ИдентификаторУстройства, ТипПодписчика");
ЗаполнитьЗначенияСвойств(ДанныеЗапроса, ИдентификаторПодписчикаДоставляемыхУведомлений);
ДанныеЗапроса.ТипПодписчика = Строка(ИдентификаторПодписчикаДоставляемыхУведомлений.ТипПодписчика);

Соединение = Новый HTTPСоединение(Сервер, Порт);
Запрос = Новый HTTPЗапрос("InfoBase2/hs/cart/notification");
Запрос.УстановитьТелоИзСтроки(ЗаписатьЗначениеJSON(ДанныеЗапроса), КодировкаТекста.UTF8);

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

Странно отрабатывает метод ПолучитьИдентификаторПодписчикаУведомлений, для телефона марки Huawei, подключенного через кабель возвращает тип подписчика HPK, а после развертывания собранного приложения возвращает тип FCM.

Шаг 5.
Отправка уведомления в сервис FCM.

Само уведомление - это простая структура, отправляемая в теле запроса на адрес fcm.googleapis.com. Адрес ресурса формируется с использованием имени проекта /v1/projects/project_id/messages:send.
При отправке как раз и используется ранее полученный токен, он указывается в заголовке Authorization.

Процедура ОтправитьУведомлениеFCM(ТокенУстройства, ЗаголовокСообщения, ТекстСообщения) Экспорт
    ДанныеЗакрытогоКлюча = ПолучитьСтруктуруЗакрытогоКлюча();
    Попытка
        AccessToken = ПолучитьGoogleAccessToken(ДанныеЗакрытогоКлюча);
    Исключение
        ЗаписьЖурналаРегистрации("FCM.Ошибка", УровеньЖурналаРегистрации.Ошибка,,, "Авторизация не удалась: " + ОписаниеОшибки());
        Возврат;
    КонецПопытки;
    Если AccessToken = Неопределено Тогда
        ЗаписьЖурналаРегистрации("FCM.Ошибка", УровеньЖурналаРегистрации.Ошибка,,, "Авторизация не удалась: " + ОписаниеОшибки());
        Возврат;
    КонецЕсли;

    // формируем тело сообщения для FCM v1 API
    Уведомление = Новый Структура("title, body", ЗаголовокСообщения, ТекстСообщения);
    СтруктураСообщения = Новый Структура("token, notification", ТокенУстройства, Уведомление);
    Сообщение = ЗаписатьЗначениеJSON(Новый Структура("message", СтруктураСообщения));

    Соединение = Новый HTTPСоединение("fcm.googleapis.com",,,,,, Новый ЗащищенноеСоединениеOpenSSL());
    АдресРесурса = "/v1/projects/" + ДанныеЗакрытогоКлюча.project_id + "/messages:send";
    Запрос = Новый HTTPЗапрос(АдресРесурса);
    Запрос.Заголовки.Вставить("Authorization", "Bearer " + AccessToken);
    Запрос.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
    Запрос.УстановитьТелоИзСтроки(Сообщение, КодировкаТекста.UTF8);
    Попытка
        Ответ = Соединение.ОтправитьДляОбработки(Запрос);
        Если Ответ.КодСостояния = 200 Тогда
            ЗаписьЖурналаРегистрации("FCM.Успех", УровеньЖурналаРегистрации.Информация, , , "Push-уведомление успешно отправлено!");
        Иначе
            ЗаписьЖурналаРегистрации("FCM.Ошибка", УровеньЖурналаРегистрации.Ошибка, , ,
                "Код ответа FCM: " + Ответ.КодСостояния + " Тело: " + Ответ.ПолучитьТелоКакСтроку());
            Сообщить("Код ответа FCM: " + Ответ.КодСостояния + " Тело: " + Ответ.ПолучитьТелоКакСтроку());
            Возврат;
        КонецЕсли;
    Исключение
        ЗаписьЖурналаРегистрации("FCM.Ошибка", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки());
        Сообщить(ОписаниеОшибки());
        Возврат;
    КонецПопытки;

    Сообщить("Сообщение отправлено!");
КонецПроцедуры

Если вам надо тестировать отправку уведомлений без реальной доставки пользователями, то формируйте сообщение с дополнительным тегом "validate_only": true, описано тут https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send.

Шаг 6.
Реализация процедуры-обработчика уведомлений.

Решение определяется исключительно логикой работы приложения, как минимум необходимо наличие в коде объявления процедуры.

Процедура ОбработчикУведомлений(Уведомление, Локальное, Показано, ДополнительныеСведения) Экспорт
    Сообщить(СтрШаблон("Текст - %1: данные - %2", Уведомление.Текст, Уведомление.Данные));
КонецПроцедуры

Шаг 7.
Сборка приложения.

При сборке надо включить в дистрибутив полученный файл google-services.json. Необходимо, чтобы полный идентификатор приложения google в сборщике полностью совпадал с Package name в консоли сервиса FCM, в противном случае сборщик будет выдавать ошибку.

Используемые версии ПО

Разработка велась на Windows 11 26H2, платформе 8.5.4.1306, мобильной платформе 8.5.4.28, Apache 2.4. Тестировалось на устройстве Huawei c OC Android и надстройкой EMUI и установленными сервисами microG 0.3.16.252432-hw и GBox 1.8.3.61. Для них в настройках приложений необходимо разрешить уведомления, процесс не описываю, т.к., судя по информации из интернета, последовательность действий зависит от установленных версий. По итогу, отправленные с ПК уведомления успешно приходят на устройство, даже если мобильная платформа закрыта, если выгружена из памяти.

Прилагаются файлы приложений, на которых отлаживался обмен между компонентами системы, архив с файлами dt и cf приложения-сервера и мобильного приложения, apk-архивы собранного мобильного приложения для разных платформ, краткая инструкция по настройке взаимодействия компонентов. Приложение сервер содержит обработку для рассылки уведомлений на мобильное устройство, есть тестовое наполнение информационной базы. Это прототипы для понимания как все работает, не более. Без доработки нельзя применять в реальных проектах. Например, необходим обмен между мобильным приложением и приложением-сервером по https, иной подход к хранению закрытого ключа и некоторые другие детали.

Возможна отсылка сообщений не только для мобильных приложений, созданных на базе платформы 1С, но и для приложений, написанных на любом языке, при условии, что приложение сможет выдать идентификатор устройства во внешний сервис.

В планах теперь познакомиться с Huawei Push Kit.

Какими ресурсами надо пользоваться:

  1. https://its.1c.ru/db/v854doc#bookmark:dev:TI000001540 Глава 29. Разработка для мобильных устройств. Раздел 28.3.6.12. Работа с уведомлениями. Читается тяжело, есть нюансы.
  2. https://habr.com/ru/articles/842056/. Хорошая статья про устройство JWT-токена.
  3. https://firebase.google.com/docs/cloud-messaging?hl=ru. Документация сервиса FCM.
  4. https://developers.google.com/identity/protocols/oauth2?hl=ru. Документация по авторизации OAuth2 для доступа к API Google.
  5. //infostart.ru/1c/tools/1518089/?ID=1518089#. Статья на Infostart, но приведенное там решение уже не работает.
  6. https://datatracker.ietf.org/doc/html/rfc5208. Описание алгоритма RSA256 PKCS#8.
  7. https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der. Исключительно полезный мануал по структурам данных, используемых в криптографии.

Техническая поддержка

В случае возникновения ошибок рекомендуем ознакомиться с описанием и документацией программы. Если не нашли решения своей проблемы в документации или решение не помогает - тогда создайте обращение по инструкции ниже:

1

Создать обращение (тикет)

Создать тикет

2

Заполнить данные

Контакты, номер заказа, подробное описание вопроса
Напишите порядок ваших действий с программой, приложите видео/скриншоты/отчеты об ошибке
Точную конфигурацию 1С, версии платформы, инструмента, СУБД

3

Дождаться ответа

Время ответа до 24 часов в рабочее время

Внимание! Техническая поддержка предоставляется исключительно в рамках переписки по обращению. В некоторых случаях для диагностики ошибок и/или вопросов, связанных с особенностями использования продукта в информационных базах покупателя, может потребоваться дополнительная платная диагностика с организацией удаленного доступа к информационной базе. Стоимость уточняется индивидуально.

Остались вопросы?

Для получения дополнительной информации и помощи в настройке модуля под нужды вашего бизнеса — оставьте заявку

Остались вопросы
Статистика:
Просмотры 109
Загрузки 0
Рейтинг 0
Создание 31.08.26 11:41
Обновление 31.08.26 11:41
№ Публикации 2767575
Характеристики:
Теги

мобильное приложение мобильная платформа Android Firebase cloud Messaging FCM.

Рубрики Мобильная разработка
Кому Программист
Тип файла Архив с данными
Платформа 1С 8.5
Конфигурация Универсальные
Операционная система Не имеет значения
Страна Россия
Отрасль Не имеет значения
Налоги Не имеет значения
Вид учета Не имеет значения
Доступ к файлу Платные (руб)
Код открыт Да

См. также

1С.Информер

Сбор заказов, инвентаризация, проверка ценников, просмотр полной информации об остатках и ценах со смартфона Онлайн - все это содержит в себе решение 1С "Штрихкод-информер" (штрих-код чекер). Отправка данных со смартфона выполняется либо на...

3050 руб.

Подписываем внутренние электронные документы из соц.сети Вконтакте, из мессенджера Макс и мобильного приложения. Решение разр...

Часто в программе «1С:Бухгалтерию государственного учреждения 8» (1С:БГУ), возникает потребность  в  оперативном согласовании и подписании первичных документов (актов, накладных, требований) сотрудниками, находящимися вне офиса. Для упроще...

15250 руб.

Мобильное приложение для автоматизации складских операций "МРМ:Маркировка" для конфигураций 1С

"МРМ:Маркировка" для работы с конфигурациями 1С - готовое решение, позволяющее быстро автоматизировать работу учетной системы 1С на ордерном и безордерном складе при работе как с товарами, подлежащими обязательной маркировке, так и с товара...

19000 руб.

Управление разъездными и мобильными сотрудниками Планизатор

Планизатор ПРОФ - управление задачами разъездных сотрудников. Диспетчер в 1С - планирует, назначает исполнителей, прикрепляет документацию к задаче. Исполнитель в мобильном приложении - отмечает исполнение, прикрепляет фотоотчеты, отмечает...

1016 руб.

Что нам стоит бота построить? Нарисуем - будет жить! Графический конструктор телеграм-ботов/Telegram

Теперь создать telegram-бота - элементарно. Достаточно просто нарисовать блок-схему телеграм-бота, и он сразу заработает. Это возможно при использовании Графического конструктора телеграм-ботов. Это единственный конструктор ботов для telegr...

13420 руб.