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

SpravkaCRM.ru

Administrators
  • Content Count

    299
  • Joined

  • Last visited

  • Days Won

    7

Everything posted by SpravkaCRM.ru

  1. Все мы наверное сталкивались с проектами, в которых поучавствовало несколько разработчиков. Каждый разработчик привносил что то свое в проект, свой стиль написания кода, свои любимые приемы для реализации тех или иных задач. Из некоторых приёмов мы подчёрпывали что то новое с мыслями типа: Или такими: Но чем больше народу поучавствовало в проекте, тем больше хочется сказать: И не по тому, что отдельно взятые программисты плохи или хороши в своем деле (хотя и это тоже :-) ), а потому что все они пишут по разному! И когда очередной программист приходит на проект, он владеет информацией о исходном проекте SugarCRM/SuiteCRM, о том, как написан код разработчиками этой CRM. А вот стиль написания предыдущего программиста он не знает. Это делает код менее читабельным, а следовательно требует больше времени на его изучение и дальнейшую правку. Ну и в целом уменьшается комфортность сопровождения подобного проекта. По этому мы так и любим начинать проекты "с нуля": потому что там нет ЧУЖОГО НЕЧИТАБЕЛЬНОГО ГОВНОКОДА! Таким образом суть дальнейших инструкций: выработать у нашей команды единый стиль написания кода. Чтобы получив проект от своего коллеги вы не изучали разные уровни участия в нем других программистов по подобию колец на дереве. Чтобы все было для вас читабельным и максимально приближенным к комфортному сопровождению проекта. Дальнейшие инструкции выработаны на основании моего представления о внешнем виде кода, подкрепленного долгой практикой. Не являются конечной инстанцией. Предлагайте в комментариях свои варианты обосновав их. Итак: Комментарии Все мы знаем, что комментировать код надо. Его приятнее читать, когда ты можешь понять при помощи простого языка что же хотел тут сделать предыдущий программист. При должном уровне знания языка программирования конечно можно читать код, и сходу понимать что он делает. Но не всегда код является явным, названия переменных описывают их суть, а внезапные require_once вообще вносят некоторую сумятицу. Конечно не надо при этом делать из кода произведение и писать сочения для каждой строки кода. Но основные позиции по комментированию я укажу: Старайтесь комментировать любой логический блок! Описывайте в целом суть внутри дальнейших инструкций: /** * Получить PDF-версию протокола * @return mPDF */ public function getProtocolPDF() { // Проверяем версию Комитета if((int)date("Ymd", strtotime($this->fact_date)) < 20160601) { // Версия протокола до 1 июня 2016 года $pdf = $this->getProtocolPDFBefore20160601(); } return $pdf; } Всегда вносите описание для функций: должно быть в целом описание функции, перечисление и описание всех входных параметров и описание типа возвращаемых функцией данных: /** * Тут краткое описание функции, для чего она нужна * @param $var1 * @param $var2 * @param string $var3 * @return array */ public function testFunction($var1, $var2, $var3 = '') { // Тут тело функции $return_array = array(); // .... return $return_array; } Тоже самое касается в обязательном порядке и JavaScript-кода: /** * Тут краткое описание функции, для чего она нужна * @param var1 * @returns {*} */ function testFunction(var1) { // Тут тело функции var i; // .... return i; } В редакторе, в котором я пользуюсь для написания проектов (PHPStorm, возможно в других редакторах типа NetBeans тоже есть), есть удобная вещь для упрощения внесения описания для функций: если прям над функцией набрать /** и нажать на энтер, то весь код описания переменных и возвращаемого функцией значения сгенерится автоматически, и вам останется лишь дописать словами описание что же делает данная функция! Вот такой удобный лайфхак! Комментарии в SQL-запросах. Да да. Встречаются редко. Почти не встречаются. Но все мы знаем как выглядят названия таблиц и полей в них для связей. Особенно связей созданных новых модулей, где не какой-нибудь `account_id`, а куча полей типа `am_tasktemplates_am_projecttemplatesam_projecttemplates_ida`. А если еще модули и с длинными названиями ... Как правило комментарии в SQL-запросах применяются, когда весь SQL-запрос формируется в одном блоке кода, а не "размазан" по коду и собирается из кирпичиков. Удобно пользоваться комментариями именно на этапе написания кода. В итоговом сгенерированном SQL-запросе комментарии уже особо не нужны. Комментировать необходимо логические блоки подключения разных таблиц через INNER/LEFT JOIN, а также не явные условия в WHERE: // Получаем список активных Проектов с активными Сделками + их Контрагент // В проектах должны быть Проектные задачи, назначенные на текущего пользователя // И ряд условий $sql = " SELECT DISTINCT `accounts`.`id` AS `account_id`, `accounts`.`name` AS `account_name`, `opportunities`.`id` AS `opportunity_id`, `opportunities`.`name` AS `opportunity_name`, `opportunities`.`name` AS `opportunity_name`, `opportunities`.`conditions` AS `opportunity_conditions`, `opportunities`.`working_sheme`, `opportunities`.`jobprice_scope_of_work`, `opportunities`.`jobprice_monthly_requirement_type`, `opportunities`.`jobprice_monthly_requirement_hours`, `opportunities`.`jobprice_single_max_hours`, `opportunities`.`hourly_money_in_month`, `opportunities`.`hourly_scope_of_work`, `opportunities`.`hourly_hours_in_day`, `opportunities`.`hourly_cost`, `opportunities`.`hourly_single_max_hours`, `project`.`id` AS `project_id`, `project`.`name` AS `project_name`, `project`.`estimated_start_date` AS `project_start`, `project`.`estimated_end_date` AS `project_end`, `project`.`assigned_user_id` AS `project_assigned_user_id` FROM `project` # Подключаем Сделку INNER JOIN `projects_opportunities` ON `projects_opportunities`.`project_id` = `project`.`id` AND `projects_opportunities`.`deleted` = 0 INNER JOIN `opportunities` ON `opportunities`.`id` = `projects_opportunities`.`opportunity_id` AND `opportunities`.`status` = 'Active' AND `opportunities`.`deleted` = 0 # Подключаем Контрагента INNER JOIN `projects_accounts` ON `projects_accounts`.`project_id` = `project`.`id` AND `projects_accounts`.`deleted` = 0 INNER JOIN `accounts` ON `accounts`.`id` = `projects_accounts`.`account_id` AND `accounts`.`deleted` = 0 # Подключаем задачу INNER JOIN `project_task` ON `project_task`.`project_id` = `project`.`id` AND `project_task`.`deleted` = 0 AND `project_task`.`assigned_user_id` = '{$row_user['id']}' WHERE `project`.`deleted` = 0 # Нужны только Проекты с активным статусом AND `project`.`status` = 'Active' ORDER BY `opportunities`.`priority_number`, `project`.`estimated_start_date` "; Комментарии в Smarty-шаблонах считаю не обязательными, хотя и будут приятным дополнением: <span style="position: relative; top: 3px;"> {if $user.app.curentAction} {* Значек статуса задачи *} {if $user.app.curentAction.action == 'startTask'} {* В работе *} <span style="position: relative; top: -3px;"> <span class="label label-sm arrowed-right label-success" style="top: 1px;"> <span style="position:relative;top: 1px;">В процессе</span></span> </span> {elseif $user.app.curentAction.action == 'pauseTask'} {* На паузе *} <span style="position: relative; top: -3px;"> <span class="label label-sm arrowed-right" style="top: 1px;"> <span style="position:relative;top: 1px;">Приостановлено</span></span> </span> {/if} {if $user.app.curentAction.trelloURL} {* Ссылка на карту Trello *} <a href="{$user.app.curentAction.trelloURL}" target="_blank"><i class="fa fa-trello"></i></a> {/if} &quot;<A href="index.php?module=ProjectTask&action=DetailView&record={$user.app.curentAction.id}" target="_blank">{$user.app.curentAction.name}</A>&quot; <small>для</small> &quot;{$user.app.curentAction.accountName}&quot; <small>делает</small> {$user.app.curentAction.taskDuration} {if $user.app.curentAction.estimated_effort} <small>при расчетной длительности</small> {$user.app.curentAction.estimated_effort} {/if} {else} <span class="red">Задача не найдена</span> {/if} </span> SQL Запросы к базе данных так же необходимо оформлять таким образом, чтобы можно было их легко читать. Структура и перечень таблиц стандартных модулей как правило разработчикам более менее знакома. А вот привнесенные модули с их таблицами и полями как правило не известны другому программисту. И увеличение читабельности SQL-запросов позволит уменьшить барьер непонимания логики запроса. Да и читать подобные запросы намного приятнее: Самое первое и ОБЯЗАТЕЛЬНОЕ правило: обрамляйте ВСЕГДА названия таблиц и полей в обратные апострафы "`"! Это позволяет не только увеличить читабельность запроса, но и избежать ошибок SQL-синтаксиса да и в целом повышает отказоустойчивость кода. Используйте отступы для описания структуры SQL-запроса. При помощи кнопки Tab это легко делается, а IDE сама подстроится под нужный уровень вложенности когда после очередной строки вы нажмете Enter. Любая строка, относящаяся к той или иной SQL-конструкции, должна быть на один уровень глубже этой конструкции. Тоже самое касается сложных логических кострукций в WHERE, где при помощи скобок и AND или OR формируются сложные запросы: выделяйте внутренние скобки углубляя их: SELECT `nra_certificates`.`rate_lang_type`, `nra_certificates`.`count`, `nra_certificates`.`date_due`, `nra_certificates`.`date_deadline`, `nra_certificates`.`description` AS `certificate_description`, `ract_ratingactions_cstm`.`data_publ_reliza_c` AS `date_publ` FROM `nra_certificates` LEFT JOIN `opportunities_cstm` ON `opportunities_cstm`.`id_c` = `opportunities`.`id` WHERE (`nra_certificates`.`status` IN ('','pr','print','send_drb') OR `nra_certificates`.`status` IS NULL) AND ( `ract_ratingactions_cstm`.`rating_type_c` = `opportunities_cstm`.`last_rating_type_1_c` AND `opportunities_cstm`.`last_rating_status_1_c` NOT IN ('otozvan','priostanovlen') OR `ract_ratingactions_cstm`.`rating_type_c` = `opportunities_cstm`.`last_rating_type_2_c` AND `opportunities_cstm`.`last_rating_status_2_c` NOT IN ('otozvan','priostanovlen') ) AND `ract_ratingactions_cstm`.`data_publ_reliza_c` IS NOT NULL AND `nra_certificates`.`deleted` = 0 Используйте верхний регистр для написания SQL-команд. это позволит визуально легко отличать команды от названия таблиц и полей, что тоже способствует повышению читабельности SQL-запроса.
  2. 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 ..."
  3. Всем добрый день! Хочу рассказать об одной интересной работе, которую делали последнюю неделю и которую закрыли вот прям буквально сегодня. К нам обратилась некая компания, которая попросила показать демо-стенд реализации интеграции 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 (но это не точно )))) ) Презентация Ну и под конец размещаю видео с презентацией получившегося решения:
  4. Всем привет! Столкнулся с небольшой проблемой: 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, ), ), ), И вот что получилось после быстрого восстановления: То, что нужно!
  5. Добрый день! при помощи этой настройки можно регулировать нагрузку как на собственный сервер, так и на почтовый сервер. Большое количество писем, если сервер не очень мощный, может привести к нехватке памяти и все встанет. Так же некоторые почтовые сервера имеют ограничения на кол-во отправляемых через них писем в течении какого то кол-ва времени (в рамках борьбы со спамом или предлагая бизнес-тарифы). Для регулирования подобных вещей и применяется этот параметр
  6. Всем привет! Хочу показать мой вариант решения следующей проблемы: Есть бизнес-процесс, который настроен на модуль "Контрагенты". В результате срабатывания бизнес-процесса должны создаться две записи: Запись в модуле "Обращения" Запись в модуле "Документы" Но вся сложность заключается в том, что и Обращение и Документ должны не просто добавиться в Контрагент, но и между собой сформировать связь. То есть зайдя потом в карточку созданного Обращения мы должны увидеть в сабпанели созданный Документ! Если очень упрощенно, то мой бизнес-процесс выглядит так: Как вы видите, у меня в действиях отмечен чекбокс "Связать с записью в контролируемом модуле". Этот чекбокс позволяет созданным записям находится в соответствующих сабпанелях Контрагента. Но вот Обращение с Документом между собой никак не хотели связываться! Как я не тестировал, что не пробовал, но без доработки CRM-системы чисто имеющимися средствами эту задачу решить не получилось. Я пожалуй не буду утомлять рассуждениями на тему "что же тут делать и как я пришел к конечному результату", а просто опишу то, что в итоге было сделано в CRM-системе, что позволило решить эту задачу: 1. Первое: Добавляем в модуль Контрагенты два новых поля: custom/Extension/modules/Accounts/Ext/Vardefs/custom_fields.tmp.php: <?php $dictionary['Account']['fields']['tmp_document_id'] = array ( 'required' => false, 'name' => 'tmp_document_id', 'vname' => 'LBL_TMP_DOCUMENT_ID', 'type' => 'id', 'massupdate' => 0, 'no_default' => false, 'comments' => '', 'help' => '', 'importable' => 'true', 'duplicate_merge' => 'disabled', 'duplicate_merge_dom_value' => 0, 'audited' => false, 'inline_edit' => true, 'reportable' => false, 'unified_search' => false, 'merge_filter' => 'disabled', 'len' => 36, 'size' => '20', 'source' => 'non-db', ); $dictionary['Account']['fields']['tmp_document'] = array ( 'required' => false, 'source' => 'non-db', 'name' => 'tmp_document', 'vname' => 'LBL_TMP_DOCUMENT', 'type' => 'relate', 'massupdate' => 1, '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' => 'tmp_document_id', 'ext2' => 'Documents', 'module' => 'Documents', 'rname' => 'name', 'quicksearch' => 'enabled', 'studio' => 'visible', ); $dictionary['Account']['fields']['tmp_case_id'] = array ( 'required' => false, 'name' => 'tmp_case_id', 'vname' => 'LBL_TMP_CASE_ID', 'type' => 'id', 'massupdate' => 0, 'no_default' => false, 'comments' => '', 'help' => '', 'importable' => 'true', 'duplicate_merge' => 'disabled', 'duplicate_merge_dom_value' => 0, 'audited' => false, 'inline_edit' => true, 'reportable' => false, 'unified_search' => false, 'merge_filter' => 'disabled', 'len' => 36, 'size' => '20', 'source' => 'non-db', ); $dictionary['Account']['fields']['tmp_case'] = array ( 'required' => false, 'source' => 'non-db', 'name' => 'tmp_case', 'vname' => 'LBL_TMP_CASE', 'type' => 'relate', 'massupdate' => 1, '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' => 'tmp_case_id', 'ext2' => 'Cases', 'module' => 'Cases', 'rname' => 'name', 'quicksearch' => 'enabled', 'studio' => 'visible', ); custom/Extension/modules/Accounts/Ext/Language/ru_RU.TMP.php: <?php $mod_strings['LBL_TMP_DOCUMENT'] = 'TMP.DOCUMENT'; $mod_strings['LBL_TMP_DOCUMENT_ID'] = 'TMP.DOCUMENT_ID'; $mod_strings['LBL_TMP_CASE'] = 'TMP.CASE'; $mod_strings['LBL_TMP_CASE_ID'] = 'TMP.CASE_ID'; Обращаю ваше внимание на то, что данные поля имеют параметр 'source' => 'non-db', что говорит о том, что эти поля исключительно расчетные, и не хранят значения в базе данных. 2. Второе. Добавляю Hook на добавление связи Контрагента с чем-либо: custom/Extension/modules/Accounts/Ext/LogicHooks/tmp.hooks.php: <?php $hook_array['after_relationship_add'][] = Array( 10, 'TMP Fields', 'custom/modules/Accounts/TMPLogicHooks.php', 'TMPLogicHooks', 'setTMPFields' ); custom/modules/Accounts/TMPLogicHooks.php: <?php /** * Created by PhpStorm. * User: crmhosting * Date: 01.06.2018 * Time: 15:52 */ class TMPLogicHooks { /** * Заполняем временные поля значениями * @param $bean * @param $event * @param $arguments */ function setTMPFields($bean, $event, $arguments) { if($arguments['module'] == 'Accounts' AND $arguments['related_module'] == 'Cases') { // Если сейчас связь Контрагента и Обращения $bean->tmp_case_id = $arguments['related_bean']->id; } elseif ($arguments['module'] == 'Accounts' AND $arguments['related_module'] == 'Documents') { // Если сейчас связь Контрагента и Документа $bean->tmp_document_id = $arguments['related_bean']->id; } } } Таким образом мы во время связи Контрагента с Обращением или Документом заполняем наши вспомогательные поля. 3. Третье. Делаем быстрое восстановление. Чтобы в системе применились все добавленные поля и хуки. 4. Четвертое. В Бизнес-процессе в действии добавления Документа указываем связь с Обращением через нашу переменную tmp_case_id: Вот как теперь будет выглядеть бизнес-процесс: После срабатывания такого бизнес-процесса у создаваемого обращения появляется прямая связь с создаваемым документом. Что нам и было нужно. Давайте теперь немного объясню что же у нас получилось: Мы в Контрагенте создали пару полей. Но не простых, а типа relate - это когда в текущем модуле есть связь с записью в другом модул Мы в Контрагенты добавили хук after_relationship_add, который будет срабатывать каждый раз, когда к Контрагенту будет добавляться связь с записью другого модуля В момент, когда срабатывает бизнес-процесс, в первую очередь создается Обращение, а затем добавляется связь этого обращения с Контрагентом, что приводит к срабатыванию нашего хука Внутри хука мы видим, что добавляется связь Контрагента и Обращения. Мы добавляем в поле Контрагента tmp_case_id айди созданного и добавляемого Обращения Все, Обращение создано и привязано к Контрагенту Далее по бизнес-процессу создается Документ Документ мало того, что привязывается к Контрагенту, так мы в бизнес-процессе еще прописали ему некую связь с Обращениями, и указали, что связать он должен по полю TMP.CASE (а это как раз наше поле tmp_case_id, которое мы заполнили в прошлый раз, когда добавляли связь с Обращением) Таким образом создаваемый документ связывается с ранее созданным Обращением. Ну и связь двухсторонняя: если привязали Документ к Обращению, значит и Обращение привязано к Документу. Profit! Если что не понятно, или есть предложения по реализации данной задачи другими способами, то буду рад пообщаться в комментариях.
  7. Всем привет! Хочу поделиться одной доработкой, которая, на мой взгляд, немного полнее позволит понять роль модуля "Процессы" в работе с CRM-системой, и что не обязательно все на свете дописывать: можно еще и активно пользоваться встроенными средствами. Итак! Задача: в Обращениях выводить время последнего сообщения от Клиента. При этом выводить не только в карточке, но и в списках, отчетах и вообще везде, где заблагорассудится. Вот если задача была вывести только в карточке обращения, я наверное бы особо не заморачивался, и добавил расчётное поле source="non-db", в котором "на лету" определял последнее сообщение и подставлял его дату. Но тут задача получить универсальное поле. По этому решено было добавить полноценное поле в модуль "Обращения" (с хранением значения в базе данных) и заполнять его соответствующим значением. Добавляем поле (создаем файл custom/Extension/modules/Cases/Ext/Vardefs/last_reply_date.php): <?php $dictionary['Case']['fields']['last_reply_date'] = array ( 'name' => 'last_reply_date', 'vname' => 'LBL_LAST_REPLY_DATE', 'type' => 'datetimecombo', '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', 'size' => '20', 'enable_range_search' => false, 'dbType' => 'datetime', ); Добавляем локализацию для поля (custom/Extension/modules/Cases/Ext/Language/ru_RU.last_reply_date.php): <?php $mod_strings['LBL_LAST_REPLY_DATE'] = 'Последний ответ'; После выполнения быстрого восстановления в админке это поле появляется в списке полей модуля Обращения, и вы можете его использовать при формировании макетов. Далее наша задача это поле корректно заполнить. По задаче нам надо туда добавлять дату последного ответа клиента. Модуль с перепиской по Обращениям называется AOP_Case_Updates и в моей локализации по русски он звучит как "Обновления обращения". Чтобы выполнить нашу задачу нам надо при создании новой записи в этом модуле обновлять поле last_reply_date в записи родительского Обращения. Но так как в модуль "Обновления обращения" добавляются ответы как Клиента, так и сотрудника, ведущего переписку по Обращению, то нам надо определять дату только тогда, когда ответил клиент. Лично я определяю записи от клиентов отсутствием значения в поле `assigned_user_id` (если вы определяете это как то по другому, то пишите в комментариях, интересно будет узнать). Таким образом нам достаточно создать Бизнес-процесс, который будет привязан к модулю "Обновления обращения". В момент, когда будет создаваться новая запись, и в ней будет пусто поле `assigned_user_id` - это будет как раз наш случай, и мы должны будем обновить поле в родительском Обращении. Вот скрин этого Бизнес-процесса: Испытания показали, что данный механизм успешно работает. И небольшой бонус: То, что мы выше создали, будет исправно фиксировать дату последнего ответа для всех обращений, по которым клиенты только ответят с момента привнесения этого функционала. Далее я приведу листинг скрипта, который можно единоразово запустить в CRM-системе, и он найдет все Обращения, найдет все последние ответы клиентов по ним, и запишет дату: <?php if(!defined('sugarEntry'))define('sugarEntry', true); /** * Created by PhpStorm. * User: crmhosting * Date: 25.04.2018 * Time: 9:32 */ require_once('include/entryPoint.php'); global $db; // Получаем список Обращений $sql = " SELECT `cases`.`id` FROM `cases` WHERE `cases`.`deleted` = 0 "; $result = $db->query($sql, true); $counter = 0; while ($row = $db->fetchByAssoc($result)) { $counter++; // Получаем последнее сообщение, полученное от клиента $sql = " SELECT `date_entered` FROM `aop_case_updates` WHERE `deleted` = 0 AND `case_id` = '".$row['id']."' AND (`assigned_user_id` IS NULL OR `assigned_user_id` = '') ORDER BY `date_entered` DESC LIMIT 1 "; $date = $db->getOne($sql, true); if(!empty($date)) { // Если есть дата $sql = "UPDATE `cases` SET `last_reply_date` = '".$date."' WHERE `id` = '".$row['id']."'"; $db->query($sql, true); } } print_array("Всего записей звонков: " . $counter);
  8. Всем привет! В одном из проектов потребовалось формировать отчеты из данных, которые находятся в аудите. Напомню, что аудит - это табличка с информацией кто что поменял в карточке, вызываемая нажатием кнопки "Просмотр журнала изменений": Классная штука, конечно, этот аудит! Знай не забывай новые поля в него добавлять в студии... Но вот беда: работать с ним из модуля Отчеты нет возможности. Совсем. Если в кратце про аудит, то: Должен ли быть в модуле аудит полей или нет определяется в vardefs.php в параметре 'audited' для всего модуля: $dictionary['Account'] = array( 'table' => 'accounts', 'audited' => true, 'unified_search' => true, 'full_text_search' => true, 'unified_search_default_enabled' => true, 'duplicate_merge' => true, 'comment' => 'Accounts are organizations or entities that are the target of selling, support, and marketing activities, or have already purchased products or services', 'fields' => array( ................ Еще раз повторюсь, что это настройка, говорящая о принципиальной возможности работы с аудитом в текущем модуле. Вести аудит того или иного поля в модуле определяется также параметром 'audited' => true, в настройках того или иного поля: 'parent_id' => array( 'name' => 'parent_id', 'vname' => 'LBL_PARENT_ACCOUNT_ID', 'type' => 'id', 'required' => false, 'reportable' => false, 'audited' => true, 'comment' => 'Account ID of the parent of this account', ), Для хранения данных в базе данных используются таблицы с названием `модуль_audit` везде одинаковой структуры: Структура от модуля к модулю неизменна, потому что названия аудируемых полей хранятся в поле `field_name`, а значения поля в `before_value_string` и `after_value_string`. Нажимая "Быстрое восстановление" в админке мы восстанавливаем таблицы аудита в том числе там, где это необходимо (например, если добавили в модуль возможность аудировать записи). Так вот данные таблицы и все эти данные не являются объектами в понимании SuiteCRM, и к ним нет доступа из модуля AOR_Reports. По этому смотреть таблички - можем, строить аналитику по ним - нет. Решение задачи анализа изменения тех или иных полей в модуле на самом деле можно решить разными способами: Сделать аудит модулем (наш способ); Добавить в модуль для каждого поля, которое мы хотим наблюдать, еще одно поле-дублер, которое будет содержать предыдущее значение анализируемого поля. Например, нам надо видеть как поменялось поле "Статус" в модуле. Для этого мы можем добавить поле `status_before`, и в него при помощи тех же Hooks при смене поля "Статус" указывать предыдущее значение. В этом случае мы будем иметь прямой удобный доступ к `status_before` из отчетов, в студии (карточка записи, поиск по полю и т.д.). Но есть в этом подходе существенный минус: мы в дополнительном поле храним лишь предыдущее значение, историю изменений не получится хранить + для каждого нового поля, которое мы хотим анализировать, придется создавать доп.поля и настраивать их запись, что несколько не удобно. Можно добавить еще один модуль, в который мы будем записывать изменения значений в записях основного модуля. Например, при срабатывании Hooks. Кстати таким образом разработчики сделали хранение смены значений в модуле Обращения: AOP_Case_Events. Если грамотно реализовать этот метод, то в целом такой подход тоже может быть не плох. Но, например в AOP_Case_Events, явно указано какие поля необходимо фиксировать при смене в модуле Обращений. Это не удобно. Нужно постоянно привлекать разработчика, если захотите добавить новое поле в аудит. Плюс по сути это дубляж уже имеющихся в аудите данных. В общем не плохо, но аудит всеже лучше ))) Итак, давайте вернемся от теории к практике. На входе у нас есть таблица аудита, и нам надо из нее сделать модуль, при этом сохранив и не изменяя весь функционал аудирования, который уже есть. Я покажу на примере создания модуля аудирования для модуля Контрагенты. Модуль назовем AccountsAudit и по русски это будет "Аудит Контрагентов". Почти все файлы модуля будут лежать в /modules/AccountsAudit/, где структура папок и файлов будет следующей: Далее я приведу листинг всех этих файлов. modules/AccountsAudit/Dashlets/AccountsAuditDashlet/AccountsAuditDashlet.meta.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ if (!defined('sugarEntry') || !sugarEntry) { die('Not A Valid Entry Point'); } global $app_strings; $dashletMeta['AccountsAuditDashlet'] = array( 'module' => 'AccountsAudit', 'title' => translate('LBL_HOMEPAGE_TITLE', 'AccountsAudit'), 'description' => 'A customizable view into AccountsAudit', 'category' => 'Module Views' ); modules/AccountsAudit/Dashlets/AccountsAuditDashlet/AccountsAuditDashlet.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ if (!defined('sugarEntry') || !sugarEntry) { die('Not A Valid Entry Point'); } require_once('include/Dashlets/DashletGeneric.php'); require_once('modules/AccountsAudit/AccountsAudit.php'); class AccountsAuditDashlet extends DashletGeneric { function __construct($id, $def = null) { global $current_user, $app_strings; require('modules/AccountsAudit/metadata/dashletviewdefs.php'); parent::__construct($id, $def); if (empty($def['title'])) { $this->title = translate('LBL_HOMEPAGE_TITLE', 'AccountsAudit'); } $this->searchFields = $dashletData['AccountsAuditDashlet']['searchFields']; $this->columns = $dashletData['AccountsAuditDashlet']['columns']; $this->seedBean = new AccountsAudit(); } } modules/AccountsAudit/language/en_us.lang.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ $mod_strings = array ( 'LBL_ASSIGNED_TO_ID' => 'Assigned User Id', 'LBL_ASSIGNED_TO_NAME' => 'Assigned to', 'LBL_SECURITYGROUPS' => 'Security Groups', 'LBL_SECURITYGROUPS_SUBPANEL_TITLE' => 'Security Groups', 'LBL_ID' => 'ID', 'LBL_DATE_ENTERED' => 'Date Created', 'LBL_DATE_MODIFIED' => 'Date Modified', 'LBL_MODIFIED' => 'Modified By', 'LBL_MODIFIED_ID' => 'Modified By Id', 'LBL_MODIFIED_NAME' => 'Modified By Name', 'LBL_CREATED' => 'Created By', 'LBL_CREATED_ID' => 'Created By Id', 'LBL_DESCRIPTION' => 'Description', 'LBL_DELETED' => 'Deleted', 'LBL_NAME' => 'Name', 'LBL_CREATED_USER' => 'Created by User', 'LBL_MODIFIED_USER' => 'Modified by User', 'LBL_LIST_NAME' => 'Name', 'LBL_EDIT_BUTTON' => 'Edit', 'LBL_REMOVE' => 'Remove', 'LBL_LIST_FORM_TITLE' => 'Аудит Контрагентов List', 'LBL_MODULE_NAME' => 'Аудит Контрагентов', 'LBL_MODULE_TITLE' => 'Аудит Контрагентов', 'LBL_HOMEPAGE_TITLE' => 'My Аудит Контрагентов', 'LNK_NEW_RECORD' => 'Create Аудит Контрагентов', 'LNK_LIST' => 'View Аудит Контрагентов', 'LNK_IMPORT_ACCOUNTSAUDIT' => 'Импорт Аудит Контрагентов', 'LBL_SEARCH_FORM_TITLE' => ' Аудит Контрагентов', 'LBL_HISTORY_SUBPANEL_TITLE' => 'View History', 'LBL_ACTIVITIES_SUBPANEL_TITLE' => 'Activities', 'LBL_ACCOUNTSAUDIT_SUBPANEL_TITLE' => 'Аудит Контрагентов', 'LBL_NEW_FORM_TITLE' => 'New Аудит Контрагентов', ); modules/AccountsAudit/language/ru_RU.lang.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ $mod_strings = array ( 'LBL_ASSIGNED_TO_ID' => 'Ответственный(ая)', 'LBL_ASSIGNED_TO_NAME' => 'Ответственный(ая)', 'LBL_SECURITYGROUPS' => 'Группы пользователей', 'LBL_SECURITYGROUPS_SUBPANEL_TITLE' => 'Группы пользователей', 'LBL_ID' => 'ID', 'LBL_DATE_ENTERED' => 'Дата создания', 'LBL_DATE_MODIFIED' => 'Дата изменения', 'LBL_MODIFIED' => 'Изменено', 'LBL_MODIFIED_ID' => 'Изменено(ID)', 'LBL_MODIFIED_NAME' => 'Изменено', 'LBL_CREATED' => 'Создано', 'LBL_CREATED_BY' => 'Создано(ID)', 'LBL_DESCRIPTION' => 'Описание', 'LBL_DELETED' => 'Удалено', 'LBL_NAME' => 'Название', 'LBL_CREATED_USER' => 'Создано', 'LBL_MODIFIED_USER' => 'Изменено', 'LBL_LIST_NAME' => 'Название', 'LBL_EDIT_BUTTON' => 'Править', 'LBL_REMOVE' => 'Удалить', 'LBL_PARENT' => 'Контрагент', 'LBL_PARENT_ID' => 'Контрагент (ID)', 'LBL_DATE_CREATED' => 'Дата создания', 'LBL_FIELD_NAME' => 'Поле', 'LBL_DATA_TYPE' => 'Тип поля', 'LBL_BEFORE_VALUE_STRING' => 'Значение ДО', 'LBL_AFTER_VALUE_STRING' => 'Значение ПОСЛЕ', 'LBL_BEFORE_VALUE_TEXT' => 'Значение ДО (TEXT)', 'LBL_AFTER_VALUE_TEXT' => 'Значение ПОСЛЕ (TEXT)', 'LBL_LIST_FORM_TITLE' => 'Список аудита', 'LBL_MODULE_NAME' => 'Аудит Контрагентов', 'LBL_MODULE_TITLE' => 'Аудит Контрагентов', 'LBL_HOMEPAGE_TITLE' => 'Мой Аудит Контрагентов', 'LNK_NEW_RECORD' => 'Добавить строку аудита', 'LNK_LIST' => 'Просмотр Аудит Контрагентов', 'LNK_IMPORT_ACCOUNTSAUDIT' => 'Импорт Аудит Контрагентов', 'LBL_SEARCH_FORM_TITLE' => 'Фильтр Аудит Контрагентов', 'LBL_HISTORY_SUBPANEL_TITLE' => 'Просмотр истории', 'LBL_ACTIVITIES_SUBPANEL_TITLE' => 'Мероприятия', 'LBL_ACCOUNTSAUDIT_SUBPANEL_TITLE' => 'Аудит Контрагентов', 'LBL_NEW_FORM_TITLE' => 'Новый Аудит Контрагентов', 'LBL_ACCOUNTS_AUDIT' => 'Аудит', ); modules/AccountsAudit/metadata/subpanels/default.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ if (!defined('sugarEntry') || !sugarEntry) { die('Not A Valid Entry Point'); } $module_name = 'AccountsAudit'; $subpanel_layout = array( 'top_buttons' => array( array('widget_class' => 'SubPanelTopCreateButton'), array('widget_class' => 'SubPanelTopSelectButton', 'popup_module' => $module_name), ), 'where' => '', 'list_fields' => array( 'name' => array( 'vname' => 'LBL_NAME', 'widget_class' => 'SubPanelDetailViewLink', 'width' => '45%', ), 'date_modified' => array( 'vname' => 'LBL_DATE_MODIFIED', 'width' => '45%', ), 'edit_button' => array( 'vname' => 'LBL_EDIT_BUTTON', 'widget_class' => 'SubPanelEditButton', 'module' => $module_name, 'width' => '4%', ), 'remove_button' => array( 'vname' => 'LBL_REMOVE', 'widget_class' => 'SubPanelRemoveButton', 'module' => $module_name, 'width' => '5%', ), ), ); modules/AccountsAudit/metadata/dashletviewdefs.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ if (!defined('sugarEntry') || !sugarEntry) { die('Not A Valid Entry Point'); } global $current_user; $dashletData['AccountsAuditDashlet']['searchFields'] = array( 'date_entered' => array('default' => ''), 'date_modified' => array('default' => ''), 'assigned_user_id' => array( 'type' => 'assigned_user_name', 'default' => $current_user->name ) ); $dashletData['AccountsAuditDashlet']['columns'] = array( 'name' => array( 'width' => '40', 'label' => 'LBL_LIST_NAME', 'link' => true, 'default' => true ), 'date_entered' => array( 'width' => '15', 'label' => 'LBL_DATE_ENTERED', 'default' => true ), 'date_modified' => array( 'width' => '15', 'label' => 'LBL_DATE_MODIFIED' ), 'created_by' => array( 'width' => '8', 'label' => 'LBL_CREATED' ), 'assigned_user_name' => array( 'width' => '8', 'label' => 'LBL_LIST_ASSIGNED_USER' ), ); modules/AccountsAudit/metadata/detailviewdefs.php: <?php $module_name = 'AccountsAudit'; $viewdefs [$module_name] = array ( 'DetailView' => array ( 'templateMeta' => array ( 'form' => array ( 'buttons' => array ( 0 => 'EDIT', 1 => 'DUPLICATE', 2 => 'DELETE', 3 => 'FIND_DUPLICATES', ), ), 'maxColumns' => '2', 'widths' => array ( 0 => array ( 'label' => '10', 'field' => '30', ), 1 => array ( 'label' => '10', 'field' => '30', ), ), 'useTabs' => false, 'tabDefs' => array ( 'DEFAULT' => array ( 'newTab' => false, 'panelDefault' => 'expanded', ), ), ), 'panels' => array ( 'default' => array ( 0 => array ( 0 => 'id', ), 1 => array ( 0 => array ( 'name' => 'parent', 'studio' => 'visible', 'label' => 'LBL_PARENT', ), ), 2 => array ( 0 => 'date_created', ), 3 => array ( 0 => 'created_by_name', ), 4 => array ( 0 => array ( 'name' => 'field_name', 'label' => 'LBL_FIELD_NAME', ), ), 5 => array ( 0 => 'data_type', ), 6 => array ( 0 => 'before_value_string', ), 7 => array ( 0 => 'after_value_string', ), 8 => array ( 0 => 'before_value_text', ), 9 => array ( 0 => 'after_value_text', ), ), ), ), ); ?> modules/AccountsAudit/metadata/editviewdefs.php: <?php $module_name = 'AccountsAudit'; $viewdefs [$module_name] = array ( 'EditView' => array ( 'templateMeta' => array ( 'maxColumns' => '2', 'widths' => array ( 0 => array ( 'label' => '10', 'field' => '30', ), 1 => array ( 'label' => '10', 'field' => '30', ), ), 'useTabs' => false, 'tabDefs' => array ( 'DEFAULT' => array ( 'newTab' => false, 'panelDefault' => 'expanded', ), ), ), 'panels' => array ( 'default' => array ( 0 => array ( 0 => 'id', ), 1 => array ( 0 => array ( 'name' => 'parent', 'studio' => 'visible', 'label' => 'LBL_PARENT', ), ), 2 => array ( 0 => 'date_created', ), 3 => array ( 0 => 'created_by_name', ), 4 => array ( 0 => array ( 'name' => 'field_name', 'label' => 'LBL_FIELD_NAME', ), ), 5 => array ( 0 => 'data_type', ), 6 => array ( 0 => 'before_value_string', ), 7 => array ( 0 => 'after_value_string', ), 8 => array ( 0 => 'before_value_text', ), 9 => array ( 0 => 'after_value_text', ), ), ), ), ); ?> modules/AccountsAudit/metadata/listviewdefs.php: <?php $module_name = 'AccountsAudit'; $listViewDefs [$module_name] = array ( 'ID' => array ( 'width' => '32%', 'label' => 'LBL_ID', 'default' => true, 'link' => true, ), 'PARENT' => array ( 'type' => 'relate', 'studio' => 'visible', 'label' => 'LBL_PARENT', 'id' => 'PARENT_ID', 'link' => true, 'width' => '10%', 'default' => true, ), 'DATE_CREATED' => array ( 'width' => '20%', 'label' => 'LBL_DATE_CREATED', 'default' => true, 'link' => false, ), 'CREATED_BY_NAME' => array ( 'width' => '20%', 'label' => 'LBL_CREATED', 'default' => true, ), 'FIELD_NAME' => array ( 'width' => '20%', 'label' => 'LBL_FIELD_NAME', 'default' => true, 'link' => false, ), 'DATA_TYPE' => array ( 'width' => '20%', 'label' => 'LBL_DATA_TYPE', 'default' => true, 'link' => false, ), 'BEFORE_VALUE_STRING' => array ( 'width' => '20%', 'label' => 'LBL_BEFORE_VALUE_STRING', 'default' => true, 'link' => false, ), 'AFTER_VALUE_STRING' => array ( 'width' => '20%', 'label' => 'LBL_AFTER_VALUE_STRING', 'default' => true, 'link' => false, ), 'BEFORE_VALUE_TEXT' => array ( 'width' => '20%', 'label' => 'LBL_BEFORE_VALUE_TEXT', 'default' => true, 'link' => false, ), 'AFTER_VALUE_TEXT' => array ( 'width' => '20%', 'label' => 'LBL_AFTER_VALUE_TEXT', 'default' => true, 'link' => false, ), 'PARENT_ID' => array ( 'width' => '20%', 'label' => 'LBL_PARENT_ID', 'default' => false, 'link' => false, ), ); ?> modules/AccountsAudit/metadata/metafiles.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ $module_name = 'AccountsAudit'; $metafiles[$module_name] = array( 'detailviewdefs' => 'modules/' . $module_name . '/metadata/detailviewdefs.php', 'editviewdefs' => 'modules/' . $module_name . '/metadata/editviewdefs.php', 'listviewdefs' => 'modules/' . $module_name . '/metadata/listviewdefs.php', 'searchdefs' => 'modules/' . $module_name . '/metadata/searchdefs.php', 'popupdefs' => 'modules/' . $module_name . '/metadata/popupdefs.php', 'searchfields' => 'modules/' . $module_name . '/metadata/SearchFields.php', ); modules/AccountsAudit/metadata/popupdefs.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ if (!defined('sugarEntry') || !sugarEntry) { die('Not A Valid Entry Point'); } $module_name = 'AccountsAudit'; $object_name = 'AccountsAudit'; $_module_name = 'accountsaudit'; $popupMeta = array( 'moduleMain' => $module_name, 'varName' => $object_name, 'orderBy' => $_module_name . '.name', 'whereClauses' => array( 'name' => $_module_name . '.name', ), 'searchInputs' => array($_module_name . '_number', 'name', 'priority', 'status'), ); modules/AccountsAudit/metadata/quickcreatedefs.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ $module_name = 'AccountsAudit'; $viewdefs[$module_name]['QuickCreate'] = array( 'templateMeta' => array( 'maxColumns' => '2', 'widths' => array( array('label' => '10', 'field' => '30'), array('label' => '10', 'field' => '30') ), ), 'panels' => array( 'default' => array( array( 'name', 'assigned_user_name', ), ), ), ); modules/AccountsAudit/metadata/searchdefs.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ $module_name = 'AccountsAudit'; $searchdefs[$module_name] = array( 'templateMeta' => array( 'maxColumns' => '3', 'maxColumnsBasic' => '4', 'widths' => array('label' => '10', 'field' => '30'), ), 'layout' => array( 'basic_search' => array( 'name', array('name' => 'current_user_only', 'label' => 'LBL_CURRENT_USER_FILTER', 'type' => 'bool'), ), 'advanced_search' => array( 'name', array( 'name' => 'assigned_user_id', 'label' => 'LBL_ASSIGNED_TO', 'type' => 'enum', 'function' => array('name' => 'get_user_array', 'params' => array(false)) ), ), ), ); modules/AccountsAudit/metadata/SearchFields.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ if (!defined('sugarEntry') || !sugarEntry) { die('Not A Valid Entry Point'); } $module_name = 'AccountsAudit'; $searchFields[$module_name] = array( 'name' => array('query_type' => 'default'), 'current_user_only' => array( 'query_type' => 'default', 'db_field' => array('assigned_user_id'), 'my_items' => true, 'vname' => 'LBL_CURRENT_USER_FILTER', 'type' => 'bool' ), 'assigned_user_id' => array('query_type' => 'default'), //Range Search Support 'range_date_entered' => array('query_type' => 'default', 'enable_range_search' => true, 'is_date_field' => true), 'start_range_date_entered' => array( 'query_type' => 'default', 'enable_range_search' => true, 'is_date_field' => true ), 'end_range_date_entered' => array( 'query_type' => 'default', 'enable_range_search' => true, 'is_date_field' => true ), 'range_date_modified' => array('query_type' => 'default', 'enable_range_search' => true, 'is_date_field' => true), 'start_range_date_modified' => array( 'query_type' => 'default', 'enable_range_search' => true, 'is_date_field' => true ), 'end_range_date_modified' => array( 'query_type' => 'default', 'enable_range_search' => true, 'is_date_field' => true ), //Range Search Support ); modules/AccountsAudit/metadata/studio.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ /** * This file adds support for studio */ modules/AccountsAudit/AccountsAudit.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ require_once('include/SugarObjects/templates/audit/Audit.php'); class AccountsAudit extends Audit { public $new_schema = true; public $module_dir = 'AccountsAudit'; public $object_name = 'AccountsAudit'; public $table_name = 'accounts_audit'; public $importable = false; public $id; public $name; public $date_entered; public $date_modified; public $modified_user_id; public $modified_by_name; public $created_by; public $created_by_name; public $description; public $deleted; public $created_by_link; public $modified_user_link; public $assigned_user_id; public $assigned_user_name; public $assigned_user_link; public $SecurityGroups; public $parent; public $parent_id; public $date_created; public $field_name; public $data_type; public $before_value_string; public $after_value_string; public $before_value_text; public $after_value_text; public function bean_implements($interface) { switch($interface) { case 'ACL': return true; } return false; } } modules/AccountsAudit/Menu.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ if (!defined('sugarEntry') || !sugarEntry) { die('Not A Valid Entry Point'); } global $mod_strings, $app_strings, $sugar_config; if(ACLController::checkAccess('AccountsAudit', 'edit', true)){ $module_menu[]=array('index.php?module=AccountsAudit&action=EditView&return_module=AccountsAudit&return_action=DetailView', $mod_strings['LNK_NEW_RECORD'], 'Add', 'AccountsAudit'); } if(ACLController::checkAccess('AccountsAudit', 'list', true)){ $module_menu[]=array('index.php?module=AccountsAudit&action=index&return_module=AccountsAudit&return_action=DetailView', $mod_strings['LNK_LIST'],'View', 'AccountsAudit'); } modules/AccountsAudit/vardefs.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ $dictionary['AccountsAudit'] = array( 'table' => 'accounts_audit', 'audited' => false, 'inline_edit' => true, 'duplicate_merge' => false, 'fields' => array ( 'parent' => array ( 'required' => false, 'source' => 'non-db', 'name' => 'parent', 'vname' => 'LBL_PARENT', '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' => 'parent_id', 'ext2' => 'Accounts', 'module' => 'Accounts', 'rname' => 'name', 'quicksearch' => 'enabled', 'studio' => 'visible', ), 'accounts_audit_link' => array ( 'name' => 'accounts_audit_link', 'type' => 'link', 'relationship' => 'accounts_audit', 'vname' => 'LBL_ACCOUNTS_AUDIT', 'link_type' => 'one', 'module' => 'Accounts', 'bean_name' => 'Account', 'source' => 'non-db', ), 'created_by_link' => array( 'name' => 'created_by_link', 'type' => 'link', 'relationship' => 'accounts_audit_created_by', 'vname' => 'LBL_CREATED_BY_USER', 'link_type' => 'one', 'module' => 'Users', 'bean_name' => 'User', 'source' => 'non-db', ), ), 'relationships' => array ( 'accounts_audit' => array ( 'lhs_module' => 'AccountsAudit', 'lhs_table' => 'accounts_audit', 'lhs_key' => 'parent_id', 'rhs_module' => 'Accounts', 'rhs_table' => 'accounts', 'rhs_key' => 'id', 'relationship_type' => 'one-to-one', 'readonly' => false, 'deleted' => true, 'relationship_only' => false, 'for_activities' => false, 'is_custom' => true, 'from_studio' => true, 'relationship_name' => 'accounts_audit', ), 'accounts_audit_created_by' => array( 'lhs_module' => 'Users', 'lhs_table' => 'users', 'lhs_key' => 'id', 'rhs_module' => 'AccountsAudit', 'rhs_table' => 'accounts_audit', 'rhs_key' => 'created_by', 'relationship_type' => 'one-to-many' ), ), 'optimistic_locking' => true, 'unified_search' => false, ); if (!class_exists('VardefManager')) { require_once('include/SugarObjects/VardefManager.php'); } VardefManager::createVardef('AccountsAudit', 'AccountsAudit', array('audit')); Это были основные базовые (почти все) файлы модуля, без которых это все просто не будет работать. Файлы в себе не содержат по сути особо никакой логики, и просто должны быть, чтобы обеспечить необходимый функционал нового модуля. Обращаю ваше внимание на файлы AccountsAudit.php и vardefs.php: мы в них указали, что таблица нашего модуля будет называться "accounts_audit", то есть наша уже существующая таблица с аудированием. Таким образом мы просто вокруг уже существующей таблицы развернули модуль, который будет использовать ее для своей работы. Далее нам необходимо заявить в CRM-системе, что у нас появился новый модуль и объявить как он называется. Для этого в папке custom/Extension/application/Ext/Include/ добавляем файл AccountsAudit.php: <?php //WARNING: The contents of this file are auto-generated $beanList['AccountsAudit'] = 'AccountsAudit'; $beanFiles['AccountsAudit'] = 'modules/AccountsAudit/AccountsAudit.php'; $moduleList[] = 'AccountsAudit'; ?> А в папке custom/Extension/application/Ext/Language/ добавляем файл en_us.AccountsAudit.php <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ $app_list_strings['moduleList']['AccountsAudit'] = 'Аудит Контрагентов'; и файл ru_RU.AccountsAudit.php: <?php /** * * SugarCRM Community Edition is a customer relationship management program developed by * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc. * * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd. * Copyright (C) 2011 - 2017 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the * Free Software Foundation with the addition of the following permission added * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along with * this program; if not, see http://www.gnu.org/licenses or write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road, * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not * reasonably feasible for technical reasons, the Appropriate Legal Notices must * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM". */ $app_list_strings['moduleList']['AccountsAudit'] = 'Аудит Контрагентов'; Но это пока не все. Как выяснилось в процессе работы над заданием, в модуле отчетов мало указать связь модуля AccountsAudit -> Accounts! Необходимо еще "прокинуть" и обратную связь: Accounts -> AccountsAudit. Для этого добавляем файл /custom/Extension/modules/Accounts/Ext/Vardefs/auditFieds.php с следующим содержанием: <?php $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', ); $dictionary['Account']['relationships']['accounts_audit'] = array ( 'lhs_module' => 'AccountsAudit', 'lhs_table' => 'accounts_audit', 'lhs_key' => 'parent_id', 'rhs_module' => 'Accounts', 'rhs_table' => 'accounts', 'rhs_key' => 'id', 'relationship_type' => 'one-to-one', 'readonly' => false, 'deleted' => true, 'relationship_only' => false, 'for_activities' => false, 'is_custom' => true, 'from_studio' => true, 'relationship_name' => 'accounts_audit', ); Здесь мы добавили поле с линком на модуль AccountsAudit и добавили связь с AccountsAudit в модуле Контрагенты. Ну вот и все! Выполняем быстрое восстановление (там система предложит что то поменять в структуре таблицы accounts_audit, помоему добавить поле `deleted`, но это не страшно и никак не повлияет на функциональность аудирования), добавляем новый модуль "Аудит Контрагентов" в админке в "Настройка отображения закладок и субпанелей" и теперь у нас есть такой модуль, котором мы можем теперь полноценно пользоваться и в меню, и в Студии, и в Отчетах, и в Процессах и вообще:
  9. Добрый день! Попробуйте зайти в модуль с шаблонами и зайдите в любой первый попавшийся шаблон. Затем в строке браузера поменяйте его айдишник на 9f0d0646-1bf4-3da9-500a-5ad6dcec5796 и попробуйте загрузить страницу. Что будет? Пустая страница или загрузится шаблон?
  10. Добрый день! У вас не корректно настроены права доступа к файлам. SuiteCRM не может редактировать/добавлять их. Для начала посмотрите от имени кого запускается ваш сервер: echo exec("id"); Посмотрите, все ли файлы в проекте от этого пользователя и от этой группы? Выправить ситуацию с настройками доступа можно примерно так: chown -R 48:48 /var/www/vhosts/crmhosting.ru/crm/public_html/ find /var/www/vhosts/crmhosting.ru/crm/public_html -type d | xargs chmod 0755 find /var/www/vhosts/crmhosting.ru/crm/public_html -type f | xargs chmod 0644 подставляем только свои значения и пути. Если не помогло, то есть еще одна особенность в SuiteCRM по настройке доступа к файлам и папкам. В config.php: 'default_permissions' => array ( 'dir_mode' => 1533, 'file_mode' => 436, 'user' => 'apache', 'group' => 'apache', ), попробуйте тут явно задать юзера и группу (обычно они пустые в этом конфиге). Или "поиграйтесь" с цифрами. Например такие попробовать: 'default_permissions' => array ( 'dir_mode' => 1528, 'file_mode' => 432, 'user' => 'crmhosting', 'group' => 'staff', ), Попробуйте, потом расскажите что получилось )) В восстановлении оно лишь таблицу для аудита анализирует (есть или надо создать). Находится ли отдельно взятое поле в аудите или нет - это задача совсем других механизмов, а быстрому восстановлению отношения не имеющих.
  11. Добрый день! На скриншете не доработанная версия отчета по плановой загрузке сотрудника. Это не какой то отдельный модуль. Это CRM-система для разработчика: то, что я сам себе для своей компании пилю. Фактически сейчас там отображается плановое количество рабочих часов по дням для текущего сотрудника. Уже не помню что хотел вложить в этот отчет. Сейчас с точки зрения контроля работы сотрудников более удобным образом можно наблюдать следующую информацию: Слева распределение по сотрудникам сколько фактически отработано часов в день. Справа распределение фиктического относительно планового кол-ва часов. Тут календарь рабочих нагрузок для определения объема занятости сотрудника. Собирает в себе плановые нагрузки и фактически назначенные. Тут сублимированная информация по сотруднику из разных областей его деятельности. А тут у нас расчетная ведомость, где список фактически выполненных работ и суммарная стоимость к выплате сотрудникам в зависимости от выполненных работ. Как видите информацию в можно очень по разному представить к выводу. Я отталкивался от наиболее удобного мне формата для решения той или иной задачи. Вряд ли подобные отчеты можно найти где то уже готовыми, уж больно это все индивидуально. Потратив немного бюджета можно запилить именно то, что Вам будет удобно. Это не очень дорого, но очень удобно использовать.
  12. Если говорить про каждого отдельного сотрудника, то тогда уж можно лучше сделать графики прямо в карточке сотрудника. Открыл сотрудника - там панель с графиком. Типа такого: И не нужно плодить кучу дашлетов на главной.
  13. На сколько я помню логика работы SuiteCRM подразумевает возможность работы с несколькими полями с типом "валюта", но валюта будет одна. То есть все значения будут в рублях, долларах, или любой другой присутствующей в CRM валюте. Но хранение нескольких полей в разной валюте в рамках одной карточки не предусмотрено. Обусловлено тем, что все добавленные поля ссылаются на едиснтвенное поле, которое будет обозначать код валюты. То есть добавляем первое поле с валютой в карточку: формируется спец.поле, хранящее код валюты. Добавляем второе поле - оно будет получать код валюты из первого поля. И так далее. Сделать мультивалютность внутри одной карты можно, но это уже программировать.
  14. Всем привет! Столкнулся только что с ситуацией, когда в карточку Контрагента добавили поле с пользователем (связанная запись из 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 Что уже то, что нам нужно...
  15. Всем привет! Давайте разберем ситуацию, когда нам необходимо выполнить единоразово какую то задачу в 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 выполнит наш скрипт единоразово в планировщике. В списке задач планировщика ничего не появится лишнего. Можно подобным образом запускать задачу необходимое количество раз.
  16. Всем привет! Просто небольшая заметка, но может кому пригодиться... Иногда бывает, что в карточке модуля на панели необходимо разместить некую информацию, но 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 для нашего поля перестал вообще отображаться!
  17. Всем привет! В последних на текущий момент (апрель 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, и вуа-ля:
  18. Решил по одному проекту сделать сортировку записей в сабпанели. Добавил кнопки, повесил 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', ), ), );
  19. Еще одно хорошее бесплатное решение отчетов и графиков - выгрузка данных в CSV. Вы эту "простынку" добавляете в виде первой вкладки в Excel-файл. А на остальных вкладках на основании данных из первой вкладки у вас уже строятся графики, диаграммы, формулы разные и прочее. Подобная схема неплохо сработала в одной компании, когда очень много логики в большом количестве отчетов нужно было. SuiteCRM просто не справлялся с логикой этой. А вот Excel вполне себе справлялся. Делаете заготовку, которая должна брать за основу данные в первой вкладке. А потом туда просто выгружаете нужные вам данные. Ну или как еще один вариант - интеграция с Google Sheets. Это из отчета данные сразу выгружать в таблицу на Google Drive. Есть такая доработка, наверное надо будет ее вытащить в виде модуля....
  20. Добрый день! Модуль с отчетами иногда глючит. Из версии в версию они там иногда фиксят, но иногда приходится подправлять и переделывать. Именно графики лично в моей практике заказывают и используют не очень часто. Как правило просто цифровых данных в таблицах вроде хватает. Сам пробовал для своих нужд использовать графики, но тоже что то не получалось, уже правда не помню что. Богатого опыта получения стандартных графиков к сожалению нет. Мне иногда проще запилить график подключив Google Charts, нежели пытаться разобраться что там в этих отчетах понапридумано. Функционал достаточно сложный, и не всегда сходу разобраться получается. Что там в конкретно вашем случае и вашей версии SuiteCRM - хз. Наверное, если вы по всякому попробовали и не получилось, значит или и правда глючит или не предусмотрено изначально.
  21. Добрый день! Указанное вами поле может содержать ссылку внешний документ, ассоциированный с записью в модуле "Документы". В SugarCRM Pro была (и думаю есть) возможность не только загружать файлы в саму CRM, но и создавать записи по документам, находящимся на других сервисах (Google Drive и так далее). Так вот это поле содержит id документа на внешнем сервисе, до которого можно добраться по API этого сервиса. В интерфейсе SugarCRM CE а далее в SuiteCRM я не встречал возможности указать расположение файла во внешнем сервисе (в прошке была возможность прямо в CRM-системе указать что файл лежит в Google Drive и можно было загрузить туда файл или указать уже ранее загруженный). Но, однако, в SuiteCRM часть этого функционала осталась. Во всяком случае поле есть (doc_id) и даже есть функции, которые при беглом осмотре вроде как ссылаются на некую возможность использования API внешних систем. Хз как с этим раборать, может на досуге разберусь... Попробуйте в "Источник" указать "Sugar", а в "Источник документа (ID)" указать ссылку на файл в гугль драйве. Как стандартными средствами загрузить кучу файлов в систему хз. У каждого документа (записи в модуле Documents) должна быть связанная запись в модуле DOcumentRevisions. и ID этой записи = название файла в папке upload То есть заходите в документ, смотрите ID записи в панели "История" (или как там она может называться в вашей локализации), и вот нужно чтобы в папке upload лежал файл, название которого = этому ID (без расширений и прочего) как автоматизировать этот процесс? наверное только непостредственно через программирование
  22. Добрый день! Попробуйте сделать восстановление ролей в админке в пункте Восстановление. Эта штука добавляет новые модули в кеш доступных действий. Без этого восстановления новый модуль будет доступен (скорее всего) только админам. Если не получится будем дальше думать.
  23. Всем привет! Хочу поделиться методом, который позволяет добавлять фильтры в список записей модуля, когда надо найти все записи, у которых есть связи с определенной записью из другого модуля. Сразу оговорюсь, что 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 Подобным образом можно заложить любую логику поиска нужной записи. Пользуйтесь )))
  24. А не пробовал завести учетку в Исходящих в админке? Или в настройках в модуле Email указать учетную запись... при отправке письма на сколько я помню можно указать через какую исходящую отправлять...
×