Budujemy workflow w Oracle Apex

Do czego potrzebny nam "przepływ pracy" w aplikacji?

Zdecydowana większość aplikacji biznesowych ma jakiś "wokflow". W każdym biznesie grupa ludzi wykonuje swoją pracę by „dostarczyć wartość”. Ale nie robimy wszystkiego wspólnie i rozbijamy zadanie na aktywności ponieważ się specjalizujemy i realizujemy zadania według umiejętności - roli. Wykonujemy swoją pracę i przekazujemy ją do następnej osoby – niczym taśma produkcyjna. Te aktywności wykonywane są w ramach procesu ktory działa wg. szablonu - "wokflow". Ma on swoje etapy, taski, statusy.

Oracle Apex nie posiada gotowego modułu do budowania "przepływu pracy" jak byśmy powiedzieli w języku polskim. Jeślibyśmy chcieli zbudować np. proces akceptacji, musimy tworzyć funkcje lub procedury w językach dostępnych w Apex (JavaScript, PL/SQL). Napisanie takiego kodu jest problematyczne. Największym jednak wyzwaniem jest zarządzanie zmianą. Wszyscy wiemy jak "biznes" zmienia założenia do aplikacji - zaczyna się do "takiego małego czegoś do takiego czegoś" a ewoluuje w całkiem duży, skomplikowany system. Więc workflow który służy nam np. do akceptacji faktur z jednego poziomu rozrasta się do kilku, powstają kolejne warunki biznesowe i wyjątki (np. przepływ zależny od rodzaju faktury, od sumy faktury, od kategorii dostawcy/klienta).

Wymagane komponenty

Systemem który umożliwi nam zbudowanie przepływu pracy w Oracle Apex bez większego wysiłku jest Camunda. Camunda umożliwia nie tylko zbudowanie diagramu przepływu w notacji BPM ale dodatkowo wyposażona jest w narzędzie do tworzenia modeli decyzyjnych, czyli warunków biznesowych (DMN). Camunda posiada dwa systemy (Modeler do tworzenia diagramów i Platformę do uruchamiania procesów) i została napisana w języku Java w modelu open source na warunkach licencji Apache.

Budując aplikację która wymaga workflow, odseparujemy zarządzanie przepływem informacji od samej aplikacji. Aplikacja "nie ma pojęcia" jak zorganizowana jest logika biznesowa przepływu informacji - przekazuje do Camundy zadanie akceptacji, tym by nastąpiła właściwa decyzja zajmuje sie platforma BPM. Sama zaś Camunda niczego nie wykonuje w bazie Oracle, nie udostępnia formularzy, nie zarządza danymi - zajmuje się tylko dostarczeniem informacji. To zewnętrzny system/osoby na ich podstawie podejmują konkretne działanie.

W Internecie znajdziesz "plugin" Camundy dla Oracle Apex który ma umożliwić łatwe zbudowanie workflow ale po przejściu tego tutoriala zrozumiesz że nie ma żadnej drogi na skróty i nie sposób uruchomić przepływów za pomocą kilku kliknięć (choć to co opiszemy nie jest raczej zbyt drudne).

Budujemy workflow - tworzymy definicję procesu

W naszym przykładzie zbudujemy workflow ktory akceptuje, lub odrzuca, koszty faktury. To prosty proces który pozwoli nam szybko zrozumieć jak zintegrować Oracle Apex z Camunda.

Diagram procesu tworzymy w Camunda Modeler który pobieramy ze strony. Nie będziemy tutaj opisywać menu programu i samej notacji BPMN. Powiemy tytlko żę cokolwiek robisz, na jakimkolwiek stanowisku pracujesz w firmie, powinieneś znać choćby podstawy BPM. Nasz proces rozmoczyna się od tasku DMN ale zacznij od pominięcia go w pierwszym etapie integracji Oracle Apex z Camunda.

Nasz diagram składa się z dwóch tasków. W zależności od kwoty faktury lub działu faktury mogą być akceptowane tylko przez jedną osobę (jeden poziom) lub dwie.

Taski "Akceptacja poziom 1" i "Akceptacja poziom 2"

To manual task który obsługuje pierwszy poziom akceptacji faktur. Każdy task ma swój ID (to po ID będziemy komunikować się z taskiem) i nazwę która nie jest obligatoryjna - służy bardziej Tobie byś orientował(a) się w procesie.

