Разработка
14
Июн
26

Фильтрация полей Lookup в CRM 4, эпизод II

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

Но есть и другие способы повесить фильтр на лукап… рассмотрим парочку…

Подмена лукапа

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

  • Создайте в папке ISV сайта CRM подпапку с именем customlookup;
  • Скопируйте файл <сайт CRM>\_controls\lookup\lookupsingle.aspx в только что созданную папку ISV\CustomLookup;
  • Добавьте следующий код на копию страницы lookupsingle.aspx:
    <script runat="server">
    	protected override void OnLoad( EventArgs e )
    	{
    		base.OnLoad(e);
    		crmGrid.PreRender += new EventHandler( crmgrid_PreRender );
    	}
    	void crmgrid_PreRender( object sender , EventArgs e )
    	{
    		//Поскольку мы не хотим испортить другие лукапы, проверяем, что параметр search начинается с <fetch
    		if (crmGrid.Parameters["search"] != null && crmGrid.Parameters["search"].StartsWith("<fetch")) {
    			crmGrid.Parameters.Add("fetchxml", crmGrid.Parameters["search"]);
    			crmGrid.Parameters.Remove("searchvalue");
    			//Чтобы юзверы не смогли создать новую запись из лукапа и выбрать ее – скрываем кнопку создания новой записи
    			this._showNewButton = false;
    		}
    	}
    </script>
    
  • Добавьте следующий скрипт на onload нужной формы:
    // Подменяем стандартный диалог лукапа на "новый"
    function ReplaceCrmLookup() {
    	window.oldOpenStdDlg = window.openStdDlg;
    	window.openStdDlg = function() {
    		arguments[0] = arguments[0].replace(/\/_controls\/lookup\/lookupsingle.aspx/i, '/ISV/customlookup/lookupsingle.aspx');
    		return oldOpenStdDlg.apply(this, arguments);
    	};
    };
    ReplaceCrmLookup();
    
    // Фильтруем данные лукапа Головная организация
    var field = crmForm.all.parentaccountid; 
    
    //Отключаем поле поиска в диалоговом окне лукапа
    field.lookupbrowse = 1; 
    
    //Передаем fetch xml через параметр поиска лукапа
    field.AddParam("search",
    	"<fetch mapping='logical'>" +
    		"<entity name='account'>" +
    			"<filter>" +
    				"<condition attribute='name' operator='eq' value='Газпром' />" +
    			"</filter>" +
    		"</entity>" +
    	"</fetch>"
    );
    
  • Первая часть кода ответсвенна за подмену стандартного диалогового окна лукапа на новый (тот что мы скопировали). Ну, а вторая часть аналогична предыдущей статье – включает фильтрацию посредством Fetch-запроса;
  • Сохраняем и публикуем 🙂



Преимущество этого метода заключается в том, что у него гораздо меньше шансов быть «убитым» роллапом 🙂

Первоисточник: http://danielcai.blogspot.com/2009/09/crm4-filtered-lookup.html

Фильтрация с помощью плагина

А теперь рассмотрим три похожих между собой способа фильтрации данных с помощью плагина. Но прежде чем начнем, маленькое отступление… Все ниже перечисленные способы требуют одной нвстройки – разрешения передавать дополнительные параметры в строке запроса в CRM. Чтобы ее включить откройте ветку реестра HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSCRM и создайте в ней DWORD ключ DisableParameterFilter со значением 1.


Отношения N:1 (лукап)

Довольно простой метод не требующий пространственных пояснений… просто:

  • Откройте Plugin Registration Tool и зарегистрируйте в CRM сборку Agile.Crm4.Plugins.FilteredLookup.dll. Для нее зарегистрируйте шаг на событие Execute, стадия Pre-stage;
  • На онлоад нужной формы и для нужного поля добавьте параметр filters содержащий часть Fetch-запроса (в части фильтрации), например, такой:
    crmForm.parentaccountid.AddParam('filters', '<filters><filter entity="account"><condition attribute="name" operator="eq" value="Gazprom" /></filter></filters>');
    

    В данном случаи для лукапа Головная организация (parentaccountid) формы Бизнес-партнера отбираются записи название которых равно «Gazprom»;

  • Все 🙂 публикуем и смотрим 🙂


