Design, Интерфейсы, Мир IT

Строим интерфейс по вводу документов через подбор

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

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

Задача

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

Рассмотрим три часто встречаемых в бизнес-приложениях типа документов:

  1. Заказ на закупку. В таких документах пользователю логично показывать список всех товаров, доступных для заказа от поставщика. В колонки удобно показывать текущий остаток, реализацию за определенный интервал, кол-во заказанного на закупку и продажу.
  2. Заказ на продажу. Здесь чаще всего показывается список товаров, которые есть на остатках выбранного склада и доступны для продажи выбранному клиенту. Также должны показываться текущие цены
  3. Изменение остатков. Это документ используется для корректировки текущих остатков в случае выявления каких-то расхождений с фактическим количеством. В подборе обычно показываются все товары, с возможностью вводить фактический остаток для любого из них. При этом в документ добавляется товар с количеством равным разнице между фактическим и текущим остатком.

Решение

Дальше я покажу как быстро и легко реализовать эту логику на базе открытой и бесплатной платформы lsFusion. В качестве примера создадим логику по вводу заказа на закупку.

Для начала добавим справочник товаров через стандартный CRUD интерфейс:

CLASS Product 'Товар';
name 'Наименование' = DATA STRING[50] (Product);
FORM product ‘Товар’
OBJECTS p = Product PANEL
PROPERTIES(p) name

EDIT Product OBJECT p // эта форма будет использоваться для редактирования товара
;

FORM products ‘Товары’
OBJECTS p = Product
PROPERTIES(p) READONLY name
PROPERTIES(p) NEWSESSION NEWEDITDELETE

LIST Product OBJECT p // эта форма будет использоваться, когда нужно будет выбрать товар из списка
;

NAVIGATOR {
NEW products;
}

Создадим понятие поставщик, и в форме редактирования дадим возможность выбирать товары, с которыми он работает:

CLASS Supplier 'Поставщик';
name 'Наименование' = DATA STRING[50] (Supplier);
in ‘Вкл’ = DATA BOOLEAN (Supplier, Product); // будет TRUE, если у поставщика разрешено закупать этот товар 

FORM supplier ‘Поставщик’
OBJECTS s = Supplier PANEL
PROPERTIES(s) name

OBJECTS p = Product // на форму поставщика добавляем список всех товаров
PROPERTIES in(s, p), name(p) READONLY // даем возможность проставлять галочку для нужных

EDIT Supplier OBJECT s
;

FORM suppliers ‘Поставщики’
OBJECTS s = Supplier
PROPERTIES(s) READONLY name
PROPERTIES(s) NEWSESSION NEWEDITDELETE

LIST Supplier OBJECT s
;

NAVIGATOR {
NEW suppliers;
}

Объявим логику заказов со строками:

CLASS Order 'Заказ';
date 'Дата' = DATA DATE (Order);
number 'Номер' = DATA INTEGER (Order);
supplier ‘Поставщик’ = DATA Supplier (Order);
nameSupplier ‘Поставщик’ (Order o) = name(supplier(o));

CLASS OrderDetail ‘Строка заказа’;
order ‘Заказ’ = DATA Order (OrderDetail) NONULL DELETE;

Добавляем к строкам товары и количество:

product 'Товар' = DATA Product (OrderDetail);
nameProduct 'Товар' (OrderDetail d) = name(product(d));
quantity ‘Кол-во’ = DATA NUMERIC[14,3] (OrderDetail);

Переходим непосредственно к построению нужной нам формы редактирования заказа. Для начала добавляем на нее сам заказ и его строки:

FORM order 'Заказ'
OBJECTS o = Order PANEL
PROPERTIES(o) date, number, nameSupplier
OBJECTS d = OrderDetail
PROPERTIES(d) nameProduct, quantity, NEWDELETE
FILTERS order(d) = o

EDIT Order OBJECT o
;

Тем самым мы получаем стандартный интерфейс по работе со строками заказа через добавление и удаление.

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

EXTEND FORM order
OBJECTS p = Product
PROPERTIES name(p) READONLY
FILTERS in(supplier(o), p) // фильтруем только доступные для заказа товары
;

Настраиваем дизайн, чтобы строки заказа и подбор отображались как вкладки одного контейнера:

DESIGN order {
OBJECTS {
NEW pane { // создаем контейнер после заголовка заказа
fill = 1// растягиваем во весь размер 
type = TABBED// контейнер, элементы
MOVE BOX(d); // перемещаем туда таблицу со строками заказов
MOVE BOX(p) { // перемещаем вновь добавленную таблицу с товарами
caption = 'Подбор';
}
}
}
}

Строим вспомогательные свойства для отображения и ввода пользователем количества в соответствующей колонке:

quantity 'Кол-во' (Order o, Product p) =
GROUP SUM quantity(OrderDetail d) BY order(d), product(d);
lastOrderDetail 'Последняя строка' (Order o, Product p) =
GROUP LAST OrderDetail d ORDER d BY order(d), product(d);

Первое свойство считает для заказа и товара его количество в этом документе. Второй находит последнюю строку (пользователь может ввести несколько строк с одним товаром).

Дальше создаем действие, которое будет обрабатывать ввод пользователем значения в соответствующую колонку во вкладке подбора:

changeQuantity 'Изменить кол-во' (Order o, Product p)  {
INPUT q = NUMERIC[14,3DO { // запрашиваем число
IF lastOrderDetail(o, p) THEN { // проверяем, есть ли хоть одна строка
IF q THEN // ввели число
quantity(OrderDetail d) <- q IF d = lastOrderDetail(o, p)
WHERE order(d) = o AND product(d) = p; // записываем количество в последнюю строку с такой книгой
ELSE // сбросили число - удаляем строку
DELETE OrderDetail d WHERE order(d) = o AND product(d) == p;
ELSE
IF q THEN
NEW d = OrderDetail { // создаем новую строку
order(d) <- o;
product(d) <- p;
quantity(d) <- q;
}
}
}

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

EXTEND FORM order
PROPERTIES(o, p) quantity ON CHANGE changeQuantity(o, p)
;

Осталось только нарисовать форму со списком заказов и добавить ее в навигатор:

FORM orders 'Заказы'
OBJECTS o = Order
PROPERTIES(o) READONLY date, number
PROPERTIES(o) NEWSESSION NEWEDITDELETE
;
NAVIGATOR {
NEW orders;
}

Результирующая форма будет выглядеть приблизительно вот так:

image

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

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

image

Посмотрим как этот функционал реализован в других бизнес-приложениях.

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

image

В этом диалоге подбирать товар можно следующим образом:

image

В этом механизме есть, как минимум, два неудобства.

Во-первых, для того, чтобы добавить количество товара в документ (а чаще всего пользователь уже знает количество, которое хочет добавить), нужно сначала дважды кликнуть на товар, добавив его в строки, а затем уже в строках менять количество. При этом в самом списке товаров пользователь не видит добавлен ли уже этот товар и с каким количеством. Кроме того, такая схема “сжирает” дополнительное место, так как приходится выводить две таблицы на один экран вместо одной.

Во-вторых, после того как были добавлены строки непосредственно в документ, если нажать повторно кнопку Подобрать, то подбор начнется “с чистого листа”. В нем не появится никаких строк в нижней таблице, и в момент повторного подбора не будет понятно, что уже есть в документе, а чего нет. Также это нарушает одно из важных правил при построении интерфейса: если пользователь ошибся, то ему нужно дать возможность вернутся назад и исправить ошибку. Здесь же получается, что он не сможет вернутся в подбор товара имея те же данные, что и до нажатия кнопки добавления в документ.

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

Microsoft Dynamics 365

В качестве решения я взял Retail (только в нем я нашла похожий функционал из доступных на https://trials.dynamics.com). В нем вызвать подбор можно на форме Purchase orders двумя способами:

image

Первая кнопка реализована по схеме 1С (хотя там гораздо меньше колонок), поэтому на ней останавливаться не буду.

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

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

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

image

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

Заключение

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

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

Такую схему можно реализовать, например, в том же React, используя в качестве состояния текущие строки документа и отображая его двумя компонентами в двух разных вкладках. Но следует помнить, что записей в таблице с товарами может быть очень много, и придется реализовывать так называемый “infinite scroll”. Кроме того, желательно добавить пользователю возможность сортировать и фильтровать товары в этом списке (причем это надо делать на сервере, а не на клиенте, чтобы не тянуть туда лишние данные). Все это становится достаточно нетривиальной задачей для разработчика.

В платформе lsFusion этот функционал реализуется из коробки и требует лишь описанный выше код. Попробовать как это работает, а при желании и модифицировать код, можно онлайн на соответствующей странице. Вот исходный код целиком, который можно вставить на вкладку Платформа и затем нажав Play:

Исходный код

CLASS Product 'Товар';
name 'Наименование' = DATA STRING[50] (Product);
FORM product ‘Товар’
OBJECTS p = Product PANEL
PROPERTIES(p) name

EDIT Product OBJECT p // эта форма будет использоваться для редактирования товара
;

FORM products ‘Товары’
OBJECTS p = Product
PROPERTIES(p) READONLY name
PROPERTIES(p) NEWSESSION NEWEDITDELETE

LIST Product OBJECT p // эта форма будет использоваться, когда нужно будет выбрать товар из списка
;

NAVIGATOR {
NEW products;
}

CLASS Supplier ‘Поставщик’;
name ‘Наименование’ = DATA STRING[50] (Supplier);

in ‘Вкл’ = DATA BOOLEAN (Supplier, Product); // будет TRUE, если у поставщика разрешено закупать этот товар 

FORM supplier ‘Поставщик’
OBJECTS s = Supplier PANEL
PROPERTIES(s) name

OBJECTS p = Product // на форму поставщика добавляем список всех товаров
PROPERTIES in(s, p), name(p) READONLY // даем возможность проставлять галочку для нужных

EDIT Supplier OBJECT s
;

FORM suppliers ‘Поставщики’
OBJECTS s = Supplier
PROPERTIES(s) READONLY name
PROPERTIES(s) NEWSESSION NEWEDITDELETE

LIST Supplier OBJECT s
;

NAVIGATOR {
NEW suppliers;
}

CLASS Order ‘Заказ’;
date ‘Дата’ = DATA DATE (Order);
number ‘Номер’ = DATA INTEGER (Order);

supplier ‘Поставщик’ = DATA Supplier (Order);
nameSupplier ‘Поставщик’ (Order o) = name(supplier(o));

CLASS OrderDetail ‘Строка заказа’;
order ‘Заказ’ = DATA Order (OrderDetail) NONULL DELETE;

product ‘Товар’ = DATA Product (OrderDetail);
nameProduct ‘Товар’ (OrderDetail d) = name(product(d));

quantity ‘Кол-во’ = DATA NUMERIC[14,3] (OrderDetail);

FORM order ‘Заказ’
OBJECTS o = Order PANEL
PROPERTIES(o) date, number, nameSupplier

OBJECTS d = OrderDetail
PROPERTIES(d) nameProduct, quantity, NEWDELETE
FILTERS order(d) = o

EDIT Order OBJECT o
;

EXTEND FORM order
OBJECTS p = Product
PROPERTIES name(p) READONLY
FILTERS in(supplier(o), p) // фильтруем только доступные для заказа товары
;

DESIGN order {
OBJECTS {
NEW pane { // создаем контейнер после заголовка заказа
fill = 1// растягиваем во весь размер 
type = TABBED// контейнер, элементы
MOVE BOX(d); // перемещаем туда таблицу со строками заказов
MOVE BOX(p) { // перемещаем вновь добавленную таблицу с товарами
caption = ‘Подбор’;
}
}
}
}

quantity ‘Кол-во’ (Order o, Product p) =
GROUP SUM quantity(OrderDetail d) BY order(d), product(d);
lastOrderDetail ‘Последняя строка’ (Order o, Product p) =
GROUP LAST OrderDetail d ORDER d BY order(d), product(d);

changeQuantity ‘Изменить кол-во’ (Order o, Product p)  {
INPUT q = NUMERIC[14,3DO { // запрашиваем число
IF lastOrderDetail(o, p) THEN { // проверяем, есть ли хоть одна строка
IF q THEN // ввели число
quantity(OrderDetail d) <- q IF d = lastOrderDetail(o, p)
WHERE order(d) = o AND product(d) = p; // записываем количество в последнюю строку с такой книгой
ELSE // сбросили число – удаляем строку
DELETE OrderDetail d WHERE order(d) = o AND product(d) == p;
ELSE
IF q THEN
NEW d = OrderDetail { // создаем новую строку
order(d) <- o;
product(d) <- p;
quantity(d) <- q;
}
}
}

EXTEND FORM order
PROPERTIES(o, p) quantity ON CHANGE changeQuantity(o, p)
;

FORM orders ‘Заказы’
OBJECTS o = Order
PROPERTIES(o) READONLY date, number
PROPERTIES(o) NEWSESSION NEWEDITDELETE
;

NAVIGATOR {
NEW orders;
}

Добавить комментарий