Скрипты. Pyrus Help
workflow:scripts

Скрипты

Платформа Pyrus Scripting позволяет аналитикам и консультантам расширять функционал форм Pyrus: автоматически вычислять значения полей и обеспечивать проверку вводимых пользователем значений.

Скрипты Pyrus предоставляют возможности:

  • Автоматически заполнять поля формы при вводе значений других полей.
  • Валидировать значения полей.
  • Автоматически заполнять поля формы с использованием реестров форм.

Скрипты разрабатываются на языке JavaScript и работают внутри формы Pyrus. Платформа обеспечивает безопасную среду выполнения и контролирует доступ скриптов к данным.

Быстрый старт

Чтобы попробовать скрипты Pyrus, выполните следующие действия:

  1. Создайте форму «Отпуск» из списка готовых шаблонов:
  2. Добавьте числовое поле «Продолжительность» (продолжительность отпуска в днях)
  3. Из настроек формы «Отпуск» нажмите кнопку «Изменить скрипт»: Откроется редактор кода скрипта:
  4. Вставьте в редактор следующий код:
     form.onChange(['Дата начала', 'Дата окончания'])
      .setValue('Продолжительность', state => {
        const [start, end] = state.changes;
      
        if (!start.date || !end.date)
          return null;
            
        return daysBetween(
          new Date(start.date), 
          new Date(end.date)
        ) + 1;
      });
    
    function daysBetween(d1, d2) {
      const msInDay = 1000 * 60 * 60 * 24;
      return Math.floor((d2.getTime() - d1.getTime()) / msInDay);
    }
      
  5. Сохраните изменения.

Теперь откройте новую форму «Отпуск» и заполните поля «Дата начала» и «Дата окончания». В поле «Продолжительность» автоматически появится количество дней отпуска:

Архитектура

Скрипт представляет собой исполняемый JavaScript-код, который подписывается на события изменения полей формы и задает логику вычисления зависимых полей. Рассмотрим пример:

Есть форма с полями «Цена единицы товара», «Количество» и «Общая сумма». Поле «Общая сумма» можно заполнять автоматически (как произведение цены на количество) с помощью следующего кода:

form
  .onChange(['Цена единицы товара', 'Количество'])
  .setValue('Общая сумма', state => {
      const price = state.changes[0].value;
      const quantity = state.changes[1].value;

      return price * quantity;
  });
Разберем подробнее этот код.

В контексте исполнения скрипта доступен глобальный объект form типа FormProxy, обеспечивающий взаимодействие кода скрипта с формой в Pyrus:

interface FormProxy {
  onChange(fieldNames: string[]): ChangeHandler;
  
  fetchSelfRegister(
    filterFn: (filter: RegisterFilter) => RegisterFilter,
    fieldNames: string[]
  ): Promise<RegisterResponse>;
}
Метод onChange принимает первым аргументом список названий полей формы (Цена единицы товара и Количество), при изменении которых нужно вычислить значения зависимых полей, и возвращает объект типа ChangeHandler.

Подробнее про метод fetchSelfRegister см. Реестр формы.

ChangeHandler позволяет задать логику вычисления зависимых полей (Общая сумма), которая будет выполняться каждый раз при изменении любого из полей, перечисленных в методе onChange.

interface ChangeHandler {
  setValue(
    fieldName: string, 
    calcFunction: (state: FormState) => CompositeValue
  ): void;
  
  setValues(
    fieldNames: string[], 
    calcFunction: (state: FormState) => CompositeValue[]
  ): void;
  
  setValueAsync(
    fieldName: string, 
    calcFunction: (state: FormState) => Promise<CompositeValue>
  ): void;
  
  setValuesAsync(
    fieldNames: string[], 
    calcFunction: (state: FormState) => Promise<CompositeValue[]>
  ): void;  
  
  validate(
    fieldName: string, 
    validateFunction: (state: FormState) => {errorMessage: string} | null
  ): void;
  