З.Ы. Сорсы: MSCRMFilteredLookup.zip

Как Вы понимаете, этому методу (как и двум последующим) грозит еще меньшая опасность быть «убитым» ролапом 🙂

Отношения 1:N

Как и в предыдущем пункте, решение состоит из двух частей: плагина и скрипта (но на этот раз скрипт гораздо более мудрёный 🙂 ):

  1. Скрипт создает кастомное диалоговое окно лукапа и передает какой-либо параметр в строке запроса;
  2. Плагин «возмет» переданный в строке запроса параметр и изменит FetchXml, подставив в него этот параметр.

Для пример произведем фильтрацию мульти-лукапа доступного в связанном представлении Дочернии организации (на левой навигационной панели формы Бизгес-партнера) по кнопке Добавить существующий объект Бизнес-партнера. Приступим:

  • На онлоад нужной формы вешаем такой код:
    var relId = "account_parent_account"; // Название отношений
    var lookupEntityTypeCode;
    var bClick = "navSubAct"; // id кнопки на левой навигационной панели
    var iFrameLoad = "areaSubAccts"; // Значение параметра передаваемое функции loadArea при клике (атрибут onclick) на кнопке на левой навигационной панели 
    var iFrameName = "areaSubAcctsFrame"; // id iFrame'а в котором открывается связанное представление
    var bTitle = "Добавить к этой записи существующий объект Бизнес-партнер"; // Значение атрибута title кнопки по которой открывается мульти-лукап
    var addParam = "&n=Gazprom"; // Дополнительный параметр который необходимо передать в строке запроса
    
    // Получаем ссылку на пункт левого меню и добавляем ему обработчик события onClick
    var navId = document.getElementById(bClick);   
    navId.onclick = function()
    {	
    	// Выполняем стандартный код назначенный на пункт меню
    	loadArea(iFrameLoad);
    	
    	// Получаем ссылку на iFrame, в котром будет отображаться связанное представление после щелчка на левой навигационной панели
    	var areaId = document.getElementById(iFrameName);
    	
    	// Добавляем iFrame'у обработчик события "изменение состояния"
    	areaId.onreadystatechange = function() 
    	{
    		// Если состояние не равно "complete" прекращаем выполнение
    		if (areaId.readyState != "complete") return;
    		
    		// Получаем все теги LI из загруженного iframe'а
    		var frame = frames[window.event.srcElement.id];  
    		var li = frame.document.getElementsByTagName("li");    
    
    		// Просматриваем все теги LI...
    		for (var i = 0; i < li.length; i++)
    		{
    			// ... и ищем среди них тот, в котором атрибут Title содержит название искомой кнопки
    			var title = li[i].getAttribute("title");
    			
    			if(title != null && title == bTitle)
    			{
    				// Подменяем событие onClick в кнопке для открытие "кастомного лукапа" (вызов функции CustomLookup) вместо стандартного
    				var action = li[i].getAttribute("action");
    				lookupEntityTypeCode = action.substring(action.indexOf("\(")+1, action.indexOf(","));
    				li[i].onclick = CustomLookup1N;
    				break;
    			}
    		}
    	}
    }
    
    function CustomLookup1N()
    {
    	// Формируем стандартный URL мульти-лукапа
    	var lookupSrc = "/" + ORG_UNIQUE_NAME + "/_controls/lookup/lookupmulti.aspx?class=&objecttypes=" + lookupEntityTypeCode + "&browse=0";
        
    	// Добавляем к URL диалогового окна мульти-лукапа дополнительный параметр
    	lookupSrc += addParam;
    	
    	// Стандартная JS-функции из файла \_static\_grid\action.js
    	var lookupItems = window.showModalDialog(lookupSrc, null, 'DialogWidth=600px; DialogHeight=460px; dialogLeft=0; dialogLeft=0; dialogTop=0; help=no; status=yes');
        if (lookupItems) {
            if (lookupItems.items.length > 0) {
                var commandAssociate = new RemoteCommand("AssociateRecords", "AssociateOneToMany");
    
                var i = 0;
                var objs = lookupItems.items;
                var iLength = objs.length;
    
                for (i = 0; i < iLength; ++i) {
                    commandAssociate.SetParameter("childType", lookupEntityTypeCode);
                    commandAssociate.SetParameter("childId", objs[i].id);
                    commandAssociate.SetParameter("parentType", crmFormSubmit.crmFormSubmitObjectType.value);
                    commandAssociate.SetParameter("parentId", crmFormSubmit.crmFormSubmitId.value);
                    commandAssociate.SetParameter("relationshipName", relId);
    
                    if (!commandAssociate.Execute().Success) {
                        break;
                    }
                }
            }
    
            try {
                auto(lookupEntityTypeCode);
            }
            catch (e) {
            }
        }
    }
    

    Соответственно, Вам необходимо изменить значения переменных заданных в начале (их Вы можете получить либо с помощью Internet Explorer Developer Toolbar, либо на форме настроек отношений).
    Этот код делает следующее:

    • Подменяет событие клика по ссылке на левой навигационной панели на вызов пользовательской функции;
    • После вызова функции выполняется код аналогичный тому что происходит при стандартном клике на ссылке (т.е. происходит загрузка iFrame’а);
    • Затем отслеживаем событие загрузки iFrame’а и когда его статус будет равен «complete», продолжаем выполнение кода;
    • Находим в iFrame’е все теги <li> (именно им кодируется кнопка);
    • Просматриваем все отобранные теги <li> и находим среди них тот, который имеет атрибут title с нужным нам содержимым (названием кнопки);
    • Подменяем событие клика на «найденной» кнопке для вызова кастомной функции;
    • Эта функции формирует URL аналогичный стандартному диалогу мульти-лукапа + дополнительный параметр;
    • Далее открывается модальное окно мульти-лукапа по этой URL. После выбора записей происходи вызов стандартных функции CRM для связывания выбранных записей с текущей.
  • Далее создаем новый плагин, подписываем его и подключаем SDK’ашные сборки;
  • Вешаем на него такой код:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.IO;
    using System.Web;
    using System.Web.Services;
    using Microsoft.Crm.Sdk;
    
    namespace CRMExecuteEvent
    {
        public class CRMExecuteEvent : IPlugin
        {
            public void Execute(IPluginExecutionContext context)
            {
                // Если в строке запроса не передана переменная n, то прекращаем выполнение
                if (HttpContext.Current.Request.QueryString["n"] == null) return;
    
                // Поучаем значение переменной n
                string name = HttpContext.Current.Request.QueryString["n"].ToString();
    
                try
                {
                    // Если контекст выполнения не содержит во входящих параметрах FetchXml, то прекращаем выполнение
                    if (!context.InputParameters.Contains("FetchXml")) return;
    
                    string beforeXml = (String)context.InputParameters["FetchXml"];
    
                    // Если FetchXml выполнения не содержит имени целевого объекта, то прекращаем выполнение
                    if (!beforeXml.Contains("<entity name=\"account\">") && !beforeXml.Contains("xml-platform")) return;
    
                    // Измениям Fetch-запрос
                    string afterXml =
                        "<fetch version='1.0' page='1' count='100' output-format='xml-platform' mapping='logical'>" +
                            "<entity name='account'>" +
                                "<attribute name='accountid' />" +
                                "<attribute name='name' />" +
                                "<attribute name='createdon' />" +
                                "<order attribute='name' />" +
                                "<filter type='and'>" +
                                    "<condition attribute='statecode' operator='eq' value='0' />" +
                                    "<condition attribute='name' operator='eq' value='" + name + "' />" +
                                "</filter>" +
                            "</entity>" +
                        "</fetch>";
    
                    // Подменяем Fetch-запрос на новый
                    context.InputParameters["FetchXml"] = beforeXml.Replace(beforeXml, afterXml);
                }
                catch (System.Web.Services.Protocols.SoapException ex)
                {
                    throw new InvalidPluginExecutionException("An error occurred in the CRM plug-in.", ex);
                }
            }
    
        }
    }
    

    Этот код перехватывает Fetch-запрос, ищет в строке URL требуемый кастомный параметр (в данном случаи это «n») и если находит его подменяет Fetch-запрос на новый;

  • Регистрируем сборку в CRM (с помощью Plugin Registration Tool) на событие Execute, стадия Pre-stage;
  • Ну, и любуемся результатом 🙂




