Цель: создать кастомную форму "Расширенная история записи" и включить в меню Tools (Сервис). Будет показывать расширенную информацию из HR по текущей записи - Ф.И.О., департамент, должность и т.д.
Для работы нам понадобится ПО Oracle Developer Suite 10. Сразу оговорюсь, что эта IDE в теории позволяет запускать формы на выполнение через Java-апплет в браузере. На практике это сделать так и не удалось (были перепробованы JInitiator 1.3.22, 18, 26) с изменением содержимого formsweb.cfg. Так что этот вопрос остается открытым.
Этап 1: модификация формсового меню. Скачиваем $AU_TOP/resource/RU/FNDMENU.mmb и открываем в Oracle Developer Suite 10:
FRM-30085: Unable to adjust form for output. Форма слишком велика, либо элементы выходят за границы формы.
FRM-47023:No such parameter named G_QUERY_FIND exists in form. Хитрая ошибка, которая поначалу вводит в тупик, потому как G_QUERY_FIND - стандартный параметр, который есть всегда. Имеет смысл проверить тип данных параметров. Даже если передается дата, то тип данных должен быть Char. После этой манипуляции проблема ушла.
ORA-06502: PL/SQL: numeric or value error: character string buffer too small. По умолчанию размер поля вывода ограничен 30 символами. Эта ошибка появится, если переменная будет содержать строковое значение большей длины. Имеет смысл поставить 32 000.
Для работы нам понадобится ПО Oracle Developer Suite 10. Сразу оговорюсь, что эта IDE в теории позволяет запускать формы на выполнение через Java-апплет в браузере. На практике это сделать так и не удалось (были перепробованы JInitiator 1.3.22, 18, 26) с изменением содержимого formsweb.cfg. Так что этот вопрос остается открытым.
Этап 1: модификация формсового меню. Скачиваем $AU_TOP/resource/RU/FNDMENU.mmb и открываем в Oracle Developer Suite 10:
Добавляем новый пункт в самый конец списка.
Прописываем блок с вызываемым триггером в окне, открывшемся из контекстного меню PL/SQL Editor. Позже мы создадим этот триггер.
:global.menu_to_appcore := 'ADVANCED_RECORD_HISTORY'; execute_trigger('menu_to_appcore');Сохраняем файл, заливаем на сервер:
frmcmp_batch module=FNDMENU.mmb userid=apps/apps output_file=$AU_TOP/resource/RU/FNDMENU.mmx compile_all=special module_type=menu ... - Inserting menu FNDMENU. - Inserting menu FILE. - Inserting menu EDIT. - Inserting menu VIEW. - Inserting menu FOLDER. - Inserting menu SPECIAL. - Inserting menu SPECIAL_B. - Inserting menu SPECIAL_C. - Inserting menu HELP. - Inserting menu QUERY. - Inserting menu DIAGNOSTICS. - Inserting menu TRACE_MENU. - Inserting menu LOGGING_MENU. - Inserting menu PROPERTIES_MENU. - Inserting menu CUSTOM_CODE_MENU. - Inserting menu CLEAR. - Inserting menu DUPLICATE. - Inserting menu PREFERENCES. - Inserting menu RECORD. - Ready.После перезахода в Forms увидим наш пункт меню:
Если политика партии запрещает правку FNDMENU.mmb (как в нашем случае), то придется добавлять пункт меню динамически. Для этого:
- добавляем код в обработчик EVENT библиотеки CUSTOM.pll. Эта библиотека как раз и была создана для кастомизаций. Приведенный ниже код будет вызывать обработчик нашей кастомной библиотеки при любом событии Forms. Там мы будем накладывать ограничения.
XXFND999.event(event_name);- создаем кастомную библиотеку XXFND999.pll c обработчиком EVENT. Обработчик выполняет двойную функцию: создает пункт меню один раз и вызывает форму при активации одноименного триггера.
procedure event(p_event_name in varchar2) is l_item_label CONSTANT varchar2(100) := 'Расширенная история записи'; l_event_name varchar2(100); l_tgt_fn_app CONSTANT varchar2(200) := 'XXMY'; l_tgt_fn_name CONSTANT varchar2(300) := 'XXFND999_HISTORY_RECORD_ADV'; l_form_name CONSTANT varchar2(300) := l_tgt_fn_name; l_frm_path varchar2(5000); param_id paramlist; l_item_feature varchar2(150); l_created_by item; l_tmp varchar2(150); l_block varchar2(150); begin -- отменяем отработку функционала внутри формы if name_in('SYSTEM.CURRENT_FORM') = l_form_name then return; end if; l_block := name_in('SYSTEM.CURRENT_BLOCK'); -- проверка на существование блока if length(l_block) = 0 then return; end if; -- событие при создании экземпляра формы if p_event_name = 'WHEN-NEW-BLOCK-INSTANCE' then l_event_name := xx_form_custom.create_special(l_item_label,'LINE'); end if; /* кастомный метод для получения первой незанятой ячейки SPECIAL */ l_event_name := xx_form_custom.get_special(l_item_label); -- включение видимости и активация нового пункта xx_form_custom.activate_special(l_item_label); -- сохранение пункта меню xx_form_custom.save_menu(); -- если вызвали нашу форму if (p_event_name = l_event_name)then l_frm_path := fnd_navigate.formpath(l_tgt_fn_app, l_tgt_fn_name); -- проверяем наличие поля CREATED_BY в таблице с текущей записью l_created_by := find_item(name_in('SYSTEM.CURRENT_BLOCK')||'.CREATED_BY'); if not id_null(l_created_by) then -- проверяем наличие созданного списка параметров param_id := get_parameter_list('FND_FORM_ARGS'); if (not id_null(param_id)) then destroy_parameter_list(param_id); end if; -- создаем список параметров и добавляем информацию из служебных полей param_id := create_parameter_list('FND_FORM_ARGS'); add_parameter( param_id, 'CREATED_BY', TEXT_PARAMETER, name_in(name_in('SYSTEM.CURSOR_BLOCK')||'.CREATED_BY')); add_parameter( param_id, 'LAST_UPDATED_BY', TEXT_PARAMETER, name_in(name_in('SYSTEM.CURSOR_BLOCK')||'.LAST_UPDATED_BY')); add_parameter( param_id, 'CREATION_DATE', TEXT_PARAMETER,name_in(name_in('SYSTEM.CURSOR_BLOCK')||'.CREATION_DATE')); add_parameter( param_id, 'LAST_UPDATE_DATE', TEXT_PARAMETER, name_in(name_in('SYSTEM.CURSOR_BLOCK')||'.LAST_UPDATE_DATE')); -- открываем форму open_form ( formmodule_name => l_frm_path , activate_mode => ACTIVATE , session_mode => SESSION , data_mode => NO_SHARE_LIBRARY_DATA , paramlist_id => param_id ); end if; end if; end event;Этап 2: создание формы. Создаем Data Block, создаем триггер WHEN-NEW-BLOCK-INSTANCE и item USER_INFO, которому будем присваивать строку с данными о пользователе, разделенными через перевод строки. Создаем канву, бросаем туда item USER_INFO, ставим в свойствах Multiline=yes, Length=32000. Создаем несколько параметров, идентичных прописанным в кастомной библиотеке: CREATED_BY, LAST_UPDATED_BY, CREATION_DATE, LAST_UPDATE_DATE. Начинаем править код PL/SQL созданного блока данных и прописываем туда такое:
declare l_create_id varchar2(50) := :PARAMETER.CREATED_BY; l_update_id varchar2(50) := :PARAMETER.LAST_UPDATED_BY; l_create_date varchar2(50) := :PARAMETER.CREATION_DATE; l_update_date varchar2(50) := :PARAMETER.LAST_UPDATE_DATE; begin :XXFND361_TEST_V.CREATED_BY_FULLNAME := XXFND361_PKG.get_record_history(p_create_id => l_create_id, p_update_id => l_update_id, p_create_date => l_create_date, p_update_date => l_update_date );XXFND999_PKG - обычный PL/SQL пакет:
function get_record_history(p_create_id in varchar2, p_update_id in varchar2, p_create_date in varchar2, p_update_date in varchar2 ) return varchar2 is l_result_line varchar2(32000); l_create_person varchar2(150); l_create_org_id varchar2(150); l_create_job varchar2(150); l_create_contact varchar2(150); l_update_person varchar2(150); l_update_org_id varchar2(150); l_update_job varchar2(150); l_update_contact varchar2(150); l_is_person varchar2(1); l_create_organization_id number; l_update_organization_id number; l_create_id varchar2(150) := p_create_id; l_update_id varchar2(150) := p_update_id; l_create_date varchar2(150) := p_create_date; l_update_date varchar2(150) := p_update_date; begin if l_create_id is null then return ''; end if; select case when user_det.full_name is null then fnu.user_name else decode(regexp_substr(upper(user_det.full_name),'I'), null, user_det.full_name, fnu.user_name) || ' ' || user_det.employee_number end, decode(user_det.full_name, null, 'N', 'Y') into l_create_person, l_is_person from FND_USER fnu, per_all_people_f user_det where fnu.employee_id = user_det.person_id(+) and trunc(sysdate) between user_det.effective_start_date(+) and user_det.effective_end_date(+) and fnu.user_id = to_number(l_create_id); -- if l_is_person = 'Y' then begin select asg.organization_id into l_create_organization_id from per_all_people_f user_det, per_all_assignments_f asg, FND_USER fnu where user_det.person_id = asg.person_id and fnu.employee_id = user_det.person_id and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date and fnu.user_id = to_number(l_create_id); exception when NO_DATA_FOUND then null; end; -- begin select j.name into l_create_job from per_all_people_f user_det, per_all_assignments_f asg, FND_USER fnu, per_jobs j where user_det.person_id = asg.person_id and fnu.employee_id = user_det.person_id and asg.job_id = j.job_id and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date and trunc(sysdate) >= j.date_from and j.date_to is null and fnu.user_id = to_number(l_create_id) and rownum = 1; exception when NO_DATA_FOUND then null; end; -- begin select coalesce(user_det.email_address, fnu.Email_Address, ' ') || ' ' || user_det.work_telephone into l_create_contact from per_all_people_f user_det, per_all_assignments_f asg, FND_USER fnu where user_det.person_id = asg.person_id and fnu.employee_id = user_det.person_id and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date and fnu.user_id = to_number(l_create_id); exception when NO_DATA_FOUND then null; end; end if; -------------------------- l_is_person := ''; -------------------------- select case when user_det.full_name is null then fnu.user_name else decode(regexp_substr(upper(user_det.full_name),'I'), null, user_det.full_name, fnu.user_name) || ' ' || user_det.employee_number end, decode(user_det.full_name, null, 'N', 'Y') into l_update_person, l_is_person from FND_USER fnu, per_all_people_f user_det where fnu.employee_id = user_det.person_id(+) and trunc(sysdate) between user_det.effective_start_date(+) and user_det.effective_end_date(+) and fnu.user_id = to_number(l_update_id); if l_is_person = 'Y' then begin select asg.organization_id into l_update_organization_id from per_all_people_f user_det, per_all_assignments_f asg, FND_USER fnu where user_det.person_id = asg.person_id and fnu.employee_id = user_det.person_id and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date and fnu.user_id = to_number(l_update_id); exception when NO_DATA_FOUND then null; end; -- begin select j.name into l_update_job from per_all_people_f user_det, per_all_assignments_f asg, FND_USER fnu, per_jobs j where user_det.person_id = asg.person_id and fnu.employee_id = user_det.person_id and asg.job_id = j.job_id and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date and trunc(sysdate) >= j.date_from and j.date_to is null and fnu.user_id = to_number(l_update_id); exception when NO_DATA_FOUND then null; end; -- begin select coalesce(user_det.email_address, fnu.Email_Address, ' ') || ' ' || user_det.work_telephone into l_update_contact from per_all_people_f user_det, per_all_assignments_f asg, FND_USER fnu where user_det.person_id = asg.person_id and fnu.employee_id = user_det.person_id and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date and fnu.user_id = to_number(l_update_id); exception when NO_DATA_FOUND then null; end; end if; --------------- select decode(l_create_person, null, '', 'Создал запись ' || l_create_person || chr(10)) into l_create_person from dual; select decode(l_create_org_id, null, '', 'Структурное подразделение ' || l_create_org_id || chr(10)) into l_create_org_id from dual; select decode(l_create_job, null, '', 'Должность ' || l_create_job || chr(10)) into l_create_job from dual; if length(trim(l_create_contact)) > 0 then l_create_contact := 'Контакты ' || l_create_contact || chr(10); else l_create_contact := ''; end if; select decode(l_create_date, null, '', 'Дата создания записи ' || l_create_date || chr(10)) into l_create_date from dual; select decode(l_update_person, null, '', 'Изменил запись ' || l_update_person || chr(10)) into l_update_person from dual; select decode(l_update_org_id, null, '', 'Структурное подразделение ' || l_update_org_id || chr(10)) into l_update_org_id from dual; select decode(l_update_job, null, '', 'Должность ' || l_update_job || chr(10)) into l_update_job from dual; if length(trim(l_update_contact)) > 0 then l_update_contact := 'Контакты ' || l_update_contact || chr(10); else l_update_contact := ''; end if; select decode(l_update_date, null, '', 'Дата изменения записи ' || l_update_date) into l_update_date from dual; l_result_line := l_create_person || l_create_org_id || l_create_job || l_create_contact || l_create_date || chr(10) || l_update_person || l_update_org_id || l_update_job || l_update_contact || l_update_date; return l_result_line; exception when OTHERS then return 'Ошибка! p_create_id=' || l_create_id || ';' || 'l_update_id=' || l_update_id || ';' || 'l_create_date=' || l_create_date || ';' || 'l_update_date=' || l_update_date || 'error info: ' || sqlerrm; end get_record_history;Этап 3: создание и связывание функции, формы в Forms. После того, как всё это будет создано, установка производится следующим скриптом:
cp ./forms/XXFND999_HISTORY_RECORD_ADV.fmb $AU_TOP/forms/RU cp ./forms/XXFND999_HISTORY_RECORD_ADV.fmb $AU_TOP/forms/US cp ./resource/CUSTOM.pll $AU_TOP/resource/CUSTOM.pll cp ./resource/XXFND999_CUSTOM.pll $AU_TOP/resource/XXFND999_CUSTOM.pll cd $AU_TOP/forms/RU frmcmp_batch.sh module=$AU_TOP/forms/RU/XXFND999_HISTORY_RECORD_ADV.fmb userid=$APPSID module_type=form compile_all=yes output_file=$XX_TOP/forms/RU/XXFND999_HISTORY_RECORD_ADV.fmx cd $AU_TOP/forms/US frmcmp_batch.sh module=$AU_TOP/forms/US/XXFND999_HISTORY_RECORD_ADV.fmb userid=$APPSID module_type=form compile_all=yes output_file=$XX_TOP/forms/US/XXFND999_HISTORY_RECORD_ADV.fmx cd $AU_TOP/resource frmcmp_batch.sh module=$AU_TOP/resource/XXFND999_CUSTOM.pll userid=$APPSID module_type=library compile_all=yes output_file=$AU_TOP/resource/XXFND999_CUSTOM.plx frmcmp_batch.sh module=$AU_TOP/resource/CUSTOM.pll userid=$APPSID module_type=library compile_all=yes output_file=$AU_TOP/resource/CUSTOM.plxРезультат:
Грабли.
FRM-41106 Cannot create record without parent. Не соблюдено обращение к данным по модели BLOCK.ITEM. Если блок можем быть любым, то корректное получение значения из item такое:name_in(name_in('SYSTEM.CURSOR_BLOCK')||'.CREATED_BY'));FRM-10056: Incorrect module type stored in the file. Неправильный параметр в sh-скрипте установки расширения. Внимание:
frmcmp_batch.sh module=$AU_TOP/forms/US/XXFND999_HISTORY_RECORD_ADV.fmb userid=$APPSID module_type=form compile_all=yes output_file=$XX_TOP/forms/US/XXFND999_HISTORY_RECORD_ADV.fmx
FRM-91507: Internal Error: Unable to generate library. Ошибка в кастомной библиотекеFRM-30085: Unable to adjust form for output. Форма слишком велика, либо элементы выходят за границы формы.
FRM-47023:No such parameter named G_QUERY_FIND exists in form. Хитрая ошибка, которая поначалу вводит в тупик, потому как G_QUERY_FIND - стандартный параметр, который есть всегда. Имеет смысл проверить тип данных параметров. Даже если передается дата, то тип данных должен быть Char. После этой манипуляции проблема ушла.
ORA-06502: PL/SQL: numeric or value error: character string buffer too small. По умолчанию размер поля вывода ограничен 30 символами. Эта ошибка появится, если переменная будет содержать строковое значение большей длины. Имеет смысл поставить 32 000.