  validateAsync(
    fieldName: string, 
    validateFunction: (state: FormState) => Promise<{errorMessage: string} | null>
  ): void;  
}

Аргументы метода setValue:

  • fieldName — название поля, значение которого нужно вычислить
  • calcFunction — функция вычисления значения поля.

Функция calcFunction получает на вход единственным аргументом объект типа FormState и возвращает новое значение (см. Формат значений полей) вычисленного поля, объявленного в методе setValue.

Для случаев, когда нужно вычислить сразу несколько полей на основе одних и тех же данных, удобно воспользоваться методом setValues — он принимает массив имен полей, а calcFunction в этом случае должна вернуть соответствующий массив значений этих полей.

Если для вычислений требуется произвести асинхронные операции (например, дождаться завершения запроса реестра), воспользуйтесь асинхронными версиями методов: setValueAsync и setValuesAsync. Их отличие в том, что в calcFunction возвращается Promise («обещание»), который вернет значения полей. Пример использования приведен в разделе Реестр формы.

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

Объект FormState позволяет получить значения зависимых полей (Цена единицы товара и Количество), на основе которых вычисляется значение поля «Общая сумма»:

interface FormState {
  changes: FieldValue[];
  prev: FieldValue[];
}
Поле changes содержит массив текущих значений полей, перечисленных в методе onChange. Поле prev содержит массив «предыдущих» значений вычисляемых полей.

Таблицы

Для форм с таблицами скрипты позволяют:

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

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

Вычисляемое поле таблицы

Есть форма с таблицей товаров, в которой для каждого товара содержится «Название», «Цена», «Количество», «Сумма» и «Ставка НДС» (поле мультивыбор со значениями «0%», «10%» и «18%»): Поле «Сумма» можно заполнять автоматически с помощью следующего кода:

form.onChange(['Цена', 'Количество', 'Ставка НДС'])
  .setValue('Сумма', state => {
    const [price, quantity, ndsRate] = state.changes;
    
    if (!price.value || !quantity.value)
      return 0;

    let cost = price.value * quantity.value;

    const nds = ndsRate.choice_name 
      ? parseInt(ndsRate.choice_name) 
      : 0;

    if (ndsRate.choice_name)
      cost += cost * (nds / 100);

    return cost;
  });

Сумма колонки таблицы

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

form.onChange(['Сумма', 'Процент скидки'])
  .setValue('Итого', state => {
    const [cost, discount] = state.changes;

    const total = cost.sum * (1 - discount.value / 100);

    return total;
  });
Оба блока кода могут работать совместно, образуя цепочку зависимых полей.

Справочники

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

Допустим, есть задача: когда оператор выбирает в форме регион, в поле «Город» должны быть доступны только города из этого региона, а не общий список городов.

В этом примере полю «Регион» соответствует справочник «Регионы», а полю «Город» — справочник «Города». Города и регионы связаны между собой в отдельном справочнике «Города по регионам».

Чтобы фильтровать список городов по выбранному региону, добавим в скрипты формы следующий блок кода:

let catalogItems = null;