Аналогичным способом можно, кстати, передавать параметры и для LookupSingle.aspx 🙂

Отношения N:N

Этот способ на 90% процентов повторяет предыдущий, т.к. в нем также задействуется мульти-лукап, но в этот раз для отношений N:N. Отличие заключается в коде кастомной функции, которая подменяет стандартный лукап.

  • Также копируем на онлоад этот код и изменяем в нем исходные параметры (в соответствии с вашими потребностями):
    var relId = "new_account_account"; // Название отношений
    var lookupEntityTypeCode;
    var bClick = "navnew_account_account"; // id кнопки на левой навигационной панели
    var iFrameLoad = "areanew_account_account"; // Первое значение параметра передаваемое функции loadArea при клике (атрибут onclick) на кнопке на левой навигационной панели...
    var iFrameLoad2 = "\x26roleOrd\x3d1"; // ... второе значение
    var iFrameName = "areanew_account_accountFrame"; // id iFrame'а в котором открывается связанное представление
    var bTitle = "Добавить к этой записи существующий объект Бизнес-партнер"; // Значение атрибута title кнопки по которой открывается мульти-лукап
    var addParam = "&n=Gazprom"; // Дополнительный параметр который необходимо передать в строке запроса
    
    // Получаем ссылку на пункт левого меню и добавляем ему обработчик события onClick
    var navId = document.getElementById(bClick);   
    navId.onclick = function()
    {	
    	// Выполняем стандартный код назначенный на пункт меню
    	loadArea(iFrameLoad, iFrameLoad2);
    	
    	// Получаем ссылку на iFrame, в котром будет отображаться связанное представление после щелчка на левой навигационной панели
    	var areaId = document.getElementById(iFrameName);
    	
    	// Добавляем iFrame'у обработчик события "изменение состояния"
    	areaId.onreadystatechange = function() 
    	{
    		// Если состояние не равно "complete" прекращаем выполнение
    		if (areaId.readyState != "complete") return;
    		
    		// Получаем все теги LI из загруженного iframe'а
    		var frame = frames[window.event.srcElement.id];  
    		var li = frame.document.getElementsByTagName("li");    
    
    		// Просматриваем все теги LI...
    		for (var i = 0; i < li.length; i++)
    		{
    			// ... и ищем среди них тот, в котором атрибут Title содержит название искомой кнопки
    			var title = li[i].getAttribute("title");
    			
    			if(title != null && title == bTitle)
    			{
    				// Подменяем событие onClick в кнопке для открытие "кастомного лукапа" (вызов функции CustomLookup) вместо стандартного
    				var action = li[i].getAttribute("action");
    				lookupEntityTypeCode = action.substring(action.indexOf("\(")+1, action.indexOf(","));
    				li[i].onclick = CustomLookupNN;
    				break;
    			}
    		}
    	}
    }
    
    function CustomLookupNN()
    {
    	// Формируем стандартный URL мульти-лукапа
    	var lookupSrc = "/" + ORG_UNIQUE_NAME + "/_controls/lookup/lookupmulti.aspx?class=&objecttypes=" + lookupEntityTypeCode + "&browse=0";
        
    	// Добавляем к URL диалогового окна мульти-лукапа дополнительный параметр
    	lookupSrc += addParam;
    	
    	// Подменяем стандартную URL на новую
    	var lookupItems = window.showModalDialog(lookupSrc, null, 'DialogWidth=600px; DialogHeight=460px; dialogLeft=0; dialogLeft=0; dialogTop=0; help=no; status=yes');
    	if (lookupItems)  // Стандартная JS-функция из файла \_static\_grid\action.js
    		if ( lookupItems.items.length > 0 )
    			AssociateObjects( crmFormSubmit.crmFormSubmitObjectType.value, crmFormSubmit.crmFormSubmitId.value, lookupEntityTypeCode, lookupItems, true, null, relId);
    }
    
  • В данном случае я создал новое отношение N:N карточки Бизнес-партнера на саму себя;
  • Далее также необходимо создать плагин (на собыет Execute, стадии Pre-stage), который отловит необходимы Fetch-запрос и изменит его. Для примера использую тот же чамый плагин что и в предыдущем пункте (поэтому если что смотрите его код);
  • Готово 🙂 смотрим на мульти-лукап 🙂