W zakładce "Forms", w polu "Form Key" wpisz nazwę formularza. Np. "10". Będzie to informacja dla Apex jaka strona obsługuje akceptację aktywności z tego tasku. Zobaczysz szczegóły dalej. Tak "Akceptacja poziom 2" różni się tylko ID i nazwą formularza ("Form Key").

Wyrażenia

Przepływ do bramki (bramki często mylnie nazywane są "punktami decyzyjnymi") nie posiada żadnego wyrażenia, może też pozostać jego domyślnie ID. Natomiast przepływy od bramki (w tym przypadku typu "Exclusive gateway" dlatego "X") posiadaja przypisane wyrażenia. Wrażenie może być zapisane na dwa sposoby: #{wyrazenie} lub ${wyrazenie}. Wpisz ${!approved} (lub ${not approved} dla flow który zakończy aktywność i ${approved && level == 2} dla flow który przekieruje aktywność do akceptacji drugiego poziomu.

Jak widzidzisz nasz proces wymagać będzie dwóch zmiennych; 'approved' i 'level'. WAŻNE: Diagramy w Camunda NIE POSIADAJĄ zadeklarowanych zmiennych przed ich uruchomieniem. Zmienne przesyłasz w momencie uruchomienia procesu lub poźniej, np. przy potwierdzaniu wykonania tasku ("task complete"). To może wydawać się dziwne ale okazuje się że dzięki temu diagramy proces.ow są niezwykle elastyczne.

Wracając do naszych zmiennych; "approved" przyjmować będzie false (akceptacja odrzucona) lub true (zaakceptowana) a "level" to wartości 1 (faktura akceptowana jest tylko przez jeden poziom) lub 2. Wrażenie flow "Zaakceptowana" od bramki do "Koniec" ma postać ${approved && level == 1} a wyrażenia od "Akceptacja poziom 2" to ${approved} dla "Zaakceptowana" i ${!approved} dla "Nie zaakceptowana".

Wysłanie definicji procesu na server Camunda

Definicję procesu możesz przesłać na serwer Camunda [poberz wcześniej i uruchom Platformę BPM dla Tomcat ze strony] bezpośrednio z Camunda Modeler lub używająć wbudowanego w plaftormę Camundy serwisu REST. Jeśli robisz to z Modelera wybierz menu "Deploy current diagram" [pozostaw domyślne wpisy w oknie "Deploy Diagram do Camunda Platform". Jeśli chcesz wykorzystać REST, użyj to tego celu programu Postman [http://localhost:8080/engine-rest/deployment/create]

Content-type: multipart/form-data
Body:
Key: upload [file] - wskaż plik definicji bpmn

Aplikacja Oracle Apex

Nie będziemy opisywać aplikacji do zarządzania fakturami; tego jak te faktury się tam znalazły ani jak wyglądają strony które prezentują listę tych faktur. Wyobraź sobie że masz w aplikacji fakturę którą chcesz teraz przesłać do aplikacji. Księgowy lub użytkownik klika na przycisk "Prześlij fakturę do akceptacji" wywołując tym samym kod PL/SQL:

declare
  l_date           varchar2(35);
  v_url            varchar2(1000);
  l_rowid     number:= :P300_ID;
  l_invoice_amount number := :P300_KWOTA;
  l_dzial     varchar2(80) := :P300_DZIAL;
  l_nrfaktury varchar2(80) := :P300_NRFAKTURY;

  v_body    clob :=
  '{
  "variables": {
    "dept" : {
        "value" : "'||l_dzial||'",
        "type": "String"
    },
    "kwota" : {
      "value" : '||l_invoice_amount||',
      "type": "Double"
    },
    "nr_faktury" : {
      "value" : "'||l_nrfaktury||'",
      "type": "String"
    },
    "row_id" : {
      "value" : "'||l_rowid||'",
      "type": "Double"
    }
  }
  }';
  l_response clob;
 
begin
  -- czyscimy headers z poprzedniej sesji
  apex_web_service.g_request_headers.delete;
  apex_web_service.g_request_headers(1).name := 'Content-type';
  apex_web_service.g_request_headers(1).value := 'application/json';
  v_url := 'http://127.0.0.1:8080/engine-rest/process-definition/key/invoice-approval-test-v5/start';
 
  l_response := apex_web_service.make_rest_request(p_url  => v_url, p_http_method => 'POST', p_body => v_body);
  if apex_web_service.g_status_code = 200 then    
    UPDATE TEST_BPMN_INVOICES SET status = 'akceptacja poziom 1', approved = 0 WHERE id = l_rowid;
  else    
  --The call failed, inspect the response and fix the code :-)
