14 апреля 2016 г.

Редактирование дебиторов/кредиторов

Дебиторы: 
  • пакет - CMD_INTERFACE (Customer Interface), 
  • основной класс - CMD_EI_API (Customer Processing APIs).

Кредиторы: 
  • пакет - VMD_INTERFACE (Vendor Interface), 
  • основной класс - VMD_EI_API (Vendor Processing APIs).
Подробнее...

15 июля 2015 г.

FM Для вычисления разницы между двумя датами в минутах

CALL FUNCTION 'DELTA_TIME_DAY_HOUR'
  EXPORTING
    T1      = l_curr_time
    T2      = l_time
    D1      = l_curr_date
    D2      = l_date
  IMPORTING
    MINUTES = l_minutes. Подробнее...

15 мая 2013 г.

WorkFlow и рассылка сообщений

В SAP есть такой интересный механизм, как WorkFlow. Ну очень интересный, с большим количеством ну очень интересных подводных камней.
Начинаем серию публикаций об этих самых камнях. Предполагается, что возможности самого WF вам известны.
Возможно некоторые проблемы с WF возникают у нас из-за того, что мы неправильно его готовим и есть штатные способы их решить. Но так как я этих способов не знаю - для меня эти проблемы реальны.

Первая тем - отправка сообщений из WF о ходе его продвижения.

В WF есть готовы шаг по рассылки почтового сообщения на SAP-почту. Там довольно легко в мастере мышкой можно набрать что-то вроде "эй, мистер ZZZ, для заказа N XXX нужно бы цены пониже, согласуй?!".
Сразу подсказка: если создать почтовую задачу, а потом добавить в WF её же, но в качестве обычной задачи, то можно отправлять письмо на внешнюю почту.

Но есть у этого способа целый ряд существенных недостатков:
1. Весь WF вываливается в логическую ошибку если получатель не нашёлся, или даже нашёлся, но заблокирован. Возможно это как-то можно обойти, но я такого способа не знаю.
2. Если нужно подробное письмо с кучей данных о заказе (дебитор, сумма заказа и тд) - эти данные надо сначала получить в контейнер WF. А это ещё одна задача в потоке. Тоже касается получателей, если это не инициатор, а кто-то другой, его тоже надо выяснить специальной задачей.
3. Иногда отправлять надо только если выполняется какое-то условие, либо текст/получатель зависит от какого-то условия. Придётся добавлять ещё и шаги ветвления по этому условию. Возможно и задачу отправки придётся дублировать.
4. Иногда надо отправить сообщение разными способами (сап-почта, email, sms). Соответственно и задач будет несколько.
5. Иногда надо отправить задачи нескольким разным получателям с разным текстом. Ещё дополнительные шаги.
6. Эти шаги могут повторяться с небольшими отклонениями в нескольких различных WF. Копипаста логики.
7. Выходит, что этот замечательный мастер создания письма надо проходить множество раз, а он не такой уж и элементарный. Плюс когда что-то меняется надо не забыть поменять везде одинаково.
8. WF становится очень громоздким - на один шаг-действие приходится целая пачка вспомогательных шагов. Да и в контейнере потока чёрт ногу сломит.
9. Вся эта информация о работе WF, куча лишних шагов и переменных, это всё хранится в бд, что не лучшим способом сказывается на быстродействии.
10. Много кода по получению доп. данных, который надо сопровождать. Актуально только для тех переменных, которые получаются только для письме, но не влияют на сам бизнес-процесс.
11. Если вам понадобится какая-то более сложная структура письма, типа список строк заказа с несколькими параметрами, или письмо в формате HTML с красивыми табличками, картинками и сложным форматированием, или вообще приложенный PDF - у вас это всё скорее всего не выйдет или выйдет очень сложно.

