29.05.2014

Разработка кастомной формы в Oracle Forms

Цель: создать кастомную форму "Расширенная история записи" и включить в меню 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:

Добавляем новый пункт в самый конец списка.

Прописываем блок с вызываемым триггером в окне, открывшемся из контекстного меню 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.