--Select from dual is just to run anything...
    SELECT TO_CHAR(SYSDATE, 'MM-DD-YYYY HH24:MI:SS') into l_date FROM dual;
  end if;
  end;

Co robi ten kod? Jest to uruchomienie procesu na Platformie Camunda ze zmiennymi określonymi w "v_body". Oracle domyśnie uniemożliwi połączenie z zewnętrznym hostem - przed wykonaniem tego kodu nadaj stosowne uprawnienia w bazie Oracle. Proces zostanie uruchomiony i piewszy task, "Akceptacja poziom 1" otrzyma zadanie do wykonania. Powyższy kod nie zawiera zmiennej 'level' przesylanej przy inicjowaniu procesu. Ta zmienna informuje proces czy aktywność ma być obsłużona przez dwa poziomy akceptacji czy tylko przez jeden. Dla uproszczenia wyłączyliśmy task "klasyfikacja faktur" który dodaje taką zmienną. Dodaj więc zmienną "level" do kodu (np przypisując jej wartość "1").

Możesz sprawdzić stan aktywności procesu logując się do platformy Camunda a następnie wchodząc do menu "cockpit" i wybierając nasz praoces. Aktywności każdego z tasków zobrazowane są numerami w kółeczkach. OK, taks Camundy otrzymal zadanie do wykonania - pora się nim zająć.

Budowanie listy zadań (task lists)

W najczęstszym scenariuszu Camunda nie inicjuje żadnych zadań. Zajmuje się tylko zarządzaniem informacją. To zewnętrzne systemy łączą się z Camundą i budują task listy i oznaczają aktyności jako wykonane (wykonują komendę "task complete"). W naszej aplikacji to Oracle Apex będzie budować task listy - pobierze listę zadań do wykonania i wyświetli w aplikacji tę listę dla właściwych użytkowników. Ok, pobierzmy wszystko co czeka w tasku "Akceptacja poziom 1".

DECLARE
  v_url        VARCHAR(200);
  t_url        VARCHAR(200);

  sJsonIndex   APEX_JSON.t_values;

  v_clob       CLOB;
  l_response   CLOB;
  t_response   CLOB;
  v_body       CLOB;
  t_body       CLOB;

  l_num_items  NUMBER;

  l_task_id           VARCHAR2(80);
  l_taskdefinitionkey VARCHAR2(80);
  l_formkey           VARCHAR2(5);

  t_kwota          NUMBER(8);
  t_rowid          NUMBER(8);
  t_dept           VARCHAR2(80);
  t_nr_faktury     VARCHAR2(80);
  t_level          VARCHAR2(5);

