По долгу работы в последние несколько месяцев мне пришлось столкнуться с разработкой инфраструктуры CRM для одного издательского дома. Руководство компании выбрало именно десктопную версию CRM, т.к. на тот момент web версия BMPonline была достаточно сырой. До начала разработки были проработаны и согласованы основные сущности, специфические для издательства, которые не имели аналогов из коробки (такие как «Издания», «Форматы», «Номера», для рекламы в журналах «Заказы», «Размещения» и т.д., тесно взаимодействующие между собой). Но каково было мое удивление, когда, приступив к разработке я не нашел адекватной документации для разработчиков, а точнее и вовсе ничего не нашел. Все что удалось откопать это ответы на вопросы и блоги самих разработчиков CRM, на форуме. Скудную документацию можно найти на terrasoft.ru/sdk/, однако в самом начале знакомства с системой мне это не очень то и помогло. Поэтому я потратил достаточно много времени, чтобы понять, что и как работает.
Итак, всю систему можно разделить на разделы (Контакты, Контрагенты, Банки и детали к этим разделам, например Адреса, Договоры, Карьера для Контактов, Бренды, Платежные реквизиты для Контрагентов и т.д.)
Вот пример раздела «Контакты» и детали «Адреса» Хочется отметить, что разделы создаются с помощью специальной утилиты.
wnd=wnd_CreateNewWorkspace
Должно получиться что-то вроде этого:
"C:Program Files (x86)Terrasoft PressBinTSClient.exe" wnd=wnd_CreateNewWorkspace
Кстати, таким же образом с помощью другого ключа можно включать профайлер запросов к БД, который бывает незаменим при поиске ошибок:
"C:Program Files (x86)Terrasoft PressBinTSClient.exe" /profiler
Собственно самое важное для разработчика это как раз добавление, редактирование и структуризация данных, проработка связей всех сущностей между собой. Поэтому создадим раздел (или также Workspace) MyWorkspace и наладим добавление данных.
После чего надо добавить только что созданный раздел на рабочее место:
Файл -> Настройки -> Рабочие места
После этого мы можем открыть Terrasoft Administrator и найти все сервисы, необходимые для работы нашего раздела (воркспейса).
Найти его можно в дереве сервисов:
Custom -> Workspaces -> myworkspace -> General -> Main Grid
Теперь коротко о сервисах, из которых состоит наш воркспейс.
- tbl_myworkspace (tbl_ = Table) Таблица с описанием полей, индексов и связей;
- sq_myworkspace (sq_ = Select Query) Запрос, который достает данные из таблицы (или нескольких таблиц с использованием JOIN’ов);
- ds_myworkspace (ds_ = Data Set) Набор данных, позволяет добавлять, удалять или изменять данные в таблице;
- wnd_myworkspaceEdit (wnd_ = Window) Окно редактирования, с помощью которого пользователь добавляет данные в данный раздел;
- wnd_myworkspaceGridArea Это грид, в котором отображаются все наши записи из данного раздела;
- wnd_myworkspaceWorkspace Главное окно раздела, которое содержит как основной грид, так детали, связанные с ним. Также, при необходимости здесь можно добавлять новые детали (получается связь один-ко-многим, т.е. одна запись грида может иметь несколько записей в детали, как я писал раньше «Контакт» может иметь несколько адресов и т.д.);
Добавление данных
Попробуем создать раздел с информацией о сотрудниках. Добавим несколько новых полей:
- Должность
- Зарплата
Сначала необходимо создать все эти поля в таблице tbl_myworkspace. Ни каких сложностей возникнуть не должно. После сохранения поля обновятся в БД которую вы используете (у меня Microsoft SQL). Здесь же мы можем добавить индексы по полям или связи при необходимости.
Теперь надо обновить запрос sq_myworkspace, который отвечает за выборку данных из этой таблицы.
Добавляем необходимые поля и сохраняем. При желании можно просмотреть SQL, запрос нажав ctrl + P.
В конце добавляем все эти поля в датасет ds_myworkspace, чтобы можно было работать с данными через специальный объект.
Тоже самое можно проделать гораздо проще, надо найти справочник нашего раздела (myworkspace) и изменить структуру полей:
Инструменты -> Справочники
Теперь, когда мы добавили необходимые поля, можно отобразить их на форме добавления/редактирования данных wnd_myworkspaceEdit.
Из множества доступных элементов, выбираем TextDataControl. В свойствах необходимо указать следующие параметры:
- DatasetLink — обычно он всего один на форме dlData (находится на вкладке «Невизуальные»);
- DataFieldName – выбираем поле, которое мы хотим отобразить (если наших добавленных полей не видно, попробуйте закрыть и заново открыть окно wnd_myworkspaceEdit)
- Name – можно оставить стандартное название, но лучше использовать определенный стиль для таких элементов (например edtNameOfField).
Для зарплаты выбираем IntegerDataControl и проделываем все то же самое. Теперь мы можем открыть Terrasoft Client и добавить запись, используя окно редактирования данных раздела. Запись без проблем добавляется, но в гриде не отображаются наши пользовательские поля. Почему? Да потому что мы не добавили их в этот самый грид. Поэтому открываем wnd_myworkspaceGridArea, находим элемент grdData -> gdvData, щелкаем правой кнопкой мыши, выбираем пункт «Определить колонки» и добавляем наши поля. После чего они станут видны в гриде в клиенте (возможно вам понадобится добавить их, нажав плюсик в правом верхнем углу грида, там вы можете добавлять или наоборот прятать необходимые колонки).
Выпадающие списки
Довольно часто приходится иметь дело с выпадающими списками. Гораздо удобнее внести, например, список должностей один раз в справочник, а потом просто выбирать название должности из этого списка. Это гораздо удобнее и правильнее с точки зрения проектирования БД.
Предположим, мы уже создали справочник со списком должностей MyWorkspaceAppointments. Создаем поле AppointmentID в таблице tbl_myworkspace, где будет храниться ID должности. Добавляем его в sq_myworkspace, и также нам надо будет делать выборку поля «Название должности», которое будет отображаться в выпадающем списке. Поэтому делаем JOIN с таблицей должностей по полю AppointmentID. И добавляем в блок SELECT это поле. Извлекаем его как AppointmentName.
Теперь добавляем поле «Поле справочника» в датасет. Выбираем колонку AppointmentID, в источнике данных справочника выбираем датасет справочника должностей, а в поле «Колонка для отображения» выбираем поле AppointmentName, выбранное нами ранее. Внизу ставим галочку «Отображать как выпадающий список в карточках». На окно добавляем элемент LookupDataControl и выбираем в DataFieldName. Все, можно проверять.
Собственно, это и есть самые основы на которых построена система Terrasoft (по моему личному мнению).
Работа с набором данных Dataset
Как я уже говорил, работа с БД происходит через набор данных dataset. Получить текущий dataset для окна, в котором мы работаем, можно через DataLink:
var Dataset = dlData.Dataset;
Получить любой другой набор данных можно также по его названию:
var TestDataset = Services.GetNewItemByUSI('ds_Test');
Добавление записи в коде
TestDataset.Append();
TestDataset.Values('Name') = 'Василий Иванов';
TestDataset.Values('Salary') = 30000;
TestDataset.Post();
ID – уникальный идентификатор добавлять не надо, это произойдет автоматически.
Редактирование записи
Для редактирования конкретной записи применяем фильтр (далее фильтры будут более подробно рассмотрены) к набору данных:
ApplyDatasetFilter(TestDataset, 'ID', '{e2ad6e6f-481a-40d0-abb6-50e4b19a43d6}', true);
TestDataset.Open(); // открываем набор
if(!TestDataset.IsEmptyPage) {
TestDataset.Edit();
TestDataset.Values('Salary') = 35000; // изменяем нужные поля
TestDataset.Post(); // сохраняем
}
TestDataset.Close(); // закрываем по завершении работы
Если необходимо отредактировать более одной записи:
TestDataset.Open();
while (!TestDataset.IsEOF) {
TestDataset.Edit();
TestDataset.Values('Salary') = 40000;
TestDataset.Post();
TestDataset.GotoNext(); // переходим к следующей записи
}
TestDataset.Close();
Удаление
// не забывайте применять соответствующие фильтры, чтобы не удалить все записи
TestDataset.Open();
while(!TestDataset.IsEOF) {
TestDataset.Delete();
TestDataset.Open();
}
TestDataset.Close();
Посчитать количество записей в наборе можно с помощью свойства RecordsCount.
var Records = TestDataset.RecordsCount;
Select Query и Dataset: пользовательские фильтры
Получаемые с помощью датасетов данные, можно фильтровать с помощью пользовательских фильтров. Они задаются в сервисах sq_. В нашем случае это sq_myworkspace. По умолчанию будет создан фильтр по ID, который сразу можно использовать в коде.
Применяется обычный фильтр с помощью функции ApplyDatasetFilter:
ApplyDatasetFilter(Dataset, 'ID', id, true);
- Dataset – датасет, к которому надо применить фильтр
- 'ID' – название фильтра в блоке WHERE
- id – идентификатор по которому будет производится поиск
- true – данный параметр говорит о том, что фильтр надо включить
Создадим простой фильтр по полю «Зарплата»:
1) создаем параметр, который будет передаваться в фильтр (SalarySum);
2) в блок WHERE добавляем «Фильтр сравнения», указываем название такое же, как и у параметра (SalarySum). Выбираем поле (tbl_myworkspace.Salary) для сравнения с параметром, который мы будем передавать из функции ApplyDatasetFilter.
Теперь можем применить его в коде и найти всех сотрудников с зарплатой 30000.
var ds = Services.GetNewItemByUSI('ds_myworkspace');
ApplyDatasetFilter(ds, 'SalarySum', 30000, true);
ds.Open();
Log.Write(2, ds.ValAsStr('Name'));
ds.Close();
Можно также применять несколько фильтров. Например, найти всех директоров с зарплатой больше 30000 и т.д.
События dataset
Также хочется отметить наиболее используемые события dataset:
OnDatasetAfterOpen — возникает после открытия набора данных;
OnDatasetAfterPositionChange — возникает после перехода на другую запись (бывает полезно при отслеживании смены выделенной записи в гриде);
OnDatasetAfterPost — возникает после добавления/изменения записи (бывает полезно, когда надо изменить связанные данные в другой таблице);
OnDatasetBeforePost — возникает перед добавлением/изменением записи (бывает полезно для пользовательской проверки данных, перед сохранением). Также в событие передается параметр DoPost, если по какой-то причине нужно отменить добавление (например, данные не прошли пользовательскую проверку) просто делаем:
DoPost.Value = false;
OnDatasetDataChange — возникает при изменении данных датасета. Например нужна какая-то логика при изменении поля должность из нашего выпадающего списка.
function dlDataOnDatasetDataChange(DataField) {
var FieldName = DataField.Name;
switch(FieldName) {
case 'EditionID':
// организуем необходимую логики при каждом изменении поля
break;
}
}
OnDatasetBeforeDelete — возникает перед удалением записи (например можно повесить пользовательскую проверку данных, и при необходимости отменить удалить используя параметр DoDelete).
DoDelete.Value = false;
OnDatasetAfterDelete — возникает после удаления записи (например можно проверить, обновить или удалить связанные данные с данной записью).
События окна, обращение к элементам
Иногда нужно прописать определенную логику при открытии окна, сделать это можно в событии OnPrepare.
По умолчанию у окна, в событии, будет прописана функция wnd_BaseDBEditOnPrepare, которая должна вызываться для правильной инициализации каждого окна. Но нам необходимо добавить свою логику, поэтому переименуем функцию в wnd_MyWorkspaceDBEditOnPrepare и дважды нажмем на нее. Откроется (или создастся новый) скрипт окна.
function wnd_MyWorkspaceDBEditOnPrepare(Window) {
scr_BaseDBEdit.wnd_BaseDBEditOnPrepare(Window);
}
Не забываем вызвать данную функцию для корректной инициализации и добавляем свою логику. Предположим, что нам надо высчитывать подоходный налог с зарплаты сотрудника. Добавляем в окно новый элемент NumericEdit.
Определяем события для поля Salary OnKeyDown и OnKeyUp, эти события буду срабатывать при вводе данных или при стирании. Напишем функцию, которая будет обновлять при каждом изменении поля Salary поле Tax (налог). Должно получиться что-то вроде этого:
function edtSalaryOnKeyDown(Control, Key, Shift) {
calculateTax();
}
function edtSalaryOnKeyUp(Control, Key, Shift) {
calculateTax();
}
function calculateTax() {
var Dataset = dlData.Dataset; // получаем набор данных для текущего окна
edtTax.Value = dlData.Dataset.ValAsInt('Salary') * 0.13;
}
Все отлично работает. Однако при открытии окна поле «Налог» не будет заполнено. Это можно исправить с помощью события окна OnPrepare.
function wnd_MyWorkspaceDBEditOnPrepare(Window) {
scr_BaseDBEdit.wnd_BaseDBEditOnPrepare(Window);
calculateTax(); // просчитываем и заполняем поле налог
}
В процессе работы появились некоторые наработки, которые мне не раз пригодились.
//изменение цвета и текста в ячейке у DataGrid
function grdDataOnGetRowDrawInfo(DataGrid, Color, TextColor, ImageName, Font) {
if (/* условие */) {
Color.Value= clRed; // цвет ячейки
TextColor.Value = clYellow; // цвет текста
}
}
//фильтрация значений выпадающего списка по определенном полю
function edtContactOnPrepareSelectWindow(LookupDataControl, SelectWindow) {
ApplyDatasetFilter(LookupDataControl.DataField.LookupDataset, 'IsActive', true, true);
}
// сбрасывает выпадающий список, и при следующем обращении опять вызовет событие OnPrepareSelectWindow< (например если данные в списке зависят от другого поля, которое изменилось, и необходимо обновить фильтр)
edtContact.UnprepareDropDownList();
// открытие любого окна
var EditWindowUSI = 'wnd_OpportunityEdit';
var Attributes = GetNewDictionary();
Attributes.Add('RecordID', GUID_NULL); // если не указываем RecordID то открывается окно для добавления данных, если указываем, то запись с ID = RecordID открывается для редактирование
var DefaultValues = GetNewDictionary(); // значения по умолчанию
DefaultValues.Add('CustomerID', AccountID);
ShowEditWindowEx(EditWindowUSI, Attributes, DefaultValues);
// что-то вроде глобальной области видимости, можно таким образом передавать данные из одного окна в другое
Connector.Attributes('DatasetToSave') = object;
// обращение к родительскому окну (также можно обратиться к элементам на этом окне)
var ParentWindow = Self.Attributes('NotifyObject').ParentContainer.ParentWindow.ComponentsByName('edtPriceListsEditionID');
// обращение к элементу родительского окна (текущее окно находится в контейнере)
var ParentWindow = Self.ParentContainer.ParentWindow.ComponentsByName('edtName’);
// обращение к одному гриду из другого через workspace
var grd = Self.ParentContainer.ParentWindow.ComponentsByName('wndGridData').Window.ComponentsByName('grdData');
// проверка, является ли данная запись новой, или она открыта на редактирование
Window.Attributes('IsNewRecordAppend')
// получить выделенные записи грида
var ArrayIDs = GetArrayByCollection(grdData.SelectedIDs);
// вывод каких либо данных в консоль
Log.Write(2, 'Text');
// например при отладке, удобно использовать исключение
try {
// ваш код
} catch(e) {
Log.Write(2, e.message);
}
Собственно, я описал все ключевые моменты по работе с системой. Думаю, что для начала разработки под данную CRM (актуально для версии 3.4, на счет других не знаю) этого будет вполне достаточно.
Автор: vkitki