Комментарии (26)
  • Николай 14.06.2010

    если поле nvarchar то ставится operator=’eq’ , а если поле lookup тогда какой оператор надо?

  • slivka_83 14.06.2010

    Думаю тоже eq 🙂 просто Вам нверно нужно будет подставлять GUID записи в после = 🙂 А вообще используйте для составления fetch запросов это http://mmcrm.ru/?p=987 или это http://mmcrm.ru/?p=494 🙂 там сразу же сможете и попробовать подставить тестовые данные 🙂

  • Игорь 14.06.2010

    Подскажите, для отношения N:1, как написать на Fetch такую фильтрацию:
    Select a.1Id, b.2Id
    from dbo.1Base a, dbo.2Base b
    where a.HID=b.IID
    FetchXML Wizard не поддерживает такой вариант.

  • slivka_83 14.06.2010

    Единственно что приходит на ум — это написать свое aspx-приложение, которое делает SQL запрос напрямую в БД и возвращает результат 🙂

  • rurik 14.06.2010

    Здравствуйте, воспользовался вашим плагином для связи n:n . У меня возникли проблемы с работой плагина на Execute. поставил плагин, смотрю HttpContext.Current.Request.QueryString там {}.
    и плагин заканчивает работу. это 17ая строка в коде плагина(я имею ввиду в коде плагина в этой статье)

  • rurik 14.06.2010

    в чем может быть проблема, что не передается параметров? ключ в реестре создал как написано.

  • slivka_83 14.06.2010

    Добрый день!

    Проверьте разрешение на передачу данных в URL таким способом http://mmcrm.ru/?p=507

  • rurik 14.06.2010

    Спс, проверил.Скажите пожалуйста, данный плагин+скрипт фильтруют ПОИСК ?(поиск в уже появившейся вьюшке,это скрин №6 в данной статье для N:N ,наверху вьюшки)

    и в каком месте кода осуществляется фильтрация этого поиска?? покажите будьте добры)

  • slivka_83 14.06.2010

    Вроде бы нет… плагин перехватывает запрос и подменяет его на свой… хотя наверно это можно доработать… но я не уверен 🙂

  • rurik 14.06.2010

    я думаю это вполне можно доработать,собственно чем я сейчас и пытаюсь заниматься:) я немного изменил схему: в скрипте я вместо вызова доп параметра «n=» (коего потом должен перехватывать плагин) я вывожу заранее сделанную вьювку уже с нужным фильтром(фильтр сделан стандартными средствами при создании вьвки) а плагин у меня работает именно на этот поиск. На данный момент он фильтрует все как надо ,НО кроме нужной вьвки он отрабатывает на все любые поиски необходимой сущности. последнее очень плохо. Встает вопрос: как заставить плагин реагировать только на нужное место(вьвку я имею ввиду)???

  • slivka_83 14.06.2010

    Возмити эту утилитку http://mmcrm.ru/?p=1000 и запустите ее при вызове мульти-лукапа и посика в нем — может надете какие-нибудь уникальные параметры 🙂

  • slivka_83 14.06.2010

    Возмити эту утилитку http://mmcrm.ru/?p=1000 и запустите ее при вызове мульти-лукапа и посика в нем — может надете какие-нибудь уникальные параметры 🙂

  • Warsch 14.06.2010

    Добавлю про раздел «Отношения N:1 (лукап)»:
    Если нужно сделать какую-то выборку в зависимости от GUID, то надо изменить скрипт на такой:

    var filterparam = '';
    crmForm.primarycontactid.AddParam('filters',filterparam);
    

    Этот пример позволяет выбрать основным контактом организации только одного из сотрудников этой орагнизации

  • Александр 14.06.2010

    Добрый день!

    Подскажите какой вариант лучше использовать для фильтрации, пример:
    Необходимо фильтровать товары, по категориям при добавлению к заказу.
    У каждого товара есть своя категория и у каждого пользователя прописано, какие категории ему доступны. Необходимо фильтровать товары при добавлении таким образом, чтобы отображались только товары, категории которых, прописаны в учетках пользователей. Если проще, то — из всех товаров необходимо выделить товары к примеру по двум или трем категориям. Использование псевдофильтра или жесткого поиска не подходит((
    Построить соответствующие fetch-запросы, для выбора необходимых продуктов, не проблема, остается вопрос по реализации, какой из вариантов фильтрации с помощью плагинов подойдет, так как кроме всего изложенного выше, необходимо использовать стандартный поиск по выбранным продуктам (стандартный поиск по продуктам, отфильтрованных по категориям)))

    Буду признателен, за советы:)

  • Александр 14.06.2010

    Разобрался, в моем случае
    использовал «Отношения N:1 (лукап)», для передачи сложного Fetch использовал структуру:

    crmForm.productid.AddParam('filters',"<filters><entity name='product'>"+
    "<condition attribute='statecode' operator='eq' value='0'/>"+
    "<link-entity name='do_cat' from='do_catid' to='do_catid' alias='productdo_catiddo_catdo_catid'><filter type='and'><condition attribute='do_blid' operator='in'>"+
    "<value  uitype='do_cat'>[ID нужной категории]</value>"+
    "<value  uitype='do_cat'>[ID нужной категории]</value>"+
    </condition></filter></link-entity></entity></filters>");
    

    Соответственно получаю ID нужных категории через SOAP запрос и в цикле

    "<value  uitype='do_bl'>[ID нужной категории]</value>"+
    

    формирую структуру Fetch фильтра

  • Igor 14.06.2010

    Подскажите, как отключить возможность внесения данных в lookup до открытия окна поиска? Т.к. если в lookup прописать значение не соответсвующее фильтру, после нажатия TAB оно все равно подхватывается ( касается отношения N:1 плагин)

  • slivka_83 14.06.2010

    Добрый день.

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

  • Greg 14.06.2010

    Существует ли нормальный способ фильтрации лукапа, в тот момент, когда используется фильтр самого лукапа? в этот момент у нас нет никаких параметров, которые могли сообщить плагину, что необходимо применитё фильтрацию.

    Вариант отключить поле фильтра хороший, но фильтрация происходит и в момент сортировки данных.

  • Александр 14.06.2010

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

  • Greg 14.06.2010

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

    Теперь я получил данные в лукапе, и в самом окне лукапа применяю фильтр или поиск BrowseLookup. Фильтр в правом верхнем углу лукапа.

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

  • slivka_83 14.06.2010

    Если Вы хотите совместить свою (неподдерживаемую) фильтрацию и стандартный поиск лукапа, то, насколько мне известно, такого способа вроде бы нет 🙂

  • Александр 14.06.2010

    Для «Отношения N:1 (лукап)» с помощью плагина можно использовать предварительную фильтрацию (при этом стандартный поиск будет работать по данным, которые отобраны предварительной фильтрацией).

  • Макс 14.06.2010

    Добрый день, возможен ли вариант для отношения 1:N
    фильтрации городов по области выбранной на главной форме? Либо по определенному выбранному параметру поля на городе?

  • slivka_83 14.06.2010

    Здрасьте.
    Что значит для 1:N? Для самого представления или для лукапа множественного выбора?

  • Макс 14.06.2010

    1:N Для лукапа множественного выбора.

  • slivka_83 14.06.2010

    для четверки было такое решение http://mmcrm.ru/?p=1230

*

code