BEGIN

  l_response := '';
  -- czyscimy headers z poprzedniej sesji
  apex_web_service.g_request_headers.delete;
  apex_web_service.g_request_headers(1).name := 'Content-type';
  apex_web_service.g_request_headers(1).value := 'application/json';
  v_url := 'http://127.0.0.1:8080/engine-rest/task?taskDefinitionKey=approve_l1';
 
  l_response := apex_web_service.make_rest_request(p_url  => v_url, p_http_method => 'GET', p_body => v_body);

  if apex_web_service.g_status_code = 200 then
 
    apex_json.parse(sJsonIndex, l_response);
    l_num_items := APEX_JSON.get_count(p_path => '.' , p_values => sJsonIndex);
 
    IF l_num_items > 0 THEN

      FOR i in 1 .. l_num_items LOOP

         l_task_id := apex_json.get_varchar2(p_path => '[%d].id', p0 => i, p_values => sJsonIndex);
         l_formkey := apex_json.get_varchar2(p_path => '[%d].formKey', p0 => i, p_values => sJsonIndex);
         l_taskdefinitionkey := apex_json.get_varchar2(p_path => '[%d].taskDefinitionKey', p0 => i, p_values => sJsonIndex);

        --Uzywajac ID tego tasku, pobierz z REST variables a nastepnie zaktualizuj tabele faktur
         t_response := '';
         -- czyscimy headers z poprzedniej sesji
         apex_web_service.g_request_headers.delete;
         apex_web_service.g_request_headers(1).name := 'Content-type';
         apex_web_service.g_request_headers(1).value := 'application/json';
         t_url := 'http://127.0.0.1:8444/engine-rest/task/'||l_task_id||'/variables';
 
         t_response := apex_web_service.make_rest_request(p_url  => t_url, p_http_method => 'GET', p_body => t_body);
         if apex_web_service.g_status_code = 200 then
 
            apex_json.parse(t_response);
            t_rowid := apex_json.get_number(p_path => 'row_id.value');
            t_kwota := apex_json.get_number(p_path => 'kwota.value');
            t_dept := apex_json.get_varchar2(p_path => 'dept.value');
            t_nr_faktury := apex_json.get_varchar2(p_path => 'nr_faktury.value');
            t_level := apex_json.get_varchar2(p_path => 'level.value');

            UPDATE TEST_BPMN_INVOICES SET camunda_task_id = l_task_id, approve_level = t_level, approved = 0, form_key = l_formkey, taskdefinitionkey = l_taskdefinitionkey WHERE id = t_rowid;

         end if;           

      END LOOP;

    END IF;

  end if;
  -- Dodaj obsluge bledow

 EXCEPTION
  WHEN OTHERS THEN
    RAISE;
 
END; 

Co robi ten kod? Apex łączy się z Camunda i pobiera wszystkie taski które znajdują się w ... tasku o ID "approve_l1" z użyciem metody "task". Mamy listę tasków. Jednak potrzebujemy zmiennych i ich wartości przypisanych do tych tasków. Robimy więc pętlę przez wszystkie taski i wykonujemy inną metodę REST, "task/x/variables" by je odczytać. Camunda nie posiada metody rest która pozwala odczytać szczegółów tasku i jego zmiennych - musimy wykonać dwa połączenia.

W wyniku uruchamiania procesu, przesyłania faktury do akceptacji, otrzymaliśmy od REST camunda ID tego tasku. Dlaczego więc musimy odczytać raz jeszcze dany task procesu by poznać ID tasków? Procesy mogą powodować rozwidlanie się przepływów w efekcie czego z jednego zadania robi się kilka . Każde z tych zadań będzie mieć inny ID.

Tak zbudowana "task listę" prezentujemy w aplikacji osobom które zajmują się akceptacją faktur pierwszego poziomu. Jeśli dodamy do tego inne informacje o fakturze które mamy w bazie Apex, doskonale wiemy kto powinien widzieć co. Zauważ że odczytaliśmy z tasku także informację o "form_key" i zapisaliśmy w tabeli przy fakturze. Dzięki temu wiemy jaki formularz wyświetlić użytkownikow który będzie akceptawać tę fakturę. Cała 'logika" akceptacji zawarta jest w tej stronie. Jeśli użytkownik kliknie przycisk "akceptuj"a faktura wymagała tylko jednego poziomu akceptacji, proces kończy się. Jeśli są to dwa poziomy, aktywność po akceptacji przechodzi do następnego poziomu. Odrzucenie oznacza skierowanie aktywności do flow "Nie zaakceptowana".

Budowanie task listy może być wykonywane przez użytkownika (np. w tle kiedy wyświetlana jest lista zadań do zrobienia) lub regularnie poprzez jakiś scheduler/cron. Kod budujący task listę z tasku "Akceptacja poziom 2" jest taki sam z tą tylko różnicą że odczytuje inny task.

Wykonanie zadania - task complete

Po zbudowaniu task listy osoba akceptująca fakturę wyświetla formularz akceptacji/odrzucenia faktury i jeśli kliknie na przycisk "akceptuj fakturę" wykonuje kod jak poniżej:

DECLARE
  v_file_name  VARCHAR2 (25);
  v_json       VARCHAR2(32767);
  v_url        VARCHAR(200);
  t_url        VARCHAR(200);

  sJsonIndex   APEX_JSON.t_values;
  tJsonIndex   APEX_JSON.t_values;

  v_clob       CLOB;
  l_response   CLOB;
  t_response   CLOB;
  v_body       CLOB;
  t_body       CLOB;

  l_task_id    VARCHAR2(80) := :P312_TASKID;
  l_rowid      NUMBER(8)    := :P312_ROWID;
  l_level      VARCHAR2(5)  := :P312_LEVEL;