form.getCatalog("
Города по регионамНазвание справочника, который связывает значения двух других справочников
").then(items => { catalogItems = items; }); form.onChange(["
РегионПоле, при изменении которого меняется список выбираемых значений в другом поле
"]).setFilter("
ГородПоле, значения в котором фильтруются в зависимости от выбора в первом поле
", state => { if (!catalogItems || !state.changes[0] || !state.changes[0].columns) return null; const region = state.changes[0].columns["
РегионНазвание колонки, по значениям которой осуществляется фильтрация
"]; const filtered = catalogItems .filter(item => item.columns["
РегионНазвание колонки, по значениям которой осуществляется фильтрация
"] === region) .map(item => item.columns["
ГородНазвание колонки, из которой берутся отфильтрованные значения
"]); return filtered.length > 0 ? { values: filtered } : null });

Валидация

Скрипты позволяют гибко валидировать значения полей для предотвращения ошибок заполнения форм.

Для примера добавим в форму «Отпуск» проверку дат начала и окончания отпуска. Эту форму мы создавали в разделе Быстрый старт. Добавим в скрипт формы следующий блок кода:

form.onChange(['Дата начала', 'Дата окончания'])
    .validate('Дата окончания', state => {
        const [start, end] = state.changes;

        if (start.date && end.date && start.date >= end.date)
            return {errorMessage: 'Не может быть раньше даты начала'};

        return null;
    });
Теперь при заполнении формы отобразится ошибка, если мы случайно выберем дату окончания отпуска раньше даты начала:

Реестр формы

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

Есть форма «Публикация приказа» с полями «Год», «Номер» и «Документ» (файл с приказом). С помощью запроса реестра из скрипта можно убедиться, что приказ за тот же год и с тем же номером еще не был опубликован:

form.onChange(['Год', 'Номер'])
    .validateAsync('Номер', async state => {
        const [year, num] = state.changes;

        if (year.text && num.text) {
            const duplicates = await form.fetchSelfRegister(f => f.fieldEquals('Год', year).fieldEquals('Номер', num), []);
            const firstDuplicate = duplicates.tasks[0];
            if (firstDuplicate)
                return {errorMessage: `Приказ <a href="#id${firstDuplicate.task_id}">уже опубликован</a>`};
        }

        return null;
    });
Теперь при попытке повторно заполнить форму теми же данными пользователь увидит ошибку со ссылкой на уже существующий приказ:

Ограничения

Пользовательские скрипты запускаются в изолированном контексте, в котором доступно стандартное окружение JavaScript, за исключением глобального контекста (window, global) и средств сетевого взаимодействия (XMLHttpRequest и fetch).

Пока скрипты работают только при заполнении и редактировании форм через веб-интерфейс.

Таймауты

Запуск скрипта и вычисления значений полей ограничены по времени в 5 секунд. Если ваш код будет работать дольше, скрипт будет остановлен.

Зависимости

  • запрещены циклические зависимости, например: поле А зависит от Б, а поле Б зависит от А. Скрипт, содержащий циклические зависимости, не будет запущен.
  • поле таблицы может ссылаться только на поля этой же таблицы.
  • при комбинировании табличных и нетабличных полей: нетабличное поле не может ссылаться на ячейки таблицы, только на сумму столбца таблицы.

Отладка

Для отладки работы скрипта доступен метод console.log(…args). Этот метод позволяет выводить значения переменных и отладочную информацию в консоль браузера из приложения Pyrus. В качестве аргументов метод принимает простые типы (String, Number, Boolean) и объекты без циклических ссылок.

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

Формат значений полей

Значения полей формы при чтении (передача значения в скрипт через объект FormState) и записи (возвращаемое из скрипта новое значение поля) могут представлять собой объект, строку или число.
Тип Чтение Запись
Текст,
Почта,
Телефон
{
  text: "значение"
}
{
  text: "новое значение"
}
или
"новое значение"
Число,
Деньги
{
  value: 10
}
или (для полей в таблице)
{
  value: 10,
  sum: 50
}
{
  value: 42
}
или
42
Мультивыбор
{
  choice_id: 1,
  choice_name: "Да"
}
{
  choice_id: 1
}
или
{
  choice_name: "Да"
}
Срок
{
  date: "2020-01-01"
}
{
  date: "2020-01-05"
}
или
{
  days_from_create: 10
}
Срок со временем
{
  date_time: "2020-01-01T10:30:00Z"
}
{
  date_time: "2020-01-05T10:30:00Z"
}
или
{
  hours_from_create: 10
}
Справочник
{
  item_id: 123,
  columns: {
    "column name": "value"
  }
}

Была ли эта статья полезной?

Да, спасибо! Нет, остался вопрос