Для борьбы с этим был придуман такой приём.
- Для всего WF (или даже группы родственных WF) для отправки сообщений создаётся один ФМ (ну или метод класса) и оборачивающая её задача WF.
- В задачу эту прокидывается минимально необходимый объём данных из контейнера (в нашем случае это только номер сбытового заказа).
- Ещё одним параметром будет "код события". То есть что только что случилось и о чём надо сообщить всех заинтересованных лиц.
- В самом ВФ везде, где надо что-то отправить, вставляется шаг с этой задачей с уникальным текстом в "код события". Такие шаги можно добавлять очень быстро, а поддержки больше не требуют.
- При желании в некоторых случаях можно встраивать вызов этого ФМ в сам код задачи-действия, а шаг в WF не делать. Тогда WF будет ещё менее громоздким.
Вся же умная логика, по отправке сосредоточена в одном ФМ/классе. А уже там вы можете делать что угодно, насколько гибко/просто/наглядно как вы хотите.
В моём случае это:
- в зависимости от события, вида заказа, различных условий вычисляемых по номеру заказа, выбираются типы получателей (например, "автор заказа", "менеджер дебитора", "логист по закупу материала", "транспортный менеджер", прочие роли в процессе) и способы отправки (сап-почта, email, sms).
- по заказу и типу получателя находятся адреса (имя получателя сап-почты, email, телефон)
- по заказу ищутся все необходимые данные для текста и делаются все необходимые проверки.
- по заказу и типу сообщения формируется текст сообщения. причём возможности тут гораздо богаче, чем в мастере.
- при желании можно приаттачить что угодно, например, выходную печатную форму документа.
- планируется добавить логирование всех этих данных, чтобы можно было посмотреть что кому когда уходило. в журнале WF всё не так наглядно.
И вся логика работы в пределах нескольких методов одного класса. Очень наглядно. Очень удобно сопровождать.
И WF выглядит гораздо понятнее. Что тоже очень большой плюс.
Подробнее...

29 сентября 2011 г.

ABAP: как узнать размер окна SAP экрана в писелях

DATA: ref_consumer TYPE REF TO cl_gui_props_consumer,
      s_metric_factors TYPE CNTL_METRIC_FACTORS.


ref_consumer = cl_gui_props_consumer=>create_consumer( ).
s_metric_factors = ref_consumer->get_metric_factors( ).

s_metric_factors-screen-x - ширина
s_metric_factors-screen-y - высота

подсмотрено здесь. Подробнее...

13 сентября 2011 г.

Позновательно о TabStrip

Помимо того, что написано в ссылке:
Закладка также может быть полем вывода. То есть если объявить переменную с таким же именем и менять её значение - оно будет подставлять в заголовок закладки. Только саму галку "поле вывода" не забудьте поставить. Подробнее...

30 августа 2011 г.

Принудительное подтверждение изменений в ALV GRID

Проблема: В ALV GRID-е чтобы отработали события data_changed и data_changed_finished необходимо нажимать enter после редактирования. С числами и текстом это более менее ожидаемо и проблем не возникает. А вот со списками drop-down это совсем неочевидно. Поэтому следующее действие (например, сохранение введённых данных) отрабатывает без учёта этих изменений.

Решение:
IF l_grid_items IS NOT INITIAL.
CALL METHOD l_grid_items->check_changed_data.
ENDIF.
Этот код заканчивает редактирование ALV GRID-а. В моем случае его оказалось достаточно вписать в PAI до обработки параметра user-command.
Подробнее...

29 июля 2011 г.

Поиск в исходных кодах

Уже писал раньше про ФМ RPR_ABAP_SOURCE_SCAN, который помогает найти в исходных кода текст
Оказалось, что он ище далеко не везде. Зато RS_ABAP_SOURCE_SCAN ищет и в классах, и в расширениях cmod. Подробнее...

13 июля 2011 г.

Партии VS Виды оценки

Оказывается "партии" (charg) и "Виды оценки" (bwtar) - совсем не одно и то же, как меня когда-то убедил кто-то из консультантов.

На сбытовых заводах у нас они соответствуют 1к1, а, например, на балансовых - не соответствуют.

Соответствие можно восстановить в таблице mcha. Там по материалу/заводу/"партии" можно узнать вид оценки. Подробнее...

27 июня 2011 г.

Работа с блокировками

1. Найти Функции блокировки/разблокировки

Например, необходимо блокировать/разблокировать элемент таблицы vbak (сбытовой заказ).
   * В транзакции se11 в графе "ОбъектБлокировки" ищем "*vbak*" и находим "EVVBAKE".
   * Заходи внутрь и нажимаем "Перейти к" - "Модули блокировки"
   * Там видим:
DEQUEUE_EVVBAKE Снятие блок. постановки в очередь с объекта EVVBAKE
ENQUEUE_EVVBAKE Запрос блокировки постановки в очередь для объекта EVVBAKE
есть ещё две, но по ним видно, что это что-то специфическое, потому что там блокировка идёт не по vbak-vbeln, а по совершенно другому полю.

2. Снять все блокировки

Эти функции работают не как "блокировать" / "разблокировать", а "увеличить счётчик блокировок" / "уменьшить счетчик блокировок". В некоторых случаях это не очень удобно, например когда мы не можем или не хотим следить за количеством блокировок (в моём случае стандартные ФМ-ы непредсказуемо снимали или устанавливали блокировки).
Вот такой код вернёт в E_COUNT количество блокировок текущим пользователем по заказу l_vbeln:
data lt_enq type table of seqg3.
data ls_enq type seqg3.
data: GTARG TYPE EQEGTARG.

CONCATENATE SY-MANDT l_vbeln into GTARG.
CALL FUNCTION 'ENQUEUE_REPORT'
   EXPORTING
      GCLIENT = SY-MANDT
      GNAME = 'VBAK'
      GTARG = GTARG
      GUNAME = SY-UNAME
   TABLES
      ENQ = lt_enq
   EXCEPTIONS
      COMMUNICATION_FAILURE = 1
      SYSTEM_FAILURE = 2
      OTHERS = 3.
E_COUNT = 0.
LOOP AT lt_enq into ls_enq.
   E_COUNT = E_COUNT + ls_enq-GUSEVB.
ENDLOOP.
Соответственно для полной разблокировки надо запускать соответсвующий фм столько раз.
А при блокировании лучше проверить, а не заблокирован ли уже заказ.
Так же могут быть полезны другие модули группы функций SENT.

3. Ожидание разблокировки стандартными FM-ами

Функции блокировки могут пригодиться в случае, когда по одному объекту (например, сбытовому заказу) необходимо последовательно выполнять разные BAPI-функции. Дело в том, что эти функции часто работаю асинхронно, и возвращают управление в вызывающую функцию раньше, чем закончат свою работу. Не помогает даже bapi_transaction_commit с признаком wait. То есть второй шаг обработки будет провален так как, первый шаг ещё не отработал.
В таких случаях может быть полезен следующий код:
* вызов первой функции, блокирующей заказ. например, SD_SALESDOCUMENT_CREATE.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING WAIT = 'X'.

is_ok = space.
DO 10 TIMES. " количество попыток, сколько раз пытаться блокировать заказ. не слишком много
* вызов фм блокирования
   IF SY-SUBRC = 0. " если удалось заблокировать
* вызов функции разблокирования
      is_ok = 'X'.
      exit.
   else.
      * если надо - запоминаем почему неудалось заблокировать. это хранится в sy-MSG*.
   ENDIF.
   WAIT UP TO 1 SECONDS. " делаем паузу перед следующей попыткой заблокировать
ENDDO.
if is_ok is not initial.
   * вызов второй функции, блокирующей заказ.
endif.
Подробнее...

26 июня 2011 г.

FM для вывода диалога с несколькими опциями

  DATA: lt_spopli TYPE TABLE OF SPOPLI,
        ls_spopli TYPE SPOPLI.
  DATA: lv_answer TYPE CHAR2.

  CLEAR ls_spopli.
  ls_spopli-VAROPTION = 'Предложение'.
  APPEND ls_spopli TO lt_spopli.
  CLEAR ls_spopli.
  ls_spopli-VAROPTION = 'Заказ с оплатой на месте'.
  APPEND ls_spopli TO lt_spopli.

  CALL FUNCTION 'POPUP_TO_DECIDE_LIST'
    EXPORTING
      CURSORLINE               = 1
      TEXTLINE1                = 'Сохранить документ как:'
      TITEL                    = 'Cохранить'
    IMPORTING
      ANSWER                   = lv_answer
    TABLES
      T_SPOPLI                 = lt_spopli
    EXCEPTIONS
      NOT_ENOUGH_ANSWERS       = 1
      TOO_MUCH_ANSWERS         = 2
      TOO_MUCH_MARKS           = 3
      OTHERS                   = 4
            .

  IF SY-SUBRC <> 0.
    MESSAGE ID SY-MSGID TYPE SY-MSGTY NUMBER SY-MSGNO
            WITH SY-MSGV1 SY-MSGV2 SY-MSGV3 SY-MSGV4.
  ENDIF.