BEGIN

  v_body := '{
  "variables": {
    "approved" : {
      "value" : true,
      "type": "Boolean"
    }
  }
}';

  l_response := '';
  -- czyscimy headers z poprzedniej sesji
  apex_web_service.g_request_headers.delete;
  apex_web_service.g_request_headers(1).name := 'Content-type';
  apex_web_service.g_request_headers(1).value := 'application/json';
  v_url := 'http://127.0.0.1:8080/engine-rest/task/'||l_task_id||'/complete';
 
  l_response := apex_web_service.make_rest_request(p_url  => v_url, p_http_method => 'POST', p_body => v_body);
 
  if apex_web_service.g_status_code = 204 then
     if l_level = 1 then
       UPDATE TEST_BPMN_INVOICES SET status = 'zaakceptowana', approved = 1, CAMUNDA_TASK_ID = null WHERE id = l_rowid;
     else
       UPDATE TEST_BPMN_INVOICES SET status = 'zaakceptowana poziom 1', approved = 3, CAMUNDA_TASK_ID = null WHERE id = l_rowid;
     end if;
   end if;

 EXCEPTION
  WHEN OTHERS THEN
    RAISE;
END;

Co robi ten kod? Łączymy się z serwerem Camunda i wykonujemy metodę 'task/x/complete'. Przesyłamy jednocześnie zmienną 'approved' dzięki której wyrażenie przepływu będzie wiedzieć jak pokierować aktywnością. Pod przyciskiem "Odrzuć fakturę" znajduje się barzo podobny kod z tą różnicą że status przyjmie inną wartość a zmienna 'approved' bezie 'false'. 'Task complete' powoduje wysłanie do Camudny informacji o zrealizowaniu zadania. Camuda przesyła taką aktywność do następnego tasku w procesie.

Kod aplikacji i komunikacja z Camunda

Zauważ ze zarówno ten kod jak i poprzednie są bardzo podstawowe. W rzeczywistości struktura tabel Twojej aplikacji będzie bardziej rozbudowana a np. statusy będą mieć zdefiniowane opisy w słownikach. Nie opisujemy też tutaj jaki kod ma posiadać strona prezentujaca task listę ani jakie są warunki powodujące że użytkownicy widzą lub nie odpowiednie przyciski które wyświetlą formularze akceptacji. Same też formularze możesz zbudować róznie. Ten tutorial ma Ci pomóc zrozumieć podstawy komunikacji z Camunda.

Diagram DMN

Diagram DMN uprości nasze życie. Zamiast rysować skomplikowany model BPM z wieloma wyrażeniami i przepływami, umieszczamy jeden task z "implementation DMN" w którym zawarte są warunki biznesowe. W naszym modelu procesu potrzebujemy informacji czy faktura będzie akceptowana przez jedna tylko osobę czy przez dwie (jeden lub dwa poziomy). Możemy o tym zadecydować po stronie aplikacji i przesłać zmienną "level" z gotową wartością. Jednak dużo łatwiej, a co ważniejsze dużo łatwiej będzie dokonać odpowiedniej zmiany później, będzie to wykonać na poziomie procesu Camudna.

W diagramie wstaw zatem task po start event i zmień "implementation" na właściwy. Nadaj odpowiednie ID. Pamiętaj że "Decision Ref" musi wskazywać na właściwy ID diagramu DMN.

 Stwórz diagram DMN.

Kliknij na fragment diagramu oznaczony na niebiesko by otworzyć okno reguł DMN:

Umieszczenie DMN na platformie

Możemy to zrobić na dwa sposoby. Pierwszy to bezpośrenio z Camundy kliknięcie na przycisk "Deploy current diagram". Drugi sposób to użycie REST i metody 'deployment/create'.

Testowanie DMN

Camunda oferuje możliwość sprawdzenia poprawności warunków DMN przed produkcyjnym wykorzystaniem procesu. Użyj do tego metody REST POST "decision-definition/key/invoice-assign-test/evaluate" wysyłajać jako "Content-type: application/json" zawartość:

{
  "variables" : {
    "kwota" : { "value" : 600"type" : "Double" },
    "dept" : { "value" : "it""type" : "String" }
  }
}