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

All Activity

This stream auto-updates     

  1. Last week
  2. Добрый день! Подскажите, пожалуйста, каким образом я могу отправить письмо на электронный ящик указанный в Текстовом поле. Т.е. задача такая: у меня есть модуль в нем текстовое поле. При создании записи, я заполняю это текстовое поле каким-либо e-mail, и при сохранении записи происходит обработка процесса: отправка шаблона на e-mail указанный в этом текстовом поле. Есть идеи, каким образом это можно исполнить? Копал в сторону подстановки Адрес - Поле (условия обработки процесса) но не могу понять какие данные в форму Поле вводить. Буду благодарен за любые идеи!
  3. Earlier
  4. Как вы потом заливаете изменения из гита в конкретную папку веб-сервера?
  5. Я бы копал в сторону того, чтобы просто сделать поле не активным с помощью html-параметра.
  6. Добрый день! Общий принцип: исключаем файлы, которые являются автогенерируемыми (папка cache, папки Ext в /custom/modules) и так далее. Так же исключаем загружаемые файлы, историю изменений metadata-файлов, индексы поиска, служебные файлы разных GUI и прочее. Я сталкивался с тем, что некоторые системные администраторы пытаются засунуть в .gitignore core-файлы типа того, что находится в папках Zend, XTemplate и так далее обосновывая тем, что не надо эти файлы править, значит и в гите им делать нечего. Я против такого подхода. Считаю, что в git должны быть файлы, достаточные для корректного запуска SuiteCRM в том месте, где его развернули из git-репозитория. Вот файл .gitignore одного из проектов: *.log .DS_Store Thumbs.db /cache/* /.idea/* /config.php /config_override.php /tmp/ /upload/ /custom/screenshots/ /test_*.php /custom/blowfish/* /custom/history/* /custom/modulebuilder/* /custom/working/* /custom/application/Ext/ /custom/modules/*/Ext/ /custom/modules/unified_search_modules_display.php /modules/AOD_Index/Index/* /.htaccess /custom/client_secret.json /custom/appsactivity-php-quickstart.json Такой подход комфортно работает если изменения структуры модулей делать не в студии, а вручную. Поля, добавляемые в студии, вносят meta-данные (vardef) в таблицу в БД, а она через git не обновляется. А если будете расширять список полей вручную, то вполне сможете работать через git запуская быстрое восстановление. Не претендую на супер-универсальность и что это решение самое лучшее, но вот одна из реализаций, которой пользуемся наиболее часто: Ветки + сервера: В git-репозитории создаем ветку master. Эта ветка содержит состояние файлов на PROD-сервере В git-репозитории создаем ветку test. Эта ветка содержит состояние файлов на TEST-сервере В git-репозитории создаем произвольное кол-во веток с конкретными задачами, находящимися в работе (по 1-ой ветке на каждую задачу) Процесс работы: Берем очередную задачу в работу: из master делаем ветку с задачей. работаем в ней. Как правило это удаленная ветка, так как в процессе работы над задачей часто работают несколько человек. После выполнения задачи она окончательно пушится на сервер. Делается мерж ветки test и ветки с задачей. На тестовом сервере делаем pull ветки test и проверяем работу. Если все норм, то ветка с задачей мержится с веткой master Если что то не норм, то ветка с задачей просто уходит в доработку. Ветки test и master никогда не мержатся. Все происходит именно путем мержа ветки с задачей. P.S. надо видимо уведомления где то тут найти чтобы на почту приходили... а то задача то наверное уже не актуальна ... больше для тех, кто по поиску потом придет...
  7. Добрый день. Есть 3 сервера SuiteCRM: DEV, DEMO, PROD. Хотелось бы упростить (читай ускорить, в идеале автоматизировать) перенос доработок DEV => DEMO и DEMO => PROD. Есть идея использовать репозиторий для хранения файлов и забирать изменения из него. Но тут возникает сразу несколько вопросов: 1. Какие файлы надо исключить из контроля версий? 2. Как правильно отслеживать и применять изменения в базе? Есть опыт, что достаточно скопировать файлы .php и изменения в базе обработает "быстрое восстановление". Можно ли всегда так делать? 3. Насколько приемлем вариант разработки сразу на площадке DEV, без создания у себя локальной копии CRM? Как в этом случае вести одновременно несколько доработок? Просьба поделиться опытом в вопросе организации репозитория и контроля версий. А также других возможных способах упрощения переноса доработок между площадками. SuiteCRM 7.10.10, MySQL
  8. SpravkaCRM.ru

    Требования к коду

    Тут поднакопилось условных "ошибок" в написании кода. Это не синтаксические или логические ошибки, а именно внешний вид, читабельность и вообще ... Итак, продолжим! Всегда добавлять локализацию для добавляемых полей в модуле! Речь идет про ситуации, когда вы для реализации своих задач добавляете поле в тот или иной модуль и в параметре vname указываете лейбл добавляемого поля. Например так: $dictionary['Account']['fields']['custom_all_link'] = array ( 'name' => 'custom_all_link', 'vname' => 'LBL_CUSTOM_ALL_LINK', 'type' => 'varchar', 'inline_edit' => '', 'source' => 'non-db', 'studio' => 'visible', ); В данном примере мы в модуль Accounts добавляем некое поле custom_all_link, которое не будет хранить значение в базе данных, а скорее всего будет неким калькулируемым "на лету" ('source' => 'non-db'). Так вот если нигде в языковых файлах не описать связь $mod_strings['LBL_CUSTOM_ALL_LINK'] = 'Все ссылки'; то мало того, что мы увидим LBL_CUSTOM_ALL_LINK в месте отображения этого поля, но мы также увидим этот лейбл и в технических местах типа в списке полей в Отчетах или в Студии: И да! Это касается не только полей, но и связей: $dictionary['Account']['fields']['accounts_audit_link'] = array ( 'name' => 'accounts_audit_link', 'type' => 'link', 'relationship' => 'accounts_audit', 'vname' => 'LBL_ACCOUNTS_AUDIT', 'link_type' => 'one', 'module' => 'AccountsAudit', 'bean_name' => 'AccountsAudit', 'source' => 'non-db', ); Нам обязательно надо в файле локализации определить ключ LBL_ACCOUNTS_AUDIT! Использование функции strtotime(): Допустим вам надо получить дату меньше текущей на 3 дня. Вот такой подход "кривой": $old_date = time() - 259200; Сходу не понятно сколько это в днях/часах/минутах. А если еще и комментариев нет - считай совсем фигня фигней. Не удобно и не понятно. Вот так чуть лучше: $old_date = time() - 60*60*24*3; Ну хотя бы наглядно видно что тут происходит. А моя рекомендация: функция strtotime(): $old_date = strtotime("-3 day"); Или, если надо относительно какой то даты, а не текущего времени: $old_time = strtotime("-3 day", strtotime($date)); Любые переменные, перед их использованием, необходимо инициализировать! Если в коде где то планируется использовать переменные, которые будут дополняться данными в процессе работы текущего скрипта (наполнение списков, конкатенация строк), то в обязательном порядке необходимо вначале работы скрипта инициализировать эти переменные. Например: if(!empty($massiv_report[$i]['our_offer_percent_total'])) { $predlogenie_ot_totala[] = $massiv_report[$i]['our_offer_percent_total']; } Перед тем, как выполнить этот блок, в скрипте обязательно необходимо как то создать или получить переменную $predlogenie_ot_totala: или она в параметре была передана в функцию, или что то типа: $predlogenie_ot_totala = []; и ровным счетом наоборот должно происходить с объявлением новых LBL_xxx в модулях (в следующем пункте) Объявление LBL_xxx: $mod_strings = array ( 'LBL_SITE_ID' => 'id на сайте', 'LBL_DESCRIPTION' => 'Текущие договоренности', 'LBL_TYPE_OF_OWNERSHIP' => 'Форма собственности', 'LBL_URIST_NAME' => 'Полное наименование', 'LBL_INN' => 'ИНН', 'LBL_KPP' => 'КПП', 'LBL_BANK' => 'Банк', 'LBL_BIK' => 'БИК', 'LBL_KS' => 'Кор. Счет', 'LBL_RS' => 'Р. Счет', 'LBL_POSITION_DIR' => 'Должность руководителя', 'LBL_DIR' => 'ФИО Руководителя', 'LBL_CONTAKT_FIO_REAL' => 'ФИО контактного лица', 'LBL_POSITION' => 'Должность', 'LBL_PHONE_OFFICE' => 'Телефон', 'LBL_EMAIL' => 'E-mail:', ); Так нельзя делать в кастомных разделах добавления новых лейблов! Оно собой затрет все что там остальное будет! Надо добавлять примерно так: $mod_strings['LBL_SITE_ID'] = 'id на сайте'; $mod_strings['LBL_DESCRIPTION'] = 'Текущие договоренности'; $mod_strings['LBL_TYPE_OF_OWNERSHIP'] = 'Форма собственности'; $mod_strings['LBL_URIST_NAME'] = 'Полное наименование'; $mod_strings['LBL_INN'] = 'ИНН'; $mod_strings['LBL_KPP'] = 'КПП'; $mod_strings['LBL_BANK'] = 'Банк'; $mod_strings['LBL_BIK'] = 'БИК'; $mod_strings['LBL_KS'] = 'Кор. Счет'; $mod_strings['LBL_RS'] = 'Р. Счет'; $mod_strings['LBL_POSITION_DIR'] = 'Должность руководителя'; $mod_strings['LBL_DIR'] = 'ФИО Руководителя'; $mod_strings['LBL_CONTAKT_FIO_REAL'] = 'ФИО контактного лица'; $mod_strings['LBL_POSITION'] = 'Должность'; $mod_strings['LBL_PHONE_OFFICE'] = 'Телефон'; $mod_strings['LBL_EMAIL'] = 'E-mail'; Переменные внутри SQL-запросов: SQL-запрос изначально просто строка. И тут действуют все те правила, что можно отнести к программированию строковых переменных. Я про ситуации типа такой: $sql="SELECT `id`, `user_id7_c` FROM `accounts` WHERE `avance_date` = '$day_control' AND `deleted`='0' "; Здесь переменная $day_control вставлена не корректно. Да, возможно это будет работать. Возможно даже чаше будет работать, чем не работать. Но следует избегать подобных вставок переменных! Вместо подобного необходимо пользоваться конструкциями типа такой: $sql="SELECT `id`, `user_id7_c` FROM `accounts` WHERE `avance_date` = '".$day_control."' AND `deleted`='0' "; Или такой: $sql="SELECT `id`, `user_id7_c` FROM `accounts` WHERE `avance_date` = '{$day_control}' AND `deleted`='0' "; Подобный подход позволяет однозначно отделить код пхп от текста, чтобы интерпретатор абсолютно четко понимал где заканчивается название переменной и начинается текст. В случае с '$var' может это еще не так критично, но например привет$username_маленькаяжирнаяжопа уже будет не понятно где текст а где переменная. Передавать второй параметр true в запросах к БД через ->query: $result = $db->query($sql); Так НЕ ПРАВИЛЬНО $result = $db->query($sql, true); А так ПРАВИЛЬНО! Второй параметр указывает на то, что если во время выполнения SQL-запроса произойдет синтаксическая ошибка, то сценарий остановится (сработает функция die()). По-умолчанию второй параметр равен false, то есть если не указать второй параметр, то в случае кривого SQL-запроса сценарий продолжит свою работу. Это плохо тем, что усложняет отладку подобного кода: то вы сразу увидели проблему (страница не прогрузилась). А то не увидили, не все ситуации предусмотрели или в принципе SQL-запрос получился "кривым" - все это ушло на боевую систему и там будет работать криво. В общем о возможных проблемах надо знать сразу на этапе отладки, и чем ярче будет уведомление, тем лучше. Просто добавляйте везде , true "return true;" в любых задачах планировщика: Если вы пишите новую задачу для планировщика, то в конце функции необходимо обязательно поставить return true; Если функция планировщика не вернула true, то планировщик считает, что задача не была выполнена, и если стоит в настройках задачи галка "Выполнить, если пропущено":то планировщик будет запускать задачу не в 5 утра каждый день, а каждую минут, потому что предыдущий запуск задачи не был корректно выполнен (true то не вернула функция). Проверять наличие индексов в массиве перед их использованием: Если наличие индекса не однозначно и массив набивается из SQL-запроса или еще из каких пришедших данных, и структура массива не является жестко заданной константой - необходимо проверять наличие используемых индексов перед тем, как их использовать. Иначе будут вот такие ошибки валиться в лог PHP: Nov-2018 13:26:30 Europe/Moscow] PHP Notice: Undefined index: description in /home/euspenskiy/suitecrm/custom/modules/Accounts/auto_update_site_sunch.php on line 27 [08-Nov-2018 13:26:30 Europe/Moscow] PHP Notice: Undefined index: deleted in /home/euspenskiy/suitecrm/custom/modules/Accounts/auto_update_site_sunch.php on line 27 [08-Nov-2018 13:26:30 Europe/Moscow] PHP Notice: Undefined index: assigned_user_id in /home/euspenskiy/suitecrm/custom/modules/Accounts/auto_update_site_sunch.php on line 27 [08-Nov-2018 13:26:30 Europe/Moscow] PHP Notice: Undefined index: account_type in /home/euspenskiy/suitecrm/custom/modules/Accounts/auto_update_site_sunch.php on line 27 [08-Nov-2018 13:26:30 Europe/Moscow] PHP Notice: Undefined index: industry in /home/euspenskiy/suitecrm/custom/modules/Accounts/auto_update_site_sunch.php on line 27 [08-Nov-2018 13:26:30 Europe/Moscow] PHP Notice: Undefined index: annual_revenue in /home/euspenskiy/suitecrm/custom/modules/Accounts/auto_update_site_sunch.php on line 27 А иногда таких логов на пару мегабайт в минуту прилетает ... Наименование переменных. Даже не так: НАИМЕНОВАНИЕ ПЕРЕМЕННЫХ: Необходимо внимательно относится к тому, как вы называете свои переменные. Они должны быть понятными. Это самое главное! Чтобы одно название переменной в себе уже содержало информацию что находится внутри этой переменной: какой тип и примерно какое значение. И второе: старайтесь называть переменные всеже английскими словами без ошибок. Очень часто не обязательно лезть в словарь, а достаточно просто внимательнее приглядеться к коду. Ну например: $seedParticpaint= new s7_event_participants(); То, что тут используется "seed" - отлично! сразу понятно что речь пойдет про экземпляр класса. Но вот "Particpaint" - видно, что есть модуль "s7_event_participants" и в нем есть слово "participants", не "particpaint". Или: $dell_volue['module_parrent'] == 'Agend_Break' Нет с SuiteCRM слова "parrent", есть "parent". Да, согласен, это мелкие придирки, и в целом то код рабочий. Но мы ведь писали выше, что код должен быть не только рабочим ))) наверное "to be continue ..."
  9. Всем добрый день! Хочу рассказать об одной интересной работе, которую делали последнюю неделю и которую закрыли вот прям буквально сегодня. К нам обратилась некая компания, которая попросила показать демо-стенд реализации интеграции 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 (но это не точно )))) ) Презентация Ну и под конец размещаю видео с презентацией получившегося решения:
  10. Всем привет! Столкнулся с небольшой проблемой: 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, ), ), ), И вот что получилось после быстрого восстановления: То, что нужно!
  11. uid=48(apache) gid=48(apache) groups=48(apache) find /var/www/vhosts/crmhosting.ru/crm/public_html -type f | xargs chmod 0644 - команда не прошла : Много строчек с: chmod: cannot access '\320\260\320\273\321\214\320\261\320\276\320\274\320\275.jpg': No such file or directory После этого на сайте появляется: You don't have permission to access / on this server. Выставил на все файлы 777 - сайт опять заработал/ В таблицу стали заноситься данные. Галочка аудита не заработала. # modified: custom/Extension/modules/Buildings/Ext/Vardefs/sugarfield_about_owner.php # modified: custom/modules/Buildings/Ext/Vardefs/vardefs.ext.php Галочка аудита не заработала. Так же не работает.
  12. Hi nvnam1 ! You must copy the minimize code from include / javascript / yui / build / autocomplete / autocomplete.js in include / javascript / yui / build / autocomplete / autocomplete-min.js Data in the cache comes from there. After you need to delete the cache folder or js filles which contain YAHOO.widget.AutoComplete.prototype._updateValue function you might need to clear your browser cache. Sorry for bad english.
  1. Load more activity
×