Результат:

Подробнее...

30 мая 2011 г.

Несколько полезных записей из внешнего интернета

Как найти что-то в исходных кодах
Для поиска чего либо в исходных кодах, очень помогает программа
RPR_ABAP_SOURCE_SCAN

Как запустить отладку в модальном окне
Вот так можно включить отладку в модальном окне:
Создаем текстовый файл со следующим содержанием, например, на рабочем столе

[FUNCTION]
Command=/H
Title=Debugger
Type=SystemCommand

Теперь просто перетаскиваем этот файл на ваше окошко и все.

Как редактировать данные в таблицах без диалога ведения, или в закрытых мандантах
Для того, что бы редактировать данные в таблицах без диалога ведения, или в закрытых мандантах необходимо:
В транзакции UASE16N выбрать таблицу, данные которой необходимо редактировать, ввести команду &SAP_EDIT в командную строку и нажать «Выполнить». Подробнее...

Вывести результат стандартных функций (BAPIRET2)



Красиво показать результат вызова стандартного ФМ:

CALL FUNCTION 'C14ALD_BAPIRET2_SHOW'
TABLES
I_BAPIRET2_TAB = RETURN.



Неожиданно приятная функция, не иначе как брахман писал. Подробнее...

20 мая 2011 г.

Быстрое создание ALV-таблицы

" например надо отобразить таблицу T_TCJ_POSITIONS
DATA: gc_alv_table TYPE REF TO cl_salv_table.
cl_salv_table=>factory( IMPORTING r_salv_table = gc_alv_table CHANGING t_table = T_TCJ_POSITIONS ).
" показать
gc_alv_table->display( ).

" если необходимо добавить события:
data: events_tb TYPE REF TO CL_SALV_EVENTS_TABLE.
CALL METHOD gc_alv_table->GET_EVENT
RECEIVING
VALUE = events_tb.

data ALV_HANDLER_pay type ref to CL_ALV_HANDLER_pay.
create object ALV_HANDLER_pay.
set handler ALV_HANDLER_pay->handle_doubleclick for events_tb.


" Переименовать колонки:
gc_columns = gc_alv_table->get_columns( ).
gc_colwork ?= gc_columns->get_column( 'P_RECEIPTS' ).
gc_colwork->set_long_text( 'Оплата' ).
gc_colwork->set_medium_text( 'Оплата' ).
gc_colwork->set_short_text( 'Оплата' ).
" там же можно сделать прочие настройки, если надо

" добавим агрегацию
gc_aggr = gc_alv_table->get_aggregations( ).
gc_aggr->add_aggregation( 'P_RECEIPTS' ). Подробнее...

генерированные экраны в своих формах

Чтобы отобразить генерированные экраны в своих формах надо описать экран:

SELECTION-SCREEN BEGIN OF SCREEN 0101 AS SUBSCREEN.
SELECT-OPTIONS s_vkorg FOR vbak-vkorg.
SELECT-OPTIONS s_vkbur FOR vbak-vkbur.
SELECT-OPTIONS s_VKGRP FOR vbak-VKGRP.
SELECT-OPTIONS s_vtweg FOR vbak-vtweg.
SELECT-OPTIONS s_spart FOR vbak-spart.

SELECT-OPTIONS s_SUBMI FOR vbak-SUBMI.
SELECT-OPTIONS s_netwr FOR vbap-netwr.
SELECT-OPTIONS s_lifnr FOR vbpa-lifnr.
SELECTION-SCREEN END OF SCREEN 0101.

А в своём экране включить его в подэкран аналогично обычным экранам. Подробнее...

3 мая 2011 г.

Оптимизация запросов select

Ряд рекомендаций:
1. Отказаться от into corresponding fields of table в пользу into table. Причём выводить только нужные поля.
Очень хорошо помогает.
Однако для этого надо обеспечить, что порядок и тип полей в таблице-получателе совадал с порядком полей запроса.

