Jump to content
SpravkaCRM.ru - Ваш справочник по CRM

Search the Community

Showing results for tags 'suitecrm'.



More search options

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


Categories

  • Перевод официального мануала SuiteCRM
  • Обучающие статьи о SuiteCRM
    • Для программиста
  • Доводим напильником SuiteCRM
  • Расширения для SuiteCRM
    • Патчи с исправлениями ошибок в SuiteCRM
  • Программист за работой
  • Диалоги о SuiteCRM

Categories

  • Records
  • CRM-система для застройщика
    • Manual
  • CRM-система для кредитного брокера
  • CRM for Programmer
  • CRM-система для салонов красоты
    • Руководство

Forums

  • SugarCRM/SuiteCRM
    • Все вопросы пока сюда
    • Заметки по ходу разработки
    • Нам пишут
    • Работа
  • CRMHosting.io
    • SuiteCRM последней версии
    • CRM для продажи пиццы/суши/ролл
    • CRM для Застройщика
    • CRM для Кредитного брокера
    • CRM для Салонов красоты
  • Другие CRM-системы
    • AmoCRM
    • Bitrix24
    • BPM Online
    • Прочие CRM
  • Всего по немногу
    • Программисту
    • Arduino
    • Без систематики

Categories

  • Модули SuiteCRM/SuiteCRM
  • Manuals

Find results in...

Find results that contain...


Date Created

  • Start

    End


Last Updated

  • Start

    End


Filter by number of...

Joined

  • Start

    End


Group


Found 74 results

  1. Всем добрый день! Хочу рассказать об одной интересной работе, которую делали последнюю неделю и которую закрыли вот прям буквально сегодня. К нам обратилась некая компания, которая попросила показать демо-стенд реализации интеграции Camunda с CRM-системой на базе SuiteCRM. В SuiteCRM есть свой собственный модуль "Процессы". Но его наглядность - не самое его сильное качество. Да и функциональность конечно весьма не плоха, но очень далеко до "отличная". По этому компания приняла решение в качестве BPM-платформы выбрать стороннее решение в виде Camunda. Вот что пишут про Camunda: Погуглив по Camunda ничего толком не нашел в русском сегменте интернета. Какие то общие описания. В английском секторе чуть больше, и есть более менее не плохое описание и REST API. Так же очень помогла наработка от ребят из hardsoft321: при помощи этого класса получилось достаточно быстро "поднять протокол" - использовать API Camunda. Постановка задачи Итак, далее опишу что нужно было сделать, что было сделано и как это все работает. По задумке надо было организовать: Прием заявки с сайта (сработала форма) Заявка назначается на ответственного менеджера Менеджер обрабатывает новую заявку проверяя хватает ли данных для оказания услуги Если данных достаточно, то менеджер передает заявку в другой отдел (в службу безопасности) В службе безопасности заявка назначается на сотрудника, он проверяет заявку Если проверка выявила проблемы, то сотрудник службы безопасности возвращает заявку обратно менеджеру Если проверка проблем не выявила, то сотрудник службы безопасности отправляет заявку на следующий этап - в юридический отдел для предоставления непосредственно самой услуги В юридическом отделе заявка назначается на сотрудника этого отдела Сотрудник оказывает услугу по заявке Если по заявке были выявлены проблемы - сотрудник юридического отдела должен иметь возможность вернуть заявку обратно менеджеру После того, как услуга по заявка была оказана, сотрудник юридического отдела возвращает заявку обратно менеджеру, чтобы тот уведомил клиента о оказании ему услуги. После уведомления клиента заявка уходит в архив Если графически, то бизнес-процесс должен выглядеть примерно так: В связке SuiteCRM + Camunda предполагалось, что все логические действия по движению заявки по отделам и статусам будет выполнять Camunda, а SuiteCRM будет выступать в виде некоего UI, в котором будут работать сотрудники: видеть список своих заявок в работе, иметь возможность отправлять заявки дальше по цепочке. Чтобы реализовать подобное взаимодействие потребуется двухсторонняя синхронизация: В момент создания заявки, а также в моменты, когда сотрудники готовы отдать заявку дальше по цепочке, SuiteCRM должна сообщать о этом Camunda, чтобы та уже определяла дальнейшее действие. В момент, когда Camunda определяет что же дальше должно происходить с заявкой, она должна свое решение передавать обратно в SuiteCRM, чтобы та соответствующим образом меня статус заявки, ответственного по заявке и прочие действия, характерные для прохождения той или иной стадии. С первым пунктом интеграции было все более или менее понятно: Необходимо в Camunda разработать и загрузить процесс, который будет описывать всю логику. При помощи Camunda Rest API мы будем должны стартовать этот разработанный процесс в момент прихода новой заявки из формы на сайте В процессе работы над заявкой, когда сотрудники отмечают свою работу выполненной, мы должны передавать в процесс информацию о этом, чтобы процесс двигал заявку дальше С вторым пунктом было больше вопросов: Как передать из Camunda в SuiteCRM информацию о очередном этапе? Умеет ли Camunda отправлять Webhooks, или используется какая то другая технология? Есть ли в Camunda встроенные возможности такого взаимодействия, или необходимо привлекать разработчиков чтобы реализовать подобные возможности? Поверхностное гугление не давало четкого ответа на эти вопросы. Решение задачи Со стороны SuiteCRM: За основу была взята CRM-система для салонов красоты и парикмахерских. В ней мы переделали модуль "Посещения" на "Заявки": Для списка предоставляемых услуг подошел модуль "Прейскурант": Клиенты так и остались в карточках модуля "Клиенты": Для тестирования получения данных из формы при помощи модуля "Компании" была сгенерирована форма тестовой заявки: Данные на нее попадали прямиком в CRM-систему. Первоначально весь бизнес-процесс мы настроили силами самой CRM при помощи модуля "Процессы": Таким образом Заявка, при попадании в CRM-систему, автоматически создавала как запись с Клиентом, так и с Заявкой. В заявке назначался ответственный менеджер из списка сотрудников, находящихся в роли "Менеджер". Кстати были созданы 3 роли: Менеджер, СБ и Юрист: Сотрудники, имеющие ту или иную роль, назначаются Заявке в соответствующий момент времени. Движение заявки по статусам осуществляется при помощи кнопок, находящихся вверху карточки Заявки. Передача заявки в службу безопасности: Заявка назначается в работу сотруднику из службы безопасности. По результатам работы он может или вернуть заявку обратно менеджеру с своими комментариями, или отправить заявку дальше по цепочке: Если сотрудник службы безопасности подтвердил заявку, то она уходит сотруднику с ролью Юрист. Юрист также может вернуть обратно в работу заявку менеджеру (если в ней чего-либо не хватает), или выполнить свою работу и отметить заявку выполненной: После того, как Юрист выполнит свою работу и отметит заявку выполненной, она возвращается Менеджеру в статусе "Уведомление клиента" и менеджеру необходимо уведомить клиента о выполнении его заявки, после чего менеджер может закрыть заявку: Со стороны Camunda: Сначала мы развернули демо-стенд с Camunda. Самый быстрый и простой способ это сделать оказалось загрузить на сервер Docker-образ с официального репозитория: # docker pull camunda/camunda-bpm-platform После этого появляется возможность развернуть уже настроенную систему с предустановленной Camunda на борту. Я воспользовался системой разворачивания виртуальных хостов, которая работает на нашем сайте crmhosting.ru, но вы можете самостоятельно посмотреть про работу с docker. Если будет совсем не понятно, то пишите под статьей, дополню подобной информацией. Оставлю тут ссылку на демо-стенд: http://camunda.crmhosting.ru/camunda/ логин/пароль = demo/demo Путем изучения Camunda пришло понимание, что данное ПО умеет обрабатывать диаграммы BPMN, но не умеет их создавать. Для создания диаграмм используется распространяемый Modeler от Camunda: В изучении возможностей моделера и Camunda в том числе очень помог процесс, присутствующий в Camunda по умолчанию: обработка счетов: Настройка взаимодействия: Задачей нашего демо-стенда было показать принципиальную возможность взаимодействия между SuiteCRM и Camunda. Для этого мы решили не реализовывать весь процесс на связке Camunda+SuiteCRM, а добавить 1-2 звена для отображения того, как SuiteCRM отправляет данные в Camunda и как Camunda уведомляет SuiteCRM. В Modeler от Camunda мы разработали простенький процесс, задачей которого было стартовать при срабатывании формы и назначить заявке ответственного менеджера по заданному алгоритму, а затем вернуть назначенного менеджера в SuiteCRM, чтобы та, в свою очередь, назначила этого ответственного за Заявкой. Таким образом мы убрали из наших ранее настроенных бизнес-процессов определение ответственного силами SuiteCRM и перевели этот блок на плечи Camunda. Вот сам получившийся процесс в Camunda: Для формирования логики блока "Определение ответственного менеджера" существует множество разных способов. Мы выбрали указание пользователя при помощи Модели принятия решений (DMN). Настройки блока "Определение ответственного менеджера": Сама модель идет в виде отдельного файла *.dmn В ней мы явно указываем, что не зависимо не от чего результат будет равен строке "manager1": Таким образом при старте процесса Camunda назначает ответственным сотрудником юзера "manager1" и уходит в блок "Утверждение введенных данных менеджером" (этот блок никак не был автоматизирован). Чтобы стартовать процесс Camunda из SuiteCRM мы создали в SuiteCRM Hook для события before_save, который через API стартует наш процесс. В настройках LogicHooks: $hook_array['before_save'][] = Array( 10, 'Стартуем процесс', 'modules/Opportunities/Hooks.php', 'OpportunitiesLogicHooks', 'camundaStartProcess' ); Сам хук: public function camundaStartProcess($bean, $event, $arguments) { global $sugar_config; if(isset($GLOBALS['OpportunityCamundaStartProcess'][$bean->id])) return; print_array("*** camundaStartProcess - start ***",0,1); require_once 'custom/include/PestJSON.php'; require_once 'modules/CamundaProcesses/SugarCamunda.php'; print_array('$bean->id = ' . $bean->id,0,1); print_array('$bean->new_with_id = ' . var_export($bean->new_with_id,1),0,1); print_array('$bean->fetched_row = ' . var_export($bean->fetched_row,1),0,1); if(empty($bean->camunda_id)) { // Создание новой Заявки print_array("* Создание новой Заявки",0,1); if(empty($bean->id)) { $bean->id = create_guid(); $bean->new_with_id = true; } if($bean->lead_source == 'Form') { // Если источник Заявки = Форма print_array("* источник Заявки = Форма",0,1); // Подключаемся к Camunda $camunda = SugarCamunda::getJsonClient(); // Получаем список процессов $processes = $camunda->get("/process-definition"); if(isset($sugar_config['camunda_new_opportunity_process_key'])) { // Если в конфигурации указан процесс для входящего запроса с формы print_array("* в конфигурации указан процесс для входящего запроса с формы = " . $sugar_config['camunda_new_opportunity_process_key'],0,1); foreach ($processes as $processInfo) { if($processInfo['key'] == $sugar_config['camunda_new_opportunity_process_key']) { // Нашли процесс, указанный в конфиге print_array("* Нашли процесс, указанный в конфиге",0,1); // Стартуем процесс print_array("* Стартуем процесс",0,1); $GLOBALS['OpportunityCamundaStartProcess'][$bean->id] = true; $resultStart = $camunda->post("/process-definition/{$processInfo['id']}/start",[ 'opportunity_id' => $bean->id, ]); if(isset($resultStart['id'])) { // Записываем ID процесса в Заявку print_array("* Записываем ID процесса в Заявку: " . $resultStart['id'],0,1); print_array("** resultStart: " . var_export($resultStart,1),0,1); $bean->camunda_id = $resultStart['id']; } } } } } } } В Camunda может быть описано множество разных процессов. В конфигурационной переменной SuiteCRM мы указали название нашего нужного процесса. $sugar_config['camunda_new_opportunity_process_key'] = 'testSuite'; Обращаясь через API Camunda к списку всех доступных процессов мы их перебираем, и как только находим процесс с нужным названием, то сразу запускаем его через вызов метода "/process-definition/{$processInfo['id']}/start" В результате вызова мы получаем ID запущенного процесса (именно не ID схемы процесса, а ID именно текущего старта), и записываем его в Заявку в поле camunda_id: $bean->camunda_id = $resultStart['id']; Так как данное действие выполняется во время before_save, то данное поле естественным образом запишется в Заявку без каких-либо дополнительных действий с нашей стороны. Дальше начинается самая интересная и сложная часть, на которую ушло больше всего времени - мы заставляем Camunda отдать в SuiteCRM логин выбранного менеджера. После долгого гугления и попыток осознать предоставляемые Camunda возможности пришло понимание, что встроенных нативных средств в Camunda для передачи данных в внешнюю среду нет. Вроде "пичаль", но не совсем. У Camunda зато есть ряд других возможностей, подразумевающих программирование. И реализуются они при помощи Listeners в Modeler при редактировании свойств блока "Определение ответственного менеджера". Listeners - это возможность вставить свою логику в тот или иной блок диаграммы используя или специально разработанные Java-классы, или другие языки программирования. Существующего опыта программирования на Java нам не хватало для анализа существующих возможностей и разворачивания действующего демо-стенда в заданные сроки. Вроде как есть поддержка JavaScript, но попытки использовать XMLHttpRequest закончились неудачей: Camunda отказалась работать с этими функциями. Но на выручку пришел язык groovy: В этом языке нашлись конструкции, которые позволяют обращаться к сторонним ресурсам по HTTP и получать результаты. А в нашем случае на текущем этапе нам надо было всего лишь обратиться к SuiteCRM с ID текущего процесса и назначенным ответственным, чтобы передать в SuiteCRM эту информацию. И вот что в результате получилось: Таким образом скрипт на groove выполнялся ПОСЛЕ того, как ответственный менеджер был выбран, и отправлял эту информацию в SuiteCRM. SuiteCRM, в свою очередь, принимала этот запрос и обрабатывала его. Для приема был добавлен новый entryPoint = camunda В настройках entryPoint: $entry_point_registry['camunda'] = array( 'file' => 'modules/CamundaProcesses/inboundRequests.php', 'auth' => false ); Файл modules/CamundaProcesses/inboundRequests.php: <?php /** * Created by PhpStorm. * User: crmhosting * Date: 06.11.2018 * Time: 19:26 */ include_once "modules/Opportunities/Opportunity.php"; global $db; $json = new JSON(); if(isset($_REQUEST['action'])) { // Пришел запрос с действием print_array('*** inboundRequests - start ***',0,1); print_array($_REQUEST,0,1); // Формируем массив с подробностями $content = []; switch ($_REQUEST['action']) { case 'setAssignedUserInOpportunity': // Указан ответственный пользователь у Заявки if(isset($_REQUEST['user']) AND $_REQUEST['user'] != '') { $content['user'] = $_REQUEST['user']; } break; } $camundaID = isset($_REQUEST['camundaID']) ? $_REQUEST['camundaID'] : ''; $camundaIDArray =explode(":", $camundaID); if(isset($camundaIDArray[2])) $camundaID = $camundaIDArray[2]; // Записываем входящее действие в таблицу print_array('* Записываем входящее действие в таблицу',0,1); $sql = " INSERT INTO `camunda_inbound` SET `action` = '".$_REQUEST['action']."', `camundaID` = '{$camundaID}', `content` = '".base64_encode($json->encode($content))."' "; $db->query($sql, true); } Таким образом мы в этом entryPoint можем принимать из Camunda запросы и подготовили тем самым платформу для реагирования на самые разные запросы. Кстати, в процессе тестирования выявилась интересная особенность: изначально мы думал, что процесс взаимодействия и взаимных вызовов будет строиться следующим образом: Пришли данные из формы -> Стартовали процесс в Camunda -> Получили ID процесса из Camunda -> Сохранили заявку -> Пришел запрос из Camunda с указанием ответственного -> определили заявку -> указали ответственного в SuiteCRM -> Сохранили заявку Но все оказалось немного иначе. Camunda не хотела отдавать ID процесса, пока не пройдется по всем своим блокам. А так как блок с определением ответственного стартует обратный вызов SuiteCRM, то фактическая цепочка получалась такой: Пришли данные из формы -> Стартовали процесс в Camunda -> Пришел запрос из Camunda с указанием ответственного -> пытаемся определить заявку (ее еще нет) -> некуда указывать ответственного -> вызов из Camunda закончился в пустую -> Получили ID процесса из Camunda -> Сохранили заявку То есть из Camunda приходит обратный запрос с ответственным еще до того, как Camunda отдала ID процесса при создании заявки. И так, как ID заявки в SuiteCRM пока нет, то ничего нормально мы не сохранили и ничего не работало. Но выход нашелся в работе через вспомогательную таблицу `camunda_inbound`: в нее мы промежуточно сохраняем информацию когда приходит ответ от Camunda - в каком процессе какой ответственный был указан. Таким образом мы корректно завершаем Hook в заявке, получаем ID процесса и сохраняем его в Заявке. А хуком after_save мы лезем в таблицу `camunda_inbound` и обрабатываем накопившиеся там отбойки от Camunda: public function camundaCheckInboundProcess($bean, $event, $arguments) { include_once "modules/Opportunities/OpportunityCamundaHelper.php"; if(isset($GLOBALS['OpportunityCamundaStartProcess'][$bean->id])) { // Запускаем анализ всех пришедших активностей из Camunda OpportunityCamundaHelper::processAllCamundaInbound(); unset($GLOBALS['OpportunityCamundaStartProcess'][$bean->id]); } } class OpportunityCamundaHelper extends Opportunity { static function processAllCamundaInbound() { global $db; print_array("*** processAllCamundaInbound - start ***",0,1); // Получаем все записи $sql = "SELECT * FROM `camunda_inbound` ORDER BY `id` ASC"; $result = $db->query($sql, true); $json = new JSON(); while ($row = $db->fetchByAssoc($result)) { $content = $json->decode(base64_decode($row['content'])); switch ($row['action']) { case 'setAssignedUserInOpportunity': // Указан ответственный пользователь у Заявки print_array("* Пришел запрос на указание ответственного у Заявки",0,1); if(isset($row['camundaID']) AND $row['camundaID'] != '') { // Пришла информация о процессе // Ищем Заявки с таким ID процесса print_array("* camundaID указано ({$row['camundaID']})",0,1); $sql = "SELECT `id` FROM `opportunities` WHERE `deleted` = 0 AND `camunda_id` = '{$row['camundaID']}'"; print_array('$sql: ' . $sql,0,1); $opportunity_id = $db->getOne($sql, true); if($opportunity_id) { // Если ID Заявки найдено print_array("* ID Заявки найдено: " . $opportunity_id,0,1); $seedOpportunity = new Opportunity(); $seedOpportunity->retrieve($opportunity_id); if(!empty($seedOpportunity->id) AND empty($seedOpportunity->assigned_user_id)) { // Заявка корректно найдена print_array("* Заявка найдена: " . $seedOpportunity->name . ' (ID = '.$seedOpportunity->id.')',0,1); // Далее ищем ответсственного пользователя if(isset($content['user']) AND $content['user'] != '') { // Пользователь указан print_array("* Пользователь указан: " . $content['user'],0,1); $sql = "SELECT `id` FROM `users` WHERE `deleted` = 0 AND `user_name` = '{$content['user']}'"; $user_id = $db->getOne($sql, true); if($user_id) { // Если Пользователь найден print_array("* Пользователь найден: " . $user_id,0,1); $seedOpportunity->assigned_user_id = $user_id; $seedOpportunity->user_assigned_manager_id = $user_id; $seedOpportunity->save(); print_array("** Сохранили: " . $seedOpportunity->assigned_user_name,0,1); } } } } } break; } // Удаляем запись из таблицы $sql = "DELETE FROM `camunda_inbound` WHERE `id` = '{$row['id']}'"; print_array("Удаляем запись из таблицы: " . $sql,0,1); $db->query($sql, true); } } } Вот такое получилось решение. Могу предположить, что это не самое оптимальное решение. Если у кого то есть мысли как можно сделать лучше, или опыт чего то подобного, то буду рад если вы прокомментируете данную статью. Демо-стенд SuiteCRM: http://u1200.crmhosting.ru admin/admin manager1/Manager1 SB1/SB1 Yur1 / Yur1 Возможно эту сборку добавим в список сборок для автоматического разворачивания на нашем сервисе CRMHosting.ru (но это не точно )))) ) Презентация Ну и под конец размещаю видео с презентацией получившегося решения:
  2. Всем привет! Столкнулся с небольшой проблемой: SuiteCRM автоматически при отображении поля типа Text преобразует все URL в ссылки. То есть берет URL и "пихает" его в тег <A>. Классная штука на самом деле, удобно. Но мне надо было отключить её, так как я создал поле "source" => "non-db", в котором хочу сам формировать список ссылок, чтобы в карточке записи был набор ссылок типа такого: Ссылка 1 Ссылка 2 Ссылка 3 Именно так, нормальное название. А не: http://site.ru/page1.html http://site.ru/page2.html http://site.ru/page3.html Ведь удобнее видеть нормальное человеческое описание, кликнуть на него и попасть на желаемую страницу. По реализации: Добавил новое поле в модуль, в котором будет находится текст с списком ссылок: 'different_links' => array ( 'required' => false, 'name' => 'different_links', 'vname' => 'LBL_DIFFERENT_LINKS', 'type' => 'text', 'massupdate' => 0, 'no_default' => false, 'comments' => '', 'help' => '', 'importable' => 'true', 'duplicate_merge' => 'disabled', 'duplicate_merge_dom_value' => '0', 'audited' => true, 'reportable' => true, 'unified_search' => false, 'merge_filter' => 'disabled', 'source' => 'non-db', ), Добавил хук after_retrieve в custom/Extension/modules/lm_CRMSystems/Ext/LogicHooks/calculateFields.php, чтобы можно было наполнять поле текстом: if (!isset($hook_array) || !is_array($hook_array)) { $hook_array = array(); } if (!isset($hook_array['after_retrieve']) || !is_array($hook_array['after_retrieve'])) { $hook_array['after_retrieve'] = array(); } $hook_array['after_retrieve'][] = Array(10, 'Ссылка на карточку на сервере', 'custom/modules/lm_CRMSystems/hooks.php', 'lm_CRMSystemsHooks', 'calculateFields'); Добавляем функцию для хука в файле custom/modules/lm_CRMSystems/hooks.php: /** * Получение ссылки на карточку CRM-системы на ее Сервере * @param $bean * @param $event * @param $arguments */ public function calculateFields($bean, $event, $arguments) { global $db; global $sugar_config; // Ссылки $bean->different_links = ""; // Ссылка на карточку на другом сервере switch ($sugar_config['CRMSystems']['systemType']) { case 'base': // Мы находимся на Базовом сервере // Пытаемся сформировать ссылка на карточку на сервере с проектами // Получаем URL сервера if(!empty($bean->lm_crmservers_lm_crmsystemslm_crmservers_ida)) { $sql = "SELECT `url` FROM `lm_crmservers` WHERE `deleted` = 0 AND `id` = '{$bean->lm_crmservers_lm_crmsystemslm_crmservers_ida}'"; $url = $db->getOne($sql, true); $bean->different_links .= '<li><A href="'.$url . '/index.php?module=lm_CRMSystems&action=DetailView&record=' . $bean->id.'" target="_blank">Карта на сервере с проектом</A></li>'; } break; case 'projects': // Мы находимся на Сервере с проектами // Добавляем ссылку на карточку на Базовом сервере // Ищем базовый сервер $sql = "SELECT `url` FROM `lm_crmservers` WHERE `deleted` = 0 AND `type` = 'base'"; $url = $db->getOne($sql, true); $bean->different_links .= '<li><A href="'.$url . '/index.php?module=lm_CRMSystems&action=DetailView&record=' . $bean->id.'" target="_blank">Карта на базовом сервере</A></li>'; break; } if($bean->different_links != '') { $bean->different_links = '<ul>'.$bean->different_links.'</ul>'; } } И вот что у меня получилось: Получается, что я сформировал корректную HTML-ссылку, а SuiteCRM внутри нее нашла URL, и обрамила ее своей ссылкой. Вышло не очень. Полез в функцию-обработчик полей типа Text include/SugarFields/Fields/Text/SugarFieldText.php, и видим там следующее: class SugarFieldText extends SugarFieldBase { function getDetailViewSmarty($parentFieldArray, $vardef, $displayParams, $tabindex) { if(!isset($displayParams['nl2br'])){ $displayParams['nl2br'] = true; } if(!isset($displayParams['htmlescape']) && $vardef['editor'] != "html") { $displayParams['htmlescape'] = true; } if(!isset($displayParams['url2html'])) { $displayParams['url2html'] = true; } return parent::getDetailViewSmarty($parentFieldArray, $vardef, $displayParams, $tabindex); } Тут особо интересный блок: if(!isset($displayParams['url2html'])) { $displayParams['url2html'] = true; } То есть если при отображении поля использовать 'url2html' => false, то в теории оно не должно преобразовывать ссылки. Пробуем: в файле custom/modules/lm_CRMSystems/metadata/detailviewdefs.php описываем поле different_links: 11 => array ( 0 => 'lm_crmservers_lm_crmsystems_name', 1 => array( 'name' => 'different_links', 'label' => 'LBL_DIFFERENT_LINKS', 'displayParams' => array( 'url2html' => false, ), ), ), И вот что получилось после быстрого восстановления: То, что нужно!
  3. Всем привет! Столкнулся только что с ситуацией, когда в карточку Контрагента добавили поле с пользователем (связанная запись из Users), пытаемся в дашлете это поле использовать как фильтр, а оно не работает. Поле добавляли через файлы в /custom/Extensions/modules/Accounts/Ext/Vardefs/ В дашлете это выглядит примерно так: В карточке контрагента это же поле: После того, как в дашлете я выбираю это поле в виде фильтра, SuiteCRM формирует для выборки записей SQL-запрос примерно такого содержания: SELECT accounts.id , accounts.user_id2_c , LTRIM(RTRIM(CONCAT(IFNULL(jt0.first_name,''),' ',IFNULL(jt0.last_name,'')))) fixed_accountant_c , LTRIM(RTRIM(CONCAT(IFNULL(jt1.first_name,''),' ',IFNULL(jt1.last_name,'')))) fixed_accountant_c , jt1.created_by fixed_accountant_c_owner , 'Users' fixed_accountant_c_mod, accounts.assigned_user_id FROM accounts LEFT JOIN users jt0 ON accounts.user_id2_c = jt0.id AND jt0.deleted=0 LEFT JOIN users jt1 ON accounts.user_id2_c=jt1.id AND jt1.deleted=0 AND jt1.deleted=0 where (user_id2_c='d3c33565-3030-cebd-2db4-592308bd456b' ) AND accounts.deleted=0 Как мы видим ((accounts.id IN (''))) - совсем не то, что нам нужно. В результате анализа и поиска места, где это все собирается и как так получается был найден файл /include/Dashlets/DashletGeneric.php и в нем функция buildWhere() с таким участком: switch($widgetDef['type']) {// handle different types case 'date': case 'datetime': case 'datetimecombo': if(is_array($params) && !empty($params)) { if(!empty($params['date'])) $widgetDef['input_name0'] = $params['date']; $filter = 'queryFilter' . $params['type']; } else { $filter = 'queryFilter' . $params; } array_push($returnArray, $widgetClass->$filter($widgetDef, true)); break; case 'assigned_user_name': // This type runs through the SugarWidgetFieldname class, and needs a little extra help to make it through if ( ! isset($widgetDef['column_key']) ) { $widgetDef['column_key'] = $name; } // No break here, we want to run through the default handler case 'relate': if (isset($widgetDef['link']) && $this->seedBean->load_relationship($widgetDef['link'])) { $widgetLink = $widgetDef['link']; $widgetDef['module'] = $this->seedBean->$widgetLink->focus->module_name; $widgetDef['link'] = $this->seedBean->$widgetLink->getRelationshipObject()->name; } // No break - run through the default handler default: $widgetDef['input_name0'] = $params; if(is_array($params) && !empty($params)) { // handle array query array_push($returnArray, $widgetClass->queryFilterone_of($widgetDef, false)); } else { array_push($returnArray, $widgetClass->queryFilterStarts_With($widgetDef, true)); } $widgetDef['input_name0'] = $params; break; } Поле, по которому мы пытаемся в дашлете искать, создано при помощи такого массива: $dictionary['Account']['fields']['fixed_accountant_c'] = array ( 'required' => false, 'source' => 'non-db', 'name' => 'fixed_accountant_c', 'vname' => 'LBL_FIXED_ACCOUNTANT_C', 'type' => 'relate', 'massupdate' => 0, 'no_default' => false, 'comments' => '', 'help' => '', 'importable' => 'true', 'duplicate_merge' => 'disabled', 'duplicate_merge_dom_value' => '0', 'audited' => false, 'inline_edit' => true, 'reportable' => true, 'unified_search' => false, 'merge_filter' => 'disabled', 'len' => '255', 'size' => '20', 'id_name' => 'user_id2_c', 'ext2' => 'Users', 'module' => 'Users', 'rname' => 'name', 'quicksearch' => 'enabled', 'studio' => 'visible', ); Судя по всему получается так, что в функции buildWhere() срабатывает case = 'relate'. Но так же выяснил, что НЕ срабатывает блок if (isset($widgetDef['link']) && $this->seedBean->load_relationship($widgetDef['link'])) {} просто по причине отсутствия 'link' в описании нашего поля. Решил добавить 'link', и ВСЕ ПОЛУЧИЛОСЬ!!! За основу для добавления link взял поле assigned_user_id, так как эти поля получились очень похожими: эти поля находятся в таблице `accounts` и ссылаются на таблицу и модуль с Пользователями (`users`) Таким образом в файл, в котором я описал мое поле, я добавил еще следующие блоки: $dictionary['Account']['fields']['fixed_accountant_c_link'] = array ( 'name' => 'fixed_accountant_c_link', 'type' => 'link', 'relationship' => 'fixed_accountant_c', 'vname' => 'LBL_FIXED_ACCOUNTANT_C_LINK', 'link_type' => 'one', 'module' => 'Users', 'bean_name' => 'User', 'source' => 'non-db', 'duplicate_merge' => 'enabled', 'rname' => 'fixed_accountant_c', 'id_name' => 'user_id2_c', 'table' => 'users', ); $dictionary['Account']['relationships']['fixed_accountant_c'] = array ( 'lhs_module' => 'Users', 'lhs_table' => 'users', 'lhs_key' => 'id', 'rhs_module' => 'Accounts', 'rhs_table' => 'accounts', 'rhs_key' => 'user_id2_c', 'relationship_type' => 'one-to-many' ); А в описание нашего поля вставляю link: $dictionary['Account']['fields']['fixed_accountant_c'] = array ( 'required' => false, 'source' => 'non-db', 'name' => 'fixed_accountant_c', 'vname' => 'LBL_FIXED_ACCOUNTANT_C', 'type' => 'relate', 'link'=>'fixed_accountant_c_link' , 'massupdate' => 0, 'no_default' => false, 'comments' => '', 'help' => '', 'importable' => 'true', 'duplicate_merge' => 'disabled', 'duplicate_merge_dom_value' => '0', 'audited' => false, 'inline_edit' => true, 'reportable' => true, 'unified_search' => false, 'merge_filter' => 'disabled', 'len' => '255', 'size' => '20', 'id_name' => 'user_id2_c', 'ext2' => 'Users', 'module' => 'Users', 'rname' => 'name', 'quicksearch' => 'enabled', 'studio' => 'visible', ); После этих манипуляций делаю быстрое восстановление и смотрим что получилось с генерацией SQL-запроса: SELECT accounts.id , accounts.name , accounts.renewal_date_c , accounts.time_zone_c , accounts.tariff_c , accounts.assigned_user_id FROM accounts where ((accounts.id IN ('d924c25c-7ab0-25c8-f5cd-5a58c168620a'))) AND accounts.deleted=0 ORDER BY accounts.date_entered DESC Что уже то, что нам нужно...
  4. Всем привет! Давайте разберем ситуацию, когда нам необходимо выполнить единоразово какую то задачу в CRM-системе, и мы хотим, чтобы эту задачу выполнил планировщик. Подобное может потребоваться, например когда: Пользователь должен запустить "тяжелый" алгоритм, результаты которого не надо сразу получить на экране. Например: синхронизация CRM-системы с какой-либо другой системой (если честно, я этот механизм "подсмотрел" в библиотеки связки Mautic и SuiteCRM). То есть пользователь где то в системе запускает синхронизацию и продолжает далее работать в CRM-системе, а CRM-система в этот момент производит долгую ресурсоёмкую процедуру синхронизации. В теории можно настроить запуск планировщик SuiteCRM на запуск из под root. Таким образом в задачах, выполняемых в планировщике, появится полный доступ к всему серверу. Это может пригодиться для каких то специфических задач (все же обычно запускать crontab из под root не рекомендуется). В этом случае запуск задачи из планировщика позволит сделать что то такое, что выполнить простым запросом не получится (в частности в данный момент при написании статьи мне было необходимо менять chmod у файла с логами php, которые создавались из под root). Нужна некая отсрочка выполнения некой задачи. Наверное можно еще много чего напридумывать, но статья про запуск задач в планировщике, а не про зачем это надо ))) Итак. Общий посыл к тому, что у нас должно происходить: Есть некая задача в планировщике Эту задачу не видно в списке задач, она не предназначена для цикличного воспроизведения, только разовый запуск Мы должны иметь некий механизм, который указывает ЕДИНОРАЗОВО запустить нашу задачу в планировщике Планировщик, используя его встроенные механизмы, видит необходимость запуска задачи и выполняет это действие Теперь давайте перейдем к практике. Давайте начнем с механизма создания задачи в планировщике: require_once('modules/SchedulersJobs/SchedulersJob.php'); require_once("include/SugarQueue/SugarJobQueue.php"); // Создаем задачу в планировщике $job = new SchedulersJob(); $job->name = "postInstall"; $job->target = "class::postInstallCRMHosting"; $job->assigned_user_id = '1'; $job->execute_time =$GLOBALS['db']->convert($GLOBALS['timedate']->getNow()->modify("+10 seconds")->asDb(), 'datetime'); $jq = new SugarJobQueue(); $jq->submitJob($job); Этот блок создаст запись в таблице `job_queue` (да да, именно в той, куда валятся все логи выполняемых задач в планировщике). Мы создаем запись с названием "postInstall" и указываем, что должен выполниться класс postInstallCRMHosting. Время выполнения - через 10 секунд. На самом деле задачи в планировщике выполняются по тикам crontab, который запускается ежеминутно в начале минуты. По этому если надо "прям сейчас", то все равно будет в ближайшую минуту. Но пусть будет 10 секунд. Также, при необходимости, можно указать через час/день/год. Таким образом в ближайшие минуту система попытается запустить выполнение задачи в классе postInstallCRMHosting. Давайте добавим файл с таким классом. Проще всего (и думаю правильнее всего) добавлять задачи в папку /custom/Extension/modules/Schedulers/Ext/ScheduledTasks. Например, файл с моей задачей будет выглядеть примерно так: /custom/Extension/modules/Schedulers/Ext/ScheduledTasks/CRMHosting.postInstall.php <?php /** * Created by PhpStorm. * User: crmhosting * Date: 06.05.2018 * Time: 9:39 */ class postInstallCRMHosting implements RunnableSchedulerJob { public function setJob(SchedulersJob $job) { $this->job = $job; } public function run($job_data) { /************************************ * Назначаем права на файл с логами */ $log_file = "/var/log/php-fpm/www-error.log"; exec("chmod 0777 " . $log_file); /************************************/ return true; } } Таким образом SuiteCRM выполнит наш скрипт единоразово в планировщике. В списке задач планировщика ничего не появится лишнего. Можно подобным образом запускать задачу необходимое количество раз.
  5. Всем привет! Просто небольшая заметка, но может кому пригодиться... Иногда бывает, что в карточке модуля на панели необходимо разместить некую информацию, но Label у этой информации будет лишним. Например: список ссылок, относящихся к тому или иному контрагенту: Как вы видите, здесь имеет место быть отдельная панель, и в этой панели единственная переменная, которая содержит кучу ссылок. В DetailView это выглядит примерно так: 'lbl_editview_panel12' => array ( 0 => array ( 0 => array ( 'name' => 'custom_all_link', 'label' => '', ), ), ), Видите две точки? Это наш пустой Label. Выглядит не красиво, не правда ли? Можно конечно было бы добавить туда какой-нибудь LBL_, но он тут избыточен! Панель и так называется "Ссылки". И внутрь вставлять еще раз "Ссылки" или что то такое уже было бы лишним! Давайте просто уберем вообще блок, в котором находится Label: 'lbl_editview_panel12' => array ( 0 => array ( 0 => array ( 'name' => 'custom_all_link', 'label' => '', 'hideLabel' => true, ), ), ), Теперь наш блок примет вид: Как вы видите Label для нашего поля перестал вообще отображаться!
  6. Всем привет! В последних на текущий момент (апрель 2018 года) версиях SuiteCRM (версии SuiteCRM 7.10.2 и 7.10.3) столкнулись с небольшой проблемой: куда то пропали боковые меню в модулях, доступных админу (которые находятся в админке). Например, когда смотрим на свой профиль: Начал искать что куда делось. Нашел не очень старые версии, в которых все работало. Состав меню определяется в том числе в файле Menu.php, лежащем в модуле. Там есть строки типа: $module_menu = Array(); if ($GLOBALS['current_user']->isAdminForModule('Users') ) { $module_menu = Array( Array("index.php?module=Users&action=EditView&return_module=Users&return_action=DetailView", $mod_strings['LNK_NEW_USER'], "Create"), Array("index.php?module=Users&action=EditView&usertype=group&return_module=Users&return_action=DetailView", $mod_strings['LNK_NEW_GROUP_USER'], "Create_Group_User") ); $module_menu[] = Array("index.php?module=Users&action=ListView&return_module=Users&return_action=DetailView", $mod_strings['LNK_USER_LIST'], "List"); $module_menu[] = Array("index.php?module=Import&action=Step1&import_module=Users&return_module=Users&return_action=index", $mod_strings['LNK_IMPORT_USERS'], "Import", 'Contacts'); } Ищем в /includes/ файлы, где активно используется переменная $module_menu. Внимание привлекла папка /include/MVC/ Сравниваем текущую версию этой папки с версией, где все работало: Смутило использование unset. Оказалось верно, что смутило. В общем убираем этот блок из файла SugarView.php, и вуа-ля:
  7. Стояла задача организовать поиск(фильтрацию на карточке списка) обращений по email связанных c ним Контактов , так же необходимо , что бы искались записи по частичному вхождению адреса, например почта pupok@mail.ru должна находиться при запросе: "pup" добавили поле mail_contact_init типа non-db выводим его в карточку расширенного поиска custom/modules/Cases/metadata/searchdefs.php или через студию, добавляем запрос по которому будит производиться поиск записи Обращения (в запросе нужно получить Id записи модуля по необходимым нам условиям) в файле custom/modules/Cases/metadata/SearchFields.php в моем случае это: 'mail_contact_init' => //Наименование поля array ( 'query_type' => 'format', // Тип запроса, важно указать format 'operator' => 'subquery', 'subquery' => 'SELECT cases.id FROM cases INNER JOIN contacts_cases ON cases.id IN (SELECT contacts_cases.case_id FROM contacts_cases WHERE contacts_cases.deleted = 0 AND contacts_cases.contact_id IN (SELECT email_addr_bean_rel.bean_id FROM email_addr_bean_rel WHERE email_addr_bean_rel.deleted = 0 AND email_addr_bean_rel.bean_module = \'Contacts\' AND email_addr_bean_rel.email_address_id IN (SELECT email_addresses.id FROM email_addresses WHERE email_addresses.deleted = 0 AND email_addresses.email_address LIKE "%{0}%" ) ) )', // вместо {0} suitecrm подставляет введенный запрос в данном поле 'db_field' => // Выборка записей основного модуля (обращения) идет по id array ( 0 => 'id', ), 'vname' => 'LBL_MAIL_CONTACT_INIT', ),
  8. Решил по одному проекту сделать сортировку записей в сабпанели. Добавил кнопки, повесил ajax-запросы при нажатии на ссылки. После сортировки надо перезагрузить сабпанель желательно без перезагрузки всей страницы: после успешной смены сортировки через ajax вызываем showSubPanel('themes',null,true); примерно так: function sortThemes(theme_id, order) { console.log('sortThemes - start'); console.log('theme_id = ', theme_id); console.log('order = ', order); var record = $('#formDetailView input[name=record]').val(); console.log('record = ', record); //Pass the properties to the controller function via ajax $.ajax({ type: "GET", url: "index.php?module=AOS_Quotes&action=setOrderThemes&record="+record+"&theme_id="+theme_id+"&sort_order="+order+"&to_pdf=true", dataType: 'json', success: function(data) { // On success generate the tasks for the chart console.log('data = ', data); showSubPanel('themes',null,true); } }); } Панель, которую перезагружаю, соответственно описывается как theme: $layout_defs["AOS_Quotes"]["subpanel_setup"]['themes'] = array ( 'order' => 100, 'module' => 'Themes', 'subpanel_name' => 'default', 'sort_order' => 'asc', 'sort_by' => 'id', 'title_key' => 'LBL_THEMES_SUBPANEL_TITLE', 'get_subpanel_data' => 'themes', 'top_buttons' => array ( 0 => array ( 'widget_class' => 'SubPanelTopButtonQuickCreate', ), 1 => array ( 'widget_class' => 'SubPanelTopSelectButton', 'mode' => 'MultiSelect', ), ), );
  9. Всем привет! Хочу поделиться методом, который позволяет добавлять фильтры в список записей модуля, когда надо найти все записи, у которых есть связи с определенной записью из другого модуля. Сразу оговорюсь, что SuiteCRM позволяет выполнять подобный поиск стандартным способом, когда ищем в связке один-ко-многим. Например, должны найти все Контакты, принадлежащие определенному Контрагенту. Подобные связи можно видеть в настройках шаблона поиска в студии, и настраиваются они без вмешательства программиста путем работы с интерфейсом. Мы же с вами поговорим о ситуациях, когда надо найти все записи модуля, который связан с другим модулем произвольным образом (возможно связью многие-ко-многим, а возможно вообще какими то сложными логическими схемами, главное чтобы эту связь можно было уместить в одном SQL-запросе). Чтобы было понятней давайте рассмотрим пример: Допустим, нам надо в модуль "Документы" в списке записей добавить фильтр, в котором мы сможем найти все документы, у которых за контрагента отвечает определенный пользователь. Таким образом: Мы находимся в модуле Документы Нам надо отфильтровать Документы по пользователю (модуль Users) У каждого Документа есть связь с Контрагентами многие-ко-многим (в карточке Документа сабпанель Контрагенты) В Контрагенте есть связь с Пользователем через поле assigned_user_id Нам надо получить список Документов по фильтру Стандартным способом без программирования мы сможем реализовать подобную задачу только используя модуль "Отчёты". Да. Так тоже можно. Но не удобно. Хочется находиться в модуле Документы, а не "шарится" по другим модулям/отчётам. Далее я тезисно опишу что нам понадобиться сделать, а потом покажу, как это будет выглядеть в файлах. Итак: Хотя механизм формирования фильтров и позволяет в них добавлять разную самописную отсебятину, но наиболее удобным средством, по результатам многочисленных опытов, стало добавление фильтра в виде поля модуля. То есть мы добавляем поле в модуль Documents, которое будет собой представлять как бы ссылку на модуль Users. А чтобы это поле не вносило изменения в структуру таблицы с документами, ему (этому полю) необходимо указать характеристику 'source' => 'non-db'. После выполнения быстрого восстановления с вновь добавленным полем модуля можно работать как с обычным полем: заходим в студию, и в модуле Документы помещаем его в макет поиска туда, куда удобно (имеет смысл работать только с макетом поиска, в других макетах это поле не будет работать, потому что не хранит данные в базе). В настройках поиска вручную задаем SQL-запрос, который должен будет выполниться, если мы введем какое то значение в фильтре в нашем поле. Ну а теперь по конкретике: Создаем файл с описанием добавляемого поля: custom/Extension/modules/Documents/Ext/Vardefs/account_assigned_user.php: <?php $dictionary['Document']['fields']['account_assigned_user'] = array ( 'required' => false, 'source' => 'non-db', 'name' => 'account_assigned_user', 'vname' => 'LBL_ACCOUNT_ASSIGNED_USER', 'type' => 'relate', 'massupdate' => 0, 'no_default' => false, 'comments' => '', 'help' => '', 'importable' => 'true', 'duplicate_merge' => 'disabled', 'duplicate_merge_dom_value' => '0', 'audited' => false, 'inline_edit' => true, 'reportable' => true, 'unified_search' => false, 'merge_filter' => 'disabled', 'len' => '255', 'size' => '20', 'id_name' => 'id', 'ext2' => 'Users', 'module' => 'Users', 'rname' => 'name', 'quicksearch' => 'enabled', 'studio' => 'visible', ); Хотел бы обратить ваше внимание на следующу строку: 'id_name' => 'id', Вообще подобное поле положено создавать в паре с полем, которое будет хранить непосредственно id связанной записи в базе данных. Это когда мы хотим добавить связь один-ко-многим. И в параметре 'id_name' нужно было бы указать название подобного поля. Но в нашем случае в таблице с документами нам не надо ничего хранить. Все необходимые данные мы будем получать из других таблиц. По этому в параметре 'id_name' мы указываем значение 'id' (если не вдаваться сильно в детали, то неуказать ничего мы тоже не можем, потому что этот параметр потом будет учавствовать в SQL-запросе в виде `documents`.`<id_name>`, и если ничего не указать, то будет SQL-ошибка). Создаем файл с локализацией создаваемого поля: custom/Extension/modules/Documents/Ext/Language/ru_RU.account_assigned_user.php <?php $mod_strings['LBL_ACCOUNT_ASSIGNED_USER'] = 'Ответственный за Контрагента'; Выполняем быстрое восстановление. После этого можно идти в Студию и настраивать внешний вид поиска: Переходим в модуль "Документы, и видим, что поле появилось в списке фильтров (верстка кривая, но сейчас не о этом): Нам необходимо немного модернизировать отображение этого поля. Дело в том, что если мы выбор пользователя оставим в текущем виде, то в наш SQL-запрос будет попадать имя пользователя. А это немного не то, что надо. Нам надо, чтобы в SQL-запрос попадал ID выбранного поля. Для этого мы поле переделываем в выпадающий список: открываем файл custom/modules/Documents/metadata/searchdefs.php, находим в нем секцию account_assigned_user, и меняем ее на 'account_assigned_user' => array ( 'type' => 'enum', 'studio' => 'visible', 'label' => 'LBL_ACCOUNT_ASSIGNED_USER', 'id' => 'ID', 'link' => true, 'width' => '10%', 'default' => true, 'name' => 'account_assigned_user', 'function' => array ( 'name' => 'get_user_array', 'params' => array ( 0 => false, ), ), ), После данной манипуляции фильтр по пользователю должен выглядеть как список пользователей (там опять кривая верстка, но мы опять не про нее): Если сейчас выбрать в этом поле пользователя, и нажать кнопку "Найти" в поиске, то мы ничего не получим, потому что еще не настроили механизм поиска нужных нам записей. А для этого переходим к файлу custom/modules/Documents/metadata/SearchFields.php (он как раз должен был создастся после манипуляций в студии), и вставляем в него блок такого содержания: 'account_assigned_user' => array ( 'query_type' => 'format', 'operator' => 'subquery', 'subquery' => ' SELECT `documents`.`id` FROM `documents` INNER JOIN `documents_accounts` ON `documents_accounts`.`document_id` = `documents`.`id` AND `documents_accounts`.`deleted` = 0 INNER JOIN `accounts` ON `accounts`.`id` = `documents_accounts`.`account_id` AND `accounts`.`deleted` = 0 WHERE `documents`.`deleted` = 0 AND `accounts`.`assigned_user_id` = "{0}" ', 'db_field' => array ( 0 => 'documents.id', ), ), Как видно (надеюсь), данный код ищет все id в таблице documents, которые связаны с контрагентом, у которого в assigned_user_id указано выбранное нами значение. А вот такой SQL-запрос соберется в итоге при попытке воспользоваться данным фильтром (указаю юзера с айди = 1 - админ): SELECT documents.id , documents.assigned_user_id , documents.id , documents.document_name , documents.document_revision_id , documents.doc_id , documents.doc_type , documents.doc_url , documents.category_id , documents.subcategory_id , documents.exp_date , jt0.user_name assigned_user_name , jt0.created_by assigned_user_name_owner , 'Users' assigned_user_name_mod, documents.date_entered , LTRIM(RTRIM(CONCAT(IFNULL(jt1.first_name,''),' ',IFNULL(jt1.last_name,'')))) account_assigned_user , documents.created_by FROM documents LEFT JOIN users jt0 ON documents.assigned_user_id=jt0.id AND jt0.deleted=0 AND jt0.deleted=0 LEFT JOIN users jt1 ON documents.id = jt1.id AND jt1.deleted=0 where ((documents.id IN ( SELECT `documents`.`id` FROM `documents` INNER JOIN `documents_accounts` ON `documents_accounts`.`document_id` = `documents`.`id` AND `documents_accounts`.`deleted` = 0 INNER JOIN `accounts` ON `accounts`.`id` = `documents_accounts`.`account_id` AND `accounts`.`deleted` = 0 WHERE `documents`.`deleted` = 0 AND `accounts`.`assigned_user_id` = "1" ))) AND documents.deleted=0 ORDER BY documents.document_name ASC Подобным образом можно заложить любую логику поиска нужной записи. Пользуйтесь )))
  10. SpravkaCRM.ru

    GMail + SuiteCRM

    Version 1.0.1

    20 downloads

    Описание будет
  11. Доброго времени суток. Только начал знакомиться с замечательным произведением программерского исскусства - SuiteCRM. Приходится работать с уже готовыми системами, опыта мало. Вопрос: Есть список звонков. Есть колонка - продолжительность разговора. У клиента в заголовке колонки указано время продолжительности в секундах. Но в строках эти значения уже в минутах. Список звонков - это стандартный модуль Calls. Есть менее стандартный модуль Asterisk, со своим скриптом asteriskLogger.php. В этом Логгере ведется вычисление продолжительности разговора. Далее совсем мне непонятным способом эти значения продолжительности попадают в журнал звонков и выводятся на экран монитора. Вопрос к опытным специалистам - какова цепочка передачи данных продолжительности звонка в список звонков? Чтобы отследить ошибку и установить верное значение продолжительности уже в секундах, как и положено. Спасибо.
  12. Хочу разместить тут одну из вариаций решения задачи: При добавлении нового Контрагента, если заполнено поле "ИНН", надо определить, нет ли в базе уже существующего Контрагента с таким же ИНН. И если есть - не дать сохранить выведя соответствующую надпись о этом. Реализация сделана на странице добавления новой записи в модуле Accounts. Если в вкратце, то алгоритм следующий: Добавляем на страницу создания/редактирования карточки Accounts произвольный js-файл В этом js-файле для кнопки "Сохранить" прописываем свой обработчик. Когда пользователь нажимает кнопку "Сохранить" обработчик получает введенное значение из поля "ИНН", и отправляет AJAX-запрос на сервер с этим ИНН, чтобы проверить его в базе. Если ИНН в базе найден, то подсвечиваем красным поле с ИНН и пишем о том, что введенное значение не уникально Если ИНН не найден, то продолжаем стандартное сохранение карточки. Теперь давайте более предметно посмотрим, как это работает. В meta-файле, описывающем editview для Account custom/modules/Accounts/metadata/editviewdefs.php, добавляем блок, который подключит js-файл: <?php $viewdefs ['Accounts'] = array ( 'EditView' => array ( 'templateMeta' => array ( 'form' => array ( 'enctype' => 'multipart/form-data', 'buttons' => array ( 0 => 'SAVE', 1 => 'CANCEL', ), ), 'maxColumns' => '2', 'widths' => array ( 0 => array ( 'label' => '10', 'field' => '30', ), 1 => array ( 'label' => '10', 'field' => '30', ), ), 'includes' => array ( 1 => array ( 'file' => 'modules/Accounts/Account.js', ), 2 => array ( 'file' => 'modules/Accounts/edit.view.js', ), ), ), Это лишь кусок файла с мета-данными. Непосредственно наша вставка, это: 2 => array ( 'file' => 'modules/Accounts/edit.view.js', ), Таким образом нам надо добавить пустой файл modules/Accounts/edit.view.js, и заполнить его примерно так: $(document).ready(function(){ // Ставим для кнопки Сохранения свой обработчик $('#EditView').find('input[type=submit][id=saveButton]').click(function(){ // Ставим действие после срабатывания формы $('#EditView').find('input[type=hidden][name=action]').val('Save'); var checkForm = check_form('EditView'); if($('#inn').val() != '') { // Проверям на уникальность ИНН $.ajax({ url: "index.php", type: "POST", data: { module: 'Accounts', action: 'checkINN', record: $('#EditView').find('input[name=record]').val(), inn: $('#inn').val(), to_pdf: true } }).done(function(responce) { // Обработка полученных данных var accounts = jQuery.parseJSON(responce); if(accounts.length > 0) { // Найдены дубликаты var list = ''; for(var i = 0; i < accounts.length; i++) { list += '<li><A href="index.php?module=Accounts&action=DetailView&record='+accounts[i]['id']+'" style="color: red;" target="_blank">'+accounts[i]['name']+'</A></li>'; } list = '<B>Найдены дубликаты по ИНН</B>:<BR>' + list; if(isAccessDoubleINN) { list += '<BR>Если желаете все равно сохранить Контрагента с текущим ИНН нажмите кнопку: <button class="button" onclick="if(check_form(\'EditView\')){$(\'#EditView\').submit();}">Пропустить</button>'; } add_error_style('EditView','inn', list); } else { // Дубликатов по ИНН нет, можем прождолжать // Стандартная проверка остальных полей if(checkForm) { $('#EditView').submit(); } } }); } else { if(checkForm) { $('#EditView').submit(); } } return false; }); // Удаляем уже ранее установленный обработчик $('#EditView').find('input[type=submit][id=saveButton]').removeAttr("onclick"); }); Здесь в блоке $.ajax({ url: "index.php", type: "POST", data: { module: 'Accounts', action: 'checkINN', record: $('#EditView').find('input[name=record]').val(), inn: $('#inn').val(), to_pdf: true } }) как раз и происходит AJAX-запрос к серверу на проверку уникальности введенного значения. Запрос происходит к action=checkINN. Нам надо добавить php-файл с таким названием по адресу modules/Accounts/checkINN.php: <?php if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point'); /** * Created by JetBrains PhpStorm. * User: Evgen * Date: 22.08.13 * Time: 12:57 * To change this template use File | Settings | File Templates. */ global $db; $sql = " SELECT `id`, `name` FROM `accounts` WHERE `inn` = '".$_REQUEST["inn"]."' AND `deleted` = 0 "; if(isset($_REQUEST['record']) AND $_REQUEST['record'] != '') { $sql .= " AND `id` != '".$_REQUEST["record"]."' "; } $result = $db->query($sql, true); $return_array = array(); while($row = $db->fetchByAssoc($result)) { $return_array[] = $row; } $json = new JSON(); echo $json->encode($return_array); Вот и все! Проверка на уникальность готова.
  13. Version 1.0.0

    39 downloads

    Пакетная загрузка файлов в субнанель документы для SuiteCRM/SugarCRM dragndropuploaddocuments - это дополнение к CRM-системе на базе SugarCRM/SuiteCRM, которое позволяет быстро и удобно добавлять файлы в субпанели документы, просто перетягивая их из папки в поле для загрузки. Поддерживает загрузку нескольких файлов одновременно. Вот смотрите, на примере модуля Контрагенты: для установки необходимо : 1 скачать файл дополнения 2 перейти в CRM систему Администрирование -> Загрузчик модулей 3 выбрать файл дополнения 4 После того , как модуль окажется в списке загруженных нажать кнопку установить 5 В появившемся окне ознакомиться с условиями лицензионного соглашения и нажать кнопку переместить переключатель в положение принимаю и нажать кнопку вперед 6 дождитесь окончания установки. Модуль готов к использованию
  14. Иногда возникает ситуация когда необходимо отключить "сохранение" данных в полях фильтра (в полях поиска). Т.е., например, в модуле Leads отфильтровали по полю name и теперь возвращаясь этот модуль, поле name в фильтре будет заполнено значением, которое искали, иногда это бывает неудобно. Чтоб отключить данный функционал идём в файл: modules/MySettings/StoreQuery.php и в самом конце файла находим метод getStoredQueryForUser. public static function getStoredQueryForUser($module) { global $current_user; return $current_user->getPreference($module . 'Q'); } Меняем на: public static function getStoredQueryForUser($module) { global $current_user; // return $current_user->getPreference($module.'Q'); return array(); } В данном случае я закоментировал возврат получаемых данных из таблицы user_preferences, которые отвечают за то что отобразится в фильтре и вместо этого возвращаю пустой массив, т.к. исходные данные возвращаются в виде массива. При таких изменениях коде: Пока мы находимся на странице модуля, то фильтр будет хранить выбранные варианты, т.е. будет работать пагинация и не сбрасывать значения фильтров, так же обновление страницы по нажатию клавиши F5 не сбрасывает значения фильтров. При этом, если покинуть страницу с текущим модулем, перейдя на другую, а потом вернуться, то ранее введённые в фильтр данные сбросятся.
  15. Может быть не все знают, что SuiteCRM позволяет двумя разными способами отображать кнопки с действиями, которые можно произвести с текущей картой. Это там, где есть кнопки "Править", "Поиск дубликатов", "Просмотреть журнал изменений" и так далее: В целом такая компоновка кнопок удобна, потому что компактна и не перегружает интерфейс кнопками, которые редко используются (часто используемая кнопка "Править" всегда наверху в быстром доступе). Но вот лично мне больше нравится, когда кнопки все расположены рядышком и их можно сразу кликнуть. Чтобы для всех CRM-системы указать именно такой формат отображения кнопок необходимо в config_override.php добавить параметр: $sugar_config['enable_action_menu'] = false; После этого кнопки примут вид:
  16. Скачали и установили новую SuiteCRM 7.9.4 При проверке работоспособности выяснилось: Модуль Email адски тормозит при наличии даже 2к писем В этом модуле судя по всему все переделали Многие названия отправителей в неправильной кодировке типа Экспресс-разбирательство данной ситуации выявило, что судя по всему теперь сделали чтение почты напрямую из почтового сервера без предварительной ее загрузки в базу CRM-системы. В целом это круто. И так и должно быть. Ведь IMAP именно такое поведение и предполагает. Но что же так долго то загружаются страницы? Ну да ладно, с этим еще разберемся, или пофиксят в следующих релизах... А неправильная кодировка писем лечится следующим образом: Находим файл modules\Emails\include\ListView\ListViewDataEmails.php находим в нем строку $ret = $emailHeader['from']; меняем на строку $ret = html_entity_decode($inboundEmail->handleMimeHeaderDecode($emailHeader['from'])); Теперь все норм: Может кому пригодиться ))
  17. В ходе разработки системы появилась не обходимость сделать логику, кторую не получится реализовать стандартным функционалом ролей. В данном случае было необходимо, чтоб контрагентов со статусом "архив" видели только администраторы системы. Для реализации данной логики нам понадобится файл modules/Accounts/Account.php Ищем в нём метод create_new_list_query, если не находим то создаём. В ней мы ограничиваем доступ к записям в listview модуля Контрагенты. Фактически дописываем допольнительное условие в запрос по которому строится список в listview. function create_new_list_query($order_by, $where,$filter=array(),$params=array(), $show_deleted = 0,$join_type='', $return_array = false,$parentbean=null, $singleSelect = false) { global $current_user; $ret_array = parent::create_new_list_query($order_by, $where, $filter, $params, $show_deleted, $join_type, $return_array, $parentbean, $singleSelect); //если текущий пользовательне админ, то запрещам просмотр записей со статусом "в архиве" if(!$current_user->is_admin){ $ret_array['where'] .= " AND accounts.contact_status <> 'archive'"; } if($return_array) { return $ret_array; } return $ret_array['select'] . $ret_array['from'] . $ret_array['where']. $ret_array['order_by']; } Далее нам необходимо ограничить доступ для просмотра и редактирования архивных контрагентов. Для этого воспользуемся методом ACLAccess (так же ищем и дописываем код или создаём) Здесь для редактирования, просмотра и удаления доступ запрещён. А сохранение закомментировано, для того чтоб обычный пользователь имел возможность поменять статус контрагента с какого-то значения на "архив" и сохранить. function ACLAccess($view, $is_owner='not_set', $in_group='not_set'){ global $current_user; $view = strtolower($view); switch($view){ case 'edit': case 'detail': // case 'save': case 'editview': case 'delete': if(!empty($this->contact_status) && $this->contact_status == "archive" && !$current_user->is_admin){ return false; } } return parent::ACLAccess($view,$is_owner,$in_group); }
  18. Version 1.0.0

    12 downloads

    TechnicalSupport - это дополнительный модуль к CRM-системе на базе SugarCRM/SuiteCRM, который позволяет обращаться к службе технической поддержки CRMHosting.ru для согласовывания и последующего выполнения Ваших пожеланий по доработке Вашей CRM-системы. Примерный перечень возможных работ и их стоимость можно посмотреть на сайте.
  19. В процессе разработки столкнулся с проблемой: независимо от того какие значения стоят в полях "Дата начала" и "Дата выполнения" в модуле "Задачи", в календаре такая задача отображается размером в 2 часа. При этом звонки и встречи отображаются нормально. Путём сравнения модулей удалось выяснить, что в звонках и встречах есть поля duration_hours и duration_minutes по которым строится отображение данных в календаре. Я предположил, что необходимо найти место где формируются данные для календаря и высчитывать эти значения там. В файле modules/Calendar/Calendar.php ищем строки "Tasks" => array("showCompleted" => true,"start" => "date_due", "end" => "date_due"), заменяем на "Tasks" => array("showCompleted" => true,"start" => "date_start", "end" => "date_due"), В этом же файле ещё ищем: if (isset($act->sugar_bean->duration_hours)) { $item['duration_hours'] = $act->sugar_bean->duration_hours; $item['duration_minutes'] = $act->sugar_bean->duration_minutes; } и заменяем на $cstm_mod = array('Opportunities', 'Tasks'); //модули в которых необходимо высчитывать duration_hours и duration_minutes if ( in_array($act->sugar_bean->module_dir, $cstm_mod) ) { //для "особенных" модулей считаем if (isset($act->sugar_bean->date_start) && isset($act->sugar_bean->date_due)) { $datetime1 = new DateTime($act->sugar_bean->date_start); //дата начала $datetime2 = new DateTime($act->sugar_bean->date_due); //дата окончания $interval = $datetime1->diff($datetime2); $item['duration_hours'] = $interval->format('%H'); $item['duration_minutes'] = $interval->format('%I'); } } else { //для обычных модулей берём существующие значения if (isset($act->sugar_bean->duration_hours)) { $item['duration_hours'] = $act->sugar_bean->duration_hours; $item['duration_minutes'] = $act->sugar_bean->duration_minutes; } } А так же в файле modules/Calendar/CalendarUtils.php в методе get_time_data комментируем строки: if($bean->object_name == 'Task') $start_field = $end_field = "date_due"; В результате получаем, что задача отображется в календаре начиная со времени начала и длится до времени окончания.
  20. Патч исправляет ошибку в работе модуля Отчёты: поиск по дате и поиск в диапазоне. Про ручное устранение этой ошибки я писал в статье: Но потом родилась мысль: а почему бы не начать писать расширения под SuiteCRM? Скачать и установить расширении ведь удобнее, чем руками лазить по коду! Вот так и родилась эта идея. И вот предлагаю дебютный мой выход в этом направлении: bugfix_patch_reports.13.01.2016.15.45.31.zip Патч устраняет указанные выше недостатки. Сейчас я еще все тут продумаю, как лучше организовать этот раздел, и, думаю, открою какую то полнофункциональную рубрику по разработке патчей. Кстати, для желающих поучавствовать вот проект на Bitbucket.org: https://bitbucket.org/crmhosting/suitecrm_bugfix_reports
  21. если не сильно отвлекаю есть еще вопрос как можно вывести наименование ролей в кук при авторизации это через модуль users ? 20:20:16 в смысле ролей на том или ином пользоватере 20:20:54 20:27:30 что такое кук? печеньки 20:33:25 cookis 20:33:31 20:33:56 ну например в after_ui_frame 20:34:01 вроде есть такой хук 20:34:10 и в нем пихнуть в $_COOKIE 20:34:23 для текущего юзера найти все его роли не подскажете как получить все роли ? я с функциями suiteCRM не очень знаком но штука я так понял интересная ))) 20:37:47 20:38:33 ну вот например: 20:38:53 foreach (ACLRole::getUserRoles($current_user->id, false) as $role) { // тут что то с ролями делать можно } 20:39:11 только перед этим не забыть global $current_user; 20:39:14 указать просто сейчас блокировка полей модулей сейчас реализована через кук id пользователя, но для каждого писать правила геморно тем более сейчас собираются расширять отдел продаж... 20:39:45 ага спасибо ))) 20:39:52 я как бы особо в суите не секу а делать заставили ))) 20:40:21 если есть кошелек яндекса кинь за доначу ))) 20:40:57 20:41:01 выбивайте бюджет, и я вам все сделаю а сколько нужно для того что бы реализовать модуль тонкой настройки роле ? 20:41:42 20:41:58 зависит от тонкости 20:42:06 если есть ТЗ мог бы оценить работы 20:42:20 ну или описание более менее понятное что хотелось бы в итоге получить 20:42:40 а то под тонкими настройками можно очень много разного функионала напридумывать )) тз нет если нужно напишем пока так на вскидку 20:47:13 20:47:24 да можно и навскидку 20:47:33 мне официальные тз на 100 листах никогда и не нужны были нужно для роли сделать возможность блокировать поля ввода 20:47:36 и статусы 20:47:40 можно это даже не на бек энде сделать а на фронтэнде 20:48:13 у нас сейчас так реализовано 20:48:25 20:49:05 можно сделать это двумя путями: 1 - в конкретно взятом модуле для конкретно взятых полей сделать блокировку для конкретно указанных ролей. можно даже список ролей куда то в админку вытащить. это займет порядка 2-3 часов = ну тысяч 5 плюс минус 20:49:47 2 - сделать надстройку в ролях, где для каждого модуля показать список полей с возможностью указания какие поля блокировать. это часов примерно на 20 наверное тянет. будет удобно и универсально. но делать долго. тысяч 30 20:50:26 это на вскидку нет интересует однозначно только 2 вариант первый и так реализован коряво но реализован... 20:51:18 но я пока бюджета не имею 20:51:32 попробую уговорить начальника... 20:51:51 а готовых подобных модулей нет ? и часов на 20 это по дням сколько ? 20:52:52 а то разные люди по разному работают ))) 20:53:10 20:53:44 часов 20 это по деньгам. я же на повременке работаю. по этому примерно и прикидываю исходя из 1500 рублей/час 20:53:57 если часа по 2 в день работать с учетом текущей загрузки, то пару недель я понимаю что на вскидку но просто порядок около недели 20:53:59 20:53:59 получается ок 20:54:11 20:54:21 у меня где то были наработки 20:54:23 по клиентам 20:54:34 там помоему было что то типа такоо 20:54:36 сейчас гляну и еще такой вопрос на меня я чую повесят эту CRM можно ли как-нибудь у вас какие нибудь типо курсы купить или просто хотя-бы вы на видео записывали как вы выполняете каке-нибудь задачи и комментируете... 20:59:56 21:00:18 да, я провожу обучающие вебинары 21:00:24 индивидуальные )) на обучение денег могут выделить 21:00:31 21:00:52 несколько таких обучений проводил отлично 21:01:00 еще вопрос вы ип или юр лицо есть ? 21:01:26 или просто фрилансер 21:01:51 просто по оплате с модулем на физ лицо боюсь точно завернут 21:02:15 21:02:20 работаю и по договору в том числе как ИП по безналу ок 21:02:29 хорошо 21:02:33 21:05:57 нашел 21:06:00 https://yadi.sk/i/GBGQbzrU3H7Mhi 21:06:26 https://yadi.sk/i/vWU9Fm-S3H7MkD 21:06:41 https://yadi.sk/i/vJaz9W893H7Mm7 21:06:56 я так понимаю то, что вам нужно 21:08:24 тысяч 10-15 думаю примерно будет стоить 21:08:40 вытащить из того проекта, вставить в ваш, потестить да вроде как оно 21:09:19 интересует только в этом модуле на определенное значение options в комбобоксе select можно накладывать блокировать 21:11:19 то есть статус в сделке нужно что бы менеджер не мог поставить отказ а только условный отказ 21:11:51 а роп мог поставить 21:12:36 21:12:55 такое по другому я бы предложил сделать 21:13:04 тоже было в одном проекте как ? 21:13:10 21:13:12 там статусы из редактирования вообще убирались 21:13:22 а в карочку вверху добавлялись кнопки 21:13:35 типа "Передать в продажи" 21:13:39 "принять в продажи" 21:13:44 "отметить продажу" 21:13:49 "Отказ в работе" 21:13:52 и так далее 21:14:05 ну кнопки, при нажатии на которые можно было в тот или иной статус перевести 21:14:17 и доступ к этим кнопкам делать только разрешенным пользователям 21:14:24 сейчас покажу как это выглядело 21:14:30 найду только ... )) ок 21:15:32 21:16:38 https://yadi.sk/i/FiqZkUxT3H7NtJ 21:16:42 ну вот например 21:16:47 сверху идет череда статусов 21:16:59 движение по воронке так же можно отобразить и прочее 21:17:06 показат текущий статус 21:17:12 показаты уже пройденные и что еще предстоит 21:17:37 и кнопка "Документы предоставлены" - если ее нажать - то задача перейдет в статус Анализ 21:17:42 и там будут свои кнопки 21:17:54 ну или "Завершить задачу" и тогда сразу в "Завершена" 21:18:12 и тогда можно прописать любую логику для этих кнопок 21:18:20 ну и наглядность движения по статусам повышается в разы 21:18:41 мне такая реализация в свое время понравилась. достаточно удобно имхо да решение интересное покажу начальству 21:18:56 21:18:56 но тут нужен программист, если в бизнес-логике что то поменяется да я понимаю 21:20:34 почту электронную можно вашу ? или скайп 21:21:02 21:22:45 admin@crmhosting.ru 21:22:53 skype: evgenjekson 21:22:57 +79109082123 21:23:19 +7 (499) 322 7403
  22. Алексей: Здравствуйте, столкнулся с проблемой, не отображаются изменения в дашлете лента событий не подскажете где копать ? 18:51:34 Евгений: 18:52:44 добрый день 18:52:57 а изменения в каком модуле? 18:53:01 поддерживаемом? 18:53:05 или в касмомном? 18:53:10 раньше они там были? нет в стандартном 18:53:18 18:53:25 просто там не все модули туда пихают свои изменения а только явно заданные просто вообще все изменения перестали отображаться в дашлете 18:53:40 18:53:42 ну возможно у вас каким то образом база данных накрылась 18:53:54 посмотрите настройки в админке 18:54:01 там есть пункт лента чего то 18:54:18 и там есть чекбоксы какие модули должны быть задействованы 18:54:23 может эта настройка слетела хорошо спасибо большое 18:54:31 18:54:47 )) Все заработало спасибо боьшое 19:06:21 19:06:28 отлично!
  23. Version

    100 downloads

    При создании руководства использованы следующие документы:  Руководства к SugarCRM CE версии 6.5, доступные по адресу: http://support.sugarcrm.com/Documentation/Sugar_Versions/6.5/CE  SuiteCRM Wiki: https://suitecrm.com/wiki/index.php/Main_Page В данном руководстве используется та же терминология, что и в языковом пакете «RUSSIAN RAPIRA Language Pack» для SuiteCRM версии 7.8, который доступен по адресу: https://github.com/likhobory/SuiteCRM7RU В описании интеграции SuiteCRM с внешним порталом (Joomla Advanced OpenPortal) используется та же терминология, что и в языковом пакете с ресурса https://crowdin.com/project/suitecrm- advanced-openportal/ru# Замечания и пожелания относительно данного перевода можно публиковать в русскоязычной теме на официальном форуме: https://suitecrm.com/forum/international-language-support/59
  24. В SuiteCRM содержит в себе предустановленные модули с открытым исходным кодом: Advanced OpenSales Advanced OpenSales (AOS) - результат непрерывной пятилетней работы. Этот набор модулей и функционалов является, пожалуй, наиболее значимым вкладом в развитие SugarCRM Community Edition. Использование вами этих модулей позволит вам также активно пользоваться Отчётами (Reporting) и Документооборотом (Workflow). Новый модуль с Категориями Продуктов В модуле Контракты списки товаров и услуг В списки товаров и услуг добавлены описания продуктов В списки товаро и услуг добавлены артикулы товаров, и теперь продукты можно искать по артикулу наравне с поиском по названию продукта Добавлена возможность указания скидок Стала доступна активация/деактивация PDF-шаблонов Новая панель администрирования: Включение/отключение группировки списков товаров и услуг Возможность задать стартовый порядковый номер для Предложений и Счетов Список возможностей AOS весьма большой, и увеличивается с каждым релизом SuiteCRM. AOS подходит для безопасного обновления, так как он полностью совместим с архитектурой SugarCRM в части механизма обновлений системы. Основные модули, входящие в AOS: Модуль Предложений Модуль с Товарами Модуль с Договорами Модуль с Счетами PDF-шаблоны Advanced OpenWorkflow Advanced OpenWorkflow (AOW) - это комплекс из модулей для настройки и автоматизации бизнес-процессов в SuiteCRM. AOW распространяется с полностью открытым исходным кодом (GPL3). AOW очень понятно сделан. Новые пользователи, которые совсем недавно познакомились с SuiteCRM, способны разобраться в функционале настройки Процессов за считанные минуты, и смогут весьма быстро начать производить настройки бизнес-процессов. AOW распространяется с открытым исходным кодом. Вы можете скачивать эти модули, изменять их и распространять по своему усмотрению. AOW проработанный. В течении 3-х месяцев его проверяли и тестировали на SugarCRM. В AOW хороший функционал. В этом комплексе есть все основные возможности, которые могут вам потребоваться для выполнения настройки бизнес-процессов в вашей CRM-системе. Advanced OpenWorkflow по своей внутренней структуре кода похожа на структуру SugarCRM. Это позволяет программистам из SugarCRM легко переносить в AOW наработки и легко разбираться в коде самого AOW дополняя его необхоимым функционалом. Advanced OpenReports Advanced OpenReports (AOR) комплекс модулей для построения отчётов в SugarCRM версий 5.x и 6.x с полностью открытым исходным кодом (GPL3). Интуитивно понятный интерфейс позволит быстро разобраться с особенностями построения модулей даже неопытным пользователям. Используя Advanced OpenReports вы сможете быстро получать необходимые вам срезы данных в виде таблиц, графиков или диаграмм. Данные вы сможете как просматривать прямо на страницах CRM-системы и в виде дашлетов на главной, так и выгружать их в виде CSV, HTML или PDF. Особенности Advanced OpenReports: Мощные и интуитивно понятные отчёты Большое количество используемых функций для получения и обработки данных Возможность запускать отчёты по расписанию Advanced OpenPortal Данный комплекс модулей позволяет подключить к вашей CRM-системе веб-портал, развернутый на Joomla. Интеграция достигается за счет компонента, устанавливаемого на портале и испльзующего SugarCRM REST API. CRM-система со своей стороны получает возможность принимать обращения с сайта и определенным образом обрабатывать их отдавая нужные ответы. Таким образом вы можете получить CRM-систему в виде хранилища данных о вашем бизнесе (клиенты, сделки, договора и так далее) и возложить на нее какие-либо сложные обработки этих данных (формирование отчетов, маркетинговые рассылки и так далее), а при помощи сайта (портала) на Joomla получить "красивый" интерфейс для работы с этими данными (или, как один из вариантов, предоставить своим клиентам вход в личный кабинет, содержащий в себе данные, находящиеся в CRM-системе). Advanced OpenEvents Advanced OpenPortal представляет собой Менеджер Событий для таких мероприятий, как семинары и вебинары. При помощи этого модуля вы сможете рассылать приглашения посетителям вебинаров через электронную почту, а также отслеживать, получили ли они ваши приглашения, посетили или нет предлагаемые им мероприятия. При помощи модуля События вы сможете: Создавать События Добавлять Участников в Событие используя Списки адресатов, Адресаты, Контакты и Предварительные контакты Создавать и назначать шаблоны писем для использования их в Событиях Рассылать в рамках того или иного События письма, сформированные по заданным шаблонам, в которых могут быть встроены возможности Согласиться или Отказаться от участия в предлагаемом Событии Получатели вашей рассылки могут Согласиться или Отказаться от предлагаемого им участия в Событии Когда предполагаемый Участник вашего События получит приглашение, согласится с участием в нем или откажется - статус этого приглашения автоматически поменяется в CRM-системе, и вы сможете оперативно это отслеживать Присутствует управление посещаемостью События: посетил мероприятие участник или нет Google Maps В SuiteCRM добавлена поддержка карт Google Maps. Присутствует возможность построения кеша геоданных по Контрагентам: из информации о Контрагенте извлекается его адрес, который отправляется на сервера Google Maps. На основании полученных данных от Google SuiteCRM может построить карту с обозначениями где какой контрагент находится. Присутствует возможность поиска Контрагентов по выделенным областям на карте и прочие гео-возможности. SecuritySuite В SuiteCRM добавлена возможность работы в командах: можно базу клиентов разбить на сегменты, и предоставлять доступ тем или иным сотрудникам только к определенным сегментам (например, по типу клиентов, или по их географическому расположению). Также были расширены стандартные возможности разграничения прав доступа пользователей. Advanced OpenTheme Специально для SuiteCRM была разработана тема оформления, которая совместима с SugarCRM 6.5.x Разработанная тема поддерживает следующие возможности: Cleaner UI Меню на AJAX/CSS3 с действиями для модулей и списком последних просмотров Меню быстрого создания записей Возможность перейти на редактирование недавно просмотренной записи в один клик
  25. CRM-система на базе SuiteCRM предлагает различные виды просмотров информации, хранимой в CRM-системе. Эти виды просмотров позволяют вам на разных этапах поиска предоставлять те или иные данные для просмотра и анализа. По сути это разные макеты данных, которые позволяют вам или смотреть списки данных, или смотреть более детальную информацию по той или иной записи, или редактировать ту или иную запись. Можно выделить три основных вида просмотра данных в SuiteCRM: Форма списка Форма просмотра Форма редактирования Каждая их этих форм имеет свое предназначение. Далее мы разберемся что эти формы обозначают и для чего они нужны. Форма списка Как правило когда вы переходите в тот или иной модуль через вернее меню, вы попадаете на список записей выбранного модуля. Внешний вид этого списка как раз и определяет вид просмотра "Форма списка". Форма списка позволяет выполнить достаточно много действий с записями модуля: Поиск записей - предоставляет возможность базового или расширенного поиска. Сортировка найденных записей - кликнув мышкой по названию той или иной колонки вы сможете таким образом отсортировать все найденные записи. Просмотр найденной записи - нажатие на любую гиперссылку в списке найденных записей приведет к просмотру выбранной записи. Редактирование найденной записи - нажатие на иконку карандаша приведет к тому, что вы попадете на форму редактирования выбранной записи. Удаление записей - вы можете выбрать несколько (или все) записи и в выпадающем списке возможных групповых действий выбрать действие "Удалить", что приведет к удалению выбранных записей. Массовое обновление - вы можете выбрать несколько записей, а затем выбрать "Массовое обновление", чтобы обновить данные для всех выбранных записей. Объединение - вы можете выбрать несколько записей и запустить механизм их объединения в одну запись. Достигается это путем выбора основной записи, а затем в нее можно добавить значения полей из других выбранных записей. Таким образом CRM-система позволяет провести процедуру дедупликации: удаления из CRM-системы дублей. Форма просмотра
×