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.

Комментариев нет :