2. Не смешивать в запросе буферизованные таблицы и не буферизованные: буферизация работать не будет. Лучше вынести получение в отдельные подзапросы - можно даже в цикле, работает быстро.
На примере заполнения имён заводов и складов:
select [...] from [...] into table RES_TB where [...].
FIELD-SYMBOLS <res_fs> TYPE res_tp.
LOOP AT RES_TB ASSIGNING <res_fs>.
SELECT SINGLE name1 into <res_fs>-werks_t from t001w where werks = <res_fs>-werks.
SELECT SINGLE LGOBE into <res_fs>-LGORT_t from t001l where werks = <res_fs>-werks and lgort = <res_fs>-lgort.

endloop.

3. Например, в конструкции where есть ограничения "fielndname <> 'X'". А список допустимых значений этого поля небольшой и известен или может получен взят из справочника. Лучше постараться заменить на "=" в каком-либо виден (in, for all entries). либо вообще получить избыточные данные и отбросить их программным кодом (delete [...] where "fielndname = 'X'").

4. Например, результат первого запроса (tab1) используется во втором запросе (tab2), а потом эти данные собираются в общую таблицу. Примерно так:
loop at tab1 assigning <tab1_fs>.
read table tab2 into ls_tab2 with key field1 = <tab1_fs>-fielnd1 [...].
<tab1_fs>-field5 = ls_tab2-field5.
" либо цикл по второй таблице с аналогичным фильтром
endloop.
На больших объёмах такая сборка может быть очень медленной.
В таких случаях бывает полезно объявить таблицу tab2 как hash, если это возможно.

5. В фильтрах и соединениях таблиц чтобы использовался индекс надо перечислить поля, стоящие выше в индексе. Даже если там константа.
Например, в таблице A512 ключ из полей: MANDT, KAPPL, KSCHL, VKORG, BWTAR, MATNR, KFRST, DATBI. А в фильтре используется только vkorg, BWTAR, matnr, а KAPPL, KSCHL - константы. Необходимо перечислить эти два поля. MANDT - необязательно, его подставит система.

см также по FOR ALL ENTRIES

to be continue... Подробнее...

Подводные камни FOR ALL ENTRIES

1. Если результаты одного запроса используются в конструкции for all entries второго - не забываем проверять, что таблица пустая. Иначе второй запрос не посчитает это ограничением.
select [...] into table table1 from [...] where [...].
if table1 is not initial.
select [...] into table table2 from [...] for all entries table1 where [...].
endif.


2. При For all entries могут не выводиться дублирующиеся строки (как будто используется параметр distinct).
Необходимо перечислить в списке полей вывода все ключевые поля всех используемых в запрос таблиц (причём даже если значение каких-то из них фактически одинаковые).
Проблема похоже в том, что такой запрос преобразуется системой в набор запросов, объединённых через метрику UNION, а не UNION ALL.

3. Например в конструкции where запрос используется набор полей из таблицы конструкции for all entries, а значения эти неуникальные в таблице (например только номера документов, а в таблице встречаются и позиции).
Такой запрос можно ускорить:
data tab_copy like tab. " либо type с тем же типом
tab_copy = tab.
sort tab_copy by vbeln." список полей, используемых в for all entries
DELETE ADJACENT DUPLICATES FROM tab_copy COMPARING vbeln. " тот же список полей

И в запросе использовать эту таблицу.

4. Иногда полезно убрать лишние фильтр по значениям из таблицы for all entries (например, вместо vbeln+posnr[+etern] оставить только vbeln) и применить пункт 3.
Особенно когда известно, что скорее всего в таблице источнике перечислены все или многие значения исключаемых полей.
Пусть даже будет какая-то избыточность данных, но запрос отработает значительно быстрее. Подробнее...

ALV-контейнеры с одинаковыми именами

Проблема: В программе с ALV-контейнером на экране вызывается фм. В рамках ФМ-а открываются другие экраны с ALV-контейнерами. Один из которых называется также, как контейнер программы.
В результате при возврате из фм в программу ALV может не отрисовываться.

Выход: делать ALV-контейнеры с уникальными именами. Работает.
Возможно можно избежать такой проблемы подобрав параметры конструктора контейнера (указать программу-REPID и экран-DYNNR), но у меня не получилось. Подробнее...

28 апреля 2011 г.

Заполнение списка предлагаемых значений Listbox-а

На примере списка кредиторов
  types:
begin of vrm_value,
key(40) type c,
text(80) type c,
end of vrm_value,
VRM_VALUES TYPE VRM_VALUE OCCURS 0.
 
data: lifnr_values TYPE VRM_VALUES with header line.
FREE lifnr_values. " обязательно
data: lfa1_tb TYPE WSTN_LFA1_TAB, lfa1_ln TYPE LFA1.
" заполнить где-то lfa1_tb поставщиками
LOOP AT lfa1_tb into lfa1_ln.
lifnr_values-key = lfa1_ln-lifnr.
CONCATENATE lfa1_ln-NAME1 lfa1_ln-NAME2 lfa1_ln-NAME3 lfa1_ln-NAME4
into lifnr_values-text.
APPEND lifnr_values.
ENDLOOP.
call function 'VRM_SET_VALUES'
EXPORTING
id = 'LIFNR' " имя элемента
values = lifnr_values[]
EXCEPTIONS
others = 1.
Подробнее...

Sap Scripting

Проблема: необходимо автоматически запустить sap logon и выполнить какие-то действия автоматически.
1. Необходимо активировать у инстанции возможность Sap scripting-а
http://searchsap.techtarget.com/feature/Step-2-Activating-SAPGUI-scripting
2. Как генерировать скориптинг
Настройка локального формата .Запись и воспроизведение скрипта. Дальше там понятно что делать.
3. Пример скрипта:


 
dim session
StartApp
if WScript.Arguments.Count > 0 Then
if WScript.Arguments(0) = "Site" Then
SiteUpload
End if
End if
 
rem Выходим
session.findById("wnd[0]/tbar[0]/okcd").text = "/nex"
session.findById("wnd[0]").sendVKey 0
 
Sub SiteUpload
rem Запускаем программу
session.findById("wnd[0]/tbar[0]/okcd").text = "se38"
session.findById("wnd[0]").sendVKey 0
session.findById("wnd[0]/usr/ctxtRS38M-PROGRAMM").text = "ZRSITE_UPLOAD"
session.findById("wnd[0]").sendVKey 8
session.findById("wnd[0]/usr/txtTUNE").text = "tyumen"
session.findById("wnd[0]/usr/txtMIN_C").text = "15"
session.findById("wnd[0]/usr/txtTEMP_DIR").text = "C:\"
session.findById("wnd[0]/usr/txtPR_VARI").text = "TYUMEN"
session.findById("wnd[0]").sendVKey 8
end sub
 
rem Подпрограма запуска приложения
Sub StartApp
rem запускаем саплогон
Set WshShell = CreateObject("WScript.Shell") 
WshShell.Run "C:\Progra~1\SAP\FrontEnd\SAPgui\saplogon.exe", 1, False
rem ждём открытия
WScript.Sleep(10000)
rem Открываеем сессию
If Not IsObject(application) Then
Set SapGuiAuto  = GetObject("SAPGUI")
Set application = SapGuiAuto.GetScriptingEngine
rem application.OpenConnection("RD1")   
application.OpenConnectionByConnectionString("rosasap09 00")
Set connection = application.Children(application.Children.Count - 1) rem свежеоткртое окно
Set session    = connection.Children(0)
End If
If IsObject(WScript) Then
WScript.ConnectObject session,     "on"
WScript.ConnectObject application, "on"
End If
rem Авторизация
session.findById("wnd[0]").maximize
session.findById("wnd[0]/usr/txtRSYST-MANDT").text = "800"
session.findById("wnd[0]/usr/txtRSYST-BNAME").text = "alexeys"
session.findById("wnd[0]/usr/pwdRSYST-BCODE").text = ""
session.findById("wnd[0]/usr/txtRSYST-LANGU").text = "ru"
session.findById("wnd[0]").sendVKey 0
 
rem Если открылось окно, что чувак уже залогинен - закрыть? а может не закрывать? Тогда вместо OPT1 - OPT2
set log_opt = session.findById("wnd[1]/usr/radMULTI_LOGON_OPT1",  False)
If not log_opt Is Nothing Then
log_opt.select
session.findById("wnd[1]/tbar[0]/btn[0]").press
End If
 
End Sub
 

Подробнее...