Tuesday, October 8, 2024

Turn SQL into an EBS Excel report

The new and free version of my Excel Out program allows creating Excel reports by pasting a query in the attachment of the concurrent request.


Installation

Install it by downloading this file Excel_Out.zip
Unzip all files in your Oracle Home, either on your local machine or the server and run eo_install.sql from SQL Plus. 

If you prefer using SQLDeveloper, TOAD or similar tool you can directly paste and execute this script: EO_SQL_DEV.sql

Either of these scripts  will install the above 'Excel Out' program that you can run immediately from System Administrator. 

Before you run it the first time you must mount the concurrent request output directory on the Database Server and point the program to it by setting up the profile option "Concurrent Requests Output DIR".

Most, but not all, of companies already have the directory with concurrent request outputs mounted on the database server. If your company does not then you will need to have your Linux admins do this for you. If your DB and application server are running on the same machine, you don't need the mount, but still need to set up the profile value. 
If you are not sure if it is mounted, follow steps below and it will become apparent. 

The physical location of the directory can be identified by running this simple statement:

SELECT OUTFILE_NAME FROM FND_CONCURRENT_REQUESTS where LAST_UPDATE_DATE>sysdate-1

Once you identified the output directory check if it is set up as a database directory. In my case this was:

select directory_name from all_directories 
where directory_path='/dev_oraapp/dev122/fs_ne/inst/DEV122_apdev001/logs/appl/conc/out'

If not you will need to define it as a database directory using the CREATE DIRECTORY syntax. 
If the directory is already defined then most likely it is already mounted on the database server

Once the directory is defined you can set up the "Concurrent Requests Output DIR" profile option:


The PROD and test instances will most likely each need a different value for this profile. 
Now you can run the Excel Out report. 

If the output does not open, but instead you get a message that the output file could not be read that means the concurrent output directory is not mounted on the database server. 

Building a new report

To create a new report copy the Excel Out concurrent request with all of its parameters then click on the attachment icon and add a new SQL attachment with a new query:


DO NOT put a semicolon at the end of the query, this will fail the program. 

If your report needs parameters that are specified during runtime put them in the query using the p_parameterXX syntax. 


During the runtime each p_parameter value will be replaced with the actual value selected by the user in the parameter. 

For dates use syntax:

fnd_date.canonical_to_date(p_parameter01)

if you want to make a parameter optional use the nvl syntax

person_id=nvl(p_parameter03, person_id)

If the person_id is specified in the parameter the query will only return records for that particular person_id, but if the user does not provide a value this will evaluate to :

person_id=person_id

meaning the parameter will have no impact on the query, making it optional. 

Troubleshooting errors

If your report failed to execute, go the the request log, copy the query and try to execute directly in SQL. This should give you an idea of that the problem is. 

Never use a semicolon in the attachment as this will guarantee the report failure. 

How it works

The concurrent request grabs the query from the attachment and writes its output into an Excel file. If you want to have a specific naming convention for the output file, specify the prefix in parameter 90.

The Excel file is written using the AS_XSLX PL/SQL package written by Anton Scheffer.  The AS_XSLX is an excellent piece of software that you can utilize for a number of other Excel reporting requirements. 
I had to slightly modify the procedure query2sheet so I renamed the package to  AS_XSLXEBS to prevent an overwrite in case you already have your own copy of AS_XSLX.
My modifications are:

- Remove the underscore for all column names and make them Sentence Case, bold and underlined
- Adjust the column width to the GREATEST(length(header label), max(data length))

This formatting allows the reports to look more user friendly. 

This is free to use and modify as you please. For details see the disclaimers in the code. 

Enjoy!






Monday, September 16, 2024

Robust messaging system in EBS

 Oracle EBS does not provide a straightforward and efficient tool for communicating important messages to users.

Currently, we have two main options for delivering actionable information to users:

  1. Create a concurrent request to generate a report for the user.
  2. Develop a workflow to send a message.

However, both methods are resource-intensive and add significant overhead to any project where they are implemented.

For the first option, a report must be developed, typically using Oracle Reports, followed by creating a BI Publisher template. This process requires a developer skilled in Oracle Reports and BI Publisher environments.

Workflows, on the other hand, are time-consuming to develop and test. Custom workflows, beyond the seeded ones, are rarely used for communication, which is unfortunate because workflow messages appear on the To-Do worklist that opens upon application login. This worklist contains all pending tasks and can serve as a checklist at the start of each workday.

Below, I outline a simple yet powerful API that enables you to transform any SQL query into a professionally formatted worklist message, offering a more efficient and user-friendly communication tool.

Here is a simple query:

   SELECT Organization_id,  organization_code org_code, calendar_code calendar, creation_date,       last_update_date

    FROM mtl_parameters

ORDER BY 2

instantly transformed into an Excel exportable worklist notification:


An this is the API call:

DECLARE

   query VARCHAR2(4000) := 

      'SELECT 

           Organization_id, 

           organization_code org_code, 

           Calendar_code calendar,

           creation_date,

           last_update_date

       FROM 

           mtl_parameters 

       ORDER BY 

           2';

BEGIN 

   XX_UNIVERSAL_WORKFLOW.start_workflow (

      p_query             => query, -- Query that will retrieve the data for the report

      p_performer         => 'PBELTER', -- Recipient of the notification, application user name from fnd_user table

      p_org_id            => 81, -- Organization ID, mainly to select the corporate logo in the report, different per company

      p_MESSAGE_TITLE     => 'Warehouse listing', -- Notification subject

      p_DOCUMENT_HEADING  => 'List of Warehouses', -- Name of the report printed in the header

      p_GENERIC_MSG_TOP   => 'Here is the list of warehouses:<br>

                              It contains the code and the calendar<br>

                              But also a date when the warehouse was created<br>

                              And when it was updated', -- Introductory message that prints before all the data. Can be in HTML

      p_GENERIC_MSG_BOTTOM => 'So now that we are done:<br>

                              ENJOY THE REST OF YOUR DAY!!' -- Summary message that prints after all the data

   );

END;


To run this you need a very simple workflow with only 1 function that delivers the message:


And the PL/QL package to run it. 

You can download the workflow form here: Download workflow


If the link doesn't work the end of document shows the detailed steps

Below you will find the PL/SQL package with detailed in-line comments and the details of how to set up your workflow. The Workflow needs to be set up only once, regardless of where you call the API from and what queries you are passing.

In the code you will find the corporate logo:
https://www.blogger.com/img/logo_blogger_40px_2x.png
Replace it with the link to your logo

You can download the package from here: Download Package

CREATE OR REPLACE PACKAGE APPS.XX_UNIVERSAL_WORKFLOW
AS

-- By Peter Belter 16-SEP-2024
   -- Define the VARIABLE_TABLE type as a table of VARCHAR2(20000)

   TYPE VARIABLE_TABLE IS TABLE OF VARCHAR2 (20000);

   -- Procedure declaration to print query columns and values

   PROCEDURE print_query_columns (p_query       IN     VARCHAR2, -- Input query to describe and print column names
                                  col_headers      OUT VARIABLE_TABLE, -- OUT parameter to hold column headers
                                  col_values       OUT VARIABLE_TABLE); -- OUT parameter to hold column values

   -- Procedure declaration for building notification body

   PROCEDURE build_ntf_body (document_id     IN            VARCHAR2,
                             display_type    IN            VARCHAR2,
                             document        IN OUT NOCOPY CLOB,
                             document_type   IN OUT NOCOPY VARCHAR2--,  query           IN            VARCHAR2
                             );

  Procedure start_workflow (p_query in varchar2, -- query that will retrieve the data for the report
  p_performer in varchar2, -- receiient of the notification, application user name from fnd_user table
  p_org_id in  number, -- organization ID, mainly to select the corporate logo in the report, different per company
  p_MESSAGE_TITLE in varchar2, -- notification subject
  p_DOCUMENT_HEADING  in varchar2, -- name of the report printed in the header
  p_GENERIC_MSG_TOP in varchar2,  -- introductory message that prints before all the data. Can be in html
  p_GENERIC_MSG_BOTTOM in varchar2 -- summary message that prints after all the data
  )    ;   


END XX_UNIVERSAL_WORKFLOW;
/
CREATE OR REPLACE PACKAGE BODY APPS.XX_UNIVERSAL_WORKFLOW
AS
-- By Peter Belter 16-SEP-2024
   PROCEDURE print_query_columns (
    p_query       IN     VARCHAR2,        -- Input query to describe and print column names
    col_headers   OUT    VARIABLE_TABLE,  -- OUT parameter to hold column headers
    col_values    OUT    VARIABLE_TABLE   -- OUT parameter to hold column values
) IS
    c                   INTEGER;           -- Cursor ID declared within the procedure
    col_count           INTEGER;           -- Number of columns in the result set
    desc_tab            DBMS_SQL.DESC_TAB2;  -- Table of column descriptions
    col_value_varchar   VARCHAR2 (20000);  -- Temporary variable to hold VARCHAR2 values
    col_value_number    NUMBER;            -- Temporary variable to hold NUMBER values
    col_value_date      DATE;              -- Temporary variable to hold DATE values
    exec_result         INTEGER;           -- Variable to store the result of execution

    -- Function to transform column names
    FUNCTION transform_column_name (col_name VARCHAR2) RETURN VARCHAR2 IS
        transformed_name VARCHAR2(4000);
    BEGIN
        -- Transformation logic
        IF col_name LIKE '%_ID' THEN
            transformed_name := INITCAP(REPLACE(SUBSTR(col_name, 1, LENGTH(col_name) - 3), '_', ' ')) || ' ID';
        ELSE
            transformed_name := INITCAP(REPLACE(col_name, '_', ' '));
        END IF;
        RETURN transformed_name;
    END transform_column_name;

BEGIN
    -- Open a new cursor
    c := DBMS_SQL.OPEN_CURSOR;

    BEGIN
        -- Parse the input query
        DBMS_SQL.PARSE(c, p_query, DBMS_SQL.NATIVE);

        -- Describe the columns of the query
        DBMS_SQL.DESCRIBE_COLUMNS2(c, col_count, desc_tab);

        -- Initialize the VARIABLE_TABLE to store column headers and values
        col_headers := VARIABLE_TABLE();
        col_values := VARIABLE_TABLE();

        -- Load column names into col_headers and define columns for fetching values
        col_headers.EXTEND(col_count);

        FOR i IN 1 .. col_count LOOP
            col_headers(i) := transform_column_name(desc_tab(i).col_name);

            -- Define columns dynamically based on their data types
            IF desc_tab(i).col_type = 2 THEN -- NUMBER type
                DBMS_SQL.DEFINE_COLUMN(c, i, col_value_number);
            ELSIF desc_tab(i).col_type = 12 THEN -- DATE type
                DBMS_SQL.DEFINE_COLUMN(c, i, col_value_date);
            ELSE -- Assume VARCHAR2 or CHAR type
                DBMS_SQL.DEFINE_COLUMN(c, i, col_value_varchar, 20000);
            END IF;
        END LOOP;

        -- Execute the query
        exec_result := DBMS_SQL.EXECUTE(c);

        -- Fetch the rows and populate col_values
        WHILE DBMS_SQL.FETCH_ROWS(c) > 0 LOOP
            FOR i IN 1 .. col_count LOOP
                IF desc_tab(i).col_type = 2 THEN -- NUMBER type
                    DBMS_SQL.COLUMN_VALUE(c, i, col_value_number);
                    col_values.EXTEND;
                    col_values(col_values.COUNT) := TO_CHAR(col_value_number);
                ELSIF desc_tab(i).col_type = 12 THEN -- DATE type
                    DBMS_SQL.COLUMN_VALUE(c, i, col_value_date);
                    col_values.EXTEND;
                    col_values(col_values.COUNT) := TO_CHAR(col_value_date, 'dd-MON-yy HH24:MI:SS');
                ELSE -- VARCHAR2 or CHAR type
                    DBMS_SQL.COLUMN_VALUE(c, i, col_value_varchar);
                    col_values.EXTEND;
                    col_values(col_values.COUNT) := col_value_varchar;
                END IF;
            END LOOP;
        END LOOP;

        -- Close the cursor
        DBMS_SQL.CLOSE_CURSOR(c);
    EXCEPTION
        WHEN OTHERS THEN
            -- Close the cursor if an error occurs
            IF DBMS_SQL.IS_OPEN(c) THEN
                DBMS_SQL.CLOSE_CURSOR(c);
            END IF;

            -- Output the SQLCODE and SQLERRM for debugging
            --DBMS_OUTPUT.PUT_LINE('Error in print_query_columns: SQLCODE=' || SQLCODE || ', SQLERRM=' || SQLERRM);

            -- Check if the error is due to invalid SQL syntax
            IF ABS(SQLCODE) BETWEEN 900 AND 999 OR ABS(SQLCODE) BETWEEN 20000 AND 20999 THEN
                -- Handle syntax errors
                --DBMS_OUTPUT.PUT_LINE('Invalid SQL query syntax: ' || SQLERRM);

                -- Replace col_headers and col_values with the default error message
                col_headers := VARIABLE_TABLE();
                col_headers.EXTEND(1);
                col_headers(1) := 'Error';

                col_values := VARIABLE_TABLE();
                col_values.EXTEND(1);
                col_values(1) := 'The query passed to the procedure had invalid syntax';
            ELSE
                -- For other errors, re-raise the exception
                RAISE;
            END IF;
    END;
END print_query_columns;


   PROCEDURE build_ntf_body (
       document_id     IN            VARCHAR2,
       display_type    IN            VARCHAR2,
       document        IN OUT NOCOPY CLOB,
       document_type   IN OUT NOCOPY VARCHAR2
   ) IS
       l_corp_logo       VARCHAR2(4000);
       main_hdr1         CLOB;
       main_hdr_ending   CLOB;
       l_button          CLOB;
       l_document_name   VARCHAR2(4000);
       l_current_date    VARCHAR2(100);
       col_headers       VARIABLE_TABLE := VARIABLE_TABLE();  -- Initialize collection
       col_values        VARIABLE_TABLE := VARIABLE_TABLE();  -- Initialize collection
       html_table        CLOB;
       col_count         INTEGER;
       v_itemtype        VARCHAR2(20);
       v_itemkey         VARCHAR2(50);
       query             VARCHAR2(4000);
       l_org_id          NUMBER;
       l_top_message     VARCHAR2(4000);
       l_bottom_message  VARCHAR2(4000);
       l_missing_query   VARCHAR2(200);
   BEGIN
       -- Initialize CLOB variables
       DBMS_LOB.CREATETEMPORARY(main_hdr1, TRUE);
       DBMS_LOB.CREATETEMPORARY(main_hdr_ending, TRUE);
       DBMS_LOB.CREATETEMPORARY(l_button, TRUE);
       DBMS_LOB.CREATETEMPORARY(html_table, TRUE);
       DBMS_LOB.CREATETEMPORARY(document, TRUE);

       v_itemtype := SUBSTR(document_id, 1, INSTR(document_id, '|') - 1);
       v_itemkey := SUBSTR(document_id, INSTR(document_id, '|') + 1);

       -- Safely retrieve the query attribute
       BEGIN
           query := WF_ENGINE.GetItemAttrTEXT(v_itemtype, v_itemkey, 'QUERY');
       EXCEPTION
           WHEN NO_DATA_FOUND THEN
               l_missing_query := 'The query was not passed into the procedure';
               query := 'SELECT ''' || l_missing_query || ''' AS Error FROM dual';
       END;

       -- Safely retrieve other attributes
       BEGIN
           l_org_id := WF_ENGINE.getItemAttrNumber(v_itemtype, v_itemkey, 'ORG_ID');
       EXCEPTION
           WHEN NO_DATA_FOUND THEN
               l_org_id := NULL;
       END;

       BEGIN
           l_document_name := WF_ENGINE.getItemAttrText(v_itemtype, v_itemkey, 'DOCUMENT_HEADING');
       EXCEPTION
           WHEN NO_DATA_FOUND THEN
               l_document_name := 'Default Document Name';
       END;

       BEGIN
           l_top_message := WF_ENGINE.getItemAttrText(v_itemtype, v_itemkey, 'GENERIC_MSG_TOP');
       EXCEPTION
           WHEN NO_DATA_FOUND THEN
               l_top_message := '';
       END;

       BEGIN
           l_bottom_message := WF_ENGINE.getItemAttrText(v_itemtype, v_itemkey, 'GENERIC_MSG_BOTTOM');
       EXCEPTION
           WHEN NO_DATA_FOUND THEN
               l_bottom_message := '';
       END;

       l_corp_logo := '<a href="https://www.blogger.com/img/logo_blogger_40px_2x.png" target="_blank">'
                      || '<img style="display:block" height="74" alt="Blogger" '
                      || 'src="https://www.blogger.com/img/logo_blogger_40px_2x.png" '
                      || 'width="249" border="0"></a>';
       l_current_date := UPPER(TO_CHAR(SYSDATE, 'dd-mon-yy hh24:mi:ss'));

       -- Create the Export to Excel button with JavaScript
       DBMS_LOB.APPEND(l_button, '<button onclick="exportTableToExcel(''dataTable'', ''exported_data'')">Export to Excel</button>'
                                || '<script>'
                                || 'function exportTableToExcel(tableID, filename = '''') {'
                                || 'var downloadLink;'
                                || 'var dataType = ''application/vnd.ms-excel'';'
                                || 'var tableSelect = document.getElementById(tableID);'
                                || 'tableSelect.style.fontSize = "10pt";'
                                || 'if (!tableSelect || tableSelect.rows.length === 0) {'
                                || 'alert(''The table is empty or not found.'');'
                                || 'return;'
                                || '}'
                                || 'var tableHTML = tableSelect.outerHTML;'
                                || 'filename = filename ? filename + ''.xls'' : ''excel_data.xls'';'
                                || 'downloadLink = document.createElement("a");'
                                || 'document.body.appendChild(downloadLink);'
                                || 'if (navigator.msSaveOrOpenBlob) {'
                                || 'var blob = new Blob([''\ufeff'', tableHTML], { type: dataType });'
                                || 'navigator.msSaveOrOpenBlob(blob, filename);'
                                || '} else {'
                                || 'downloadLink.href = ''data:'' + dataType + '';base64,'' + window.btoa(unescape(encodeURIComponent(tableHTML)));'
                                || 'downloadLink.download = filename;'
                                || 'downloadLink.click();'
                                || '}'
                                || 'tableSelect.style.fontSize = "";'
                                || 'document.body.removeChild(downloadLink);'
                                || '}'
                                || '</script>');

       -- Create the top section of the email
       DBMS_LOB.APPEND(main_hdr1, '<html><head><title>Export Table to Excel</title>'
                                 || '<style>'
                                 || '.exportTable { border-collapse: collapse; }'
                                 || '.exportTable th { padding: 8px; text-align: left; background-color: #f2f2f2; border: none; }'
                                 || '.exportTable td { padding: 8px; text-align: left; border-bottom: 1px solid #cccccc; }'
                                 || 'button { margin-left: 10px; padding: 10px 20px; font-size: 16px; cursor: pointer; }'
                                 || '</style></head><body>'
                                 || '<div style="width: 100%; background-color: #72A1C8; height: 5px;"></div>'
                                 || '<div style="display: flex; align-items: center; padding: 10px 0;">'
                                 || l_corp_logo
                                 || '<div style="margin-left: 40px; display: flex; align-items: center;">'
                                 || '<h3 style="margin: 0; display: inline-block;">' || l_document_name || '</h3>'
                                 || '<span style="font-size: 10px; color: #000000; margin-left: 20px;">' || l_current_date || '</span>'
                                 || l_button
                                 || '</div></div>'
                                 || '<div style="width: 100%; background-color: #72A1C8; height: 5px;"></div>'
                                 || '<div style="height: 25px;"></div>'
                                 || '<div style="font-size: 12px; font-weight: bold; margin-top: 10px; margin-bottom: 10px;">' || l_top_message || '</div>');

       -- Call print_query_columns to get headers and values
       print_query_columns(query, col_headers, col_values);

       -- Prepare the HTML table
       DBMS_LOB.APPEND(html_table, '<table id="dataTable" class="exportTable" style="font-size: 11px; color: #000000; width: 100%;" cellpadding="4">');
       DBMS_LOB.APPEND(html_table, '<thead><tr>');

       FOR i IN 1 .. col_headers.COUNT LOOP
           DBMS_LOB.APPEND(html_table, '<th style="padding: 4px;">' || col_headers(i) || '</th>');
       END LOOP;
       DBMS_LOB.APPEND(html_table, '</tr></thead><tbody>');

       col_count := col_headers.COUNT;

       FOR i IN 1 .. col_values.COUNT LOOP
           IF (i - 1) MOD col_count = 0 THEN
               DBMS_LOB.APPEND(html_table, '<tr>');
           END IF;
           DBMS_LOB.APPEND(html_table, '<td style="padding: 4px;">' || NVL(col_values(i), '&nbsp;') || '</td>');
           IF i MOD col_count = 0 THEN
               DBMS_LOB.APPEND(html_table, '</tr>');
           END IF;
       END LOOP;

       DBMS_LOB.APPEND(html_table, '</tbody></table>');

       -- Add the bottom message
       DBMS_LOB.APPEND(html_table, '<div style="font-size: 12px; font-weight: bold; margin-top: 10px; margin-bottom: 10px;">' || l_bottom_message || '</div>');

       -- Prepare the footer and close the HTML document
       DBMS_LOB.APPEND(main_hdr_ending, '</body></html>');

       -- Combine the header, export table, and footer to create the final document
       DBMS_LOB.APPEND(document, main_hdr1);
       DBMS_LOB.APPEND(document, html_table);
       DBMS_LOB.APPEND(document, main_hdr_ending);

   EXCEPTION
       WHEN OTHERS THEN
           --DBMS_OUTPUT.PUT_LINE('Error in build_ntf_body: ' || SQLERRM);
           -- Safely free the temporary CLOBs without additional checks
           BEGIN
               DBMS_LOB.FREETEMPORARY(main_hdr1);
           EXCEPTION
               WHEN OTHERS THEN
                   NULL;
           END;

           BEGIN
               DBMS_LOB.FREETEMPORARY(l_button);
           EXCEPTION
               WHEN OTHERS THEN
                   NULL;
           END;

           BEGIN
               DBMS_LOB.FREETEMPORARY(html_table);
           EXCEPTION
               WHEN OTHERS THEN
                   NULL;
           END;

           BEGIN
               DBMS_LOB.FREETEMPORARY(main_hdr_ending);
           EXCEPTION
               WHEN OTHERS THEN
                   NULL;
           END;

           BEGIN
               DBMS_LOB.FREETEMPORARY(document);
           EXCEPTION
               WHEN OTHERS THEN
                   NULL;
           END;

           RAISE;
   END build_ntf_body;

   PROCEDURE start_workflow (
       p_query              IN VARCHAR2,
       p_performer          IN VARCHAR2,
       p_org_id             IN NUMBER,
       p_MESSAGE_TITLE      IN VARCHAR2,
       p_DOCUMENT_HEADING   IN VARCHAR2,
       p_GENERIC_MSG_TOP    IN VARCHAR2,
       p_GENERIC_MSG_BOTTOM IN VARCHAR2
   ) IS
       l_itemkey    VARCHAR2(50);
       l_itemtype   VARCHAR2(20) := 'WFUNIVER';
       l_query      VARCHAR2(4000);
   BEGIN
       --DBMS_OUTPUT.PUT_LINE('Workflow is getting initialized');
       l_itemkey := TO_CHAR(XX_UNIVERSAL_WF_S.NEXTVAL);

       --DBMS_OUTPUT.PUT_LINE('l_itemkey=' || l_itemkey);

       wf_engine.createprocess(l_itemtype, l_itemkey, 'UNIVERSAL_NTF');

       --DBMS_OUTPUT.PUT_LINE('Process created');

       wf_engine.setitemuserkey(itemtype => l_itemtype, itemkey => l_itemkey, userkey => l_itemkey);

       --DBMS_OUTPUT.PUT_LINE('User key created');

       wf_engine.setitemowner(itemtype => l_itemtype, itemkey => l_itemkey, owner => p_performer);

       --DBMS_OUTPUT.PUT_LINE('Owner set');

       -- Set item attributes
       WF_ENGINE.setItemAttrText(l_itemtype, l_itemkey, 'PERFORMER', p_performer);
       WF_ENGINE.setItemAttrNumber(l_itemtype, l_itemkey, 'ORG_ID', p_org_id);
       WF_ENGINE.setItemAttrText(l_itemtype, l_itemkey, 'MESSAGE_TITLE', p_MESSAGE_TITLE);
       WF_ENGINE.setItemAttrText(l_itemtype, l_itemkey, 'DOCUMENT_HEADING', p_DOCUMENT_HEADING);
       WF_ENGINE.setItemAttrText(l_itemtype, l_itemkey, 'GENERIC_MSG_TOP', p_GENERIC_MSG_TOP);
       WF_ENGINE.setItemAttrText(l_itemtype, l_itemkey, 'GENERIC_MSG_BOTTOM', p_GENERIC_MSG_BOTTOM);
       WF_ENGINE.setItemAttrText(l_itemtype, l_itemkey, 'QUERY', p_query);

       -- Build the message
       wf_engine.setitemattrdocument(
           itemtype   => l_itemtype,
           itemkey    => l_itemkey,
           aname      => 'MESSAGE',
           documentid => 'PLSQLCLOB:XX_UNIVERSAL_WORKFLOW.BUILD_NTF_BODY/' || l_itemtype || '|' || l_itemkey
       );

       wf_engine.startprocess(l_itemtype, l_itemkey);

       COMMIT;

       --DBMS_OUTPUT.PUT_LINE('Query set');

       l_query := WF_ENGINE.getItemAttrText(l_itemtype, l_itemkey, 'QUERY');
       --DBMS_OUTPUT.PUT_LINE('l_query:=' || l_query);
   EXCEPTION
       WHEN OTHERS THEN
       null;
           --DBMS_OUTPUT.PUT_LINE('Error in start_workflow: ' || SQLERRM);
   END start_workflow;

END XX_UNIVERSAL_WORKFLOW;
/


Here are the workflow builder steps:

Create the new Item Type WFUNIVER


Create new Process UNIVERSAL_NTF



Create attributes:


Attributes that are text:

QUERY
PERFORMER
MESSAGE_TITLE
DOCUMENT_HEADING
GENERIC_MSG_TOP
GENERIC_MSG_BOTTOM ​

ORG_ID is a numeric attribute

MESSAGE is a Document. This is the most important attribute as it contains the message displayed in the notification:



Now define the GENERIC_MSG itself


&MESSAGE_TITLE in the subject and &MESSAGE in the HTML body


For the message to work drag the top MESSAGE_TITLE and MESSAGE attributes to attach them to the message itself.



The GENERIC_NTF notification needs to have the Generic Message attaches to it.


Now it is time to build the Process.
Double click the Universal Notification icon


The drag the Start and End Function in there. The easiest way is to copy them from the "Standard" workflow item type but you can define them from scratch here as well.


It is important to designate this function as "Start" in the node tab or the workflow will not work



Same with the End function:





Drag the newly defined notification into the process and join it to the start and end by holding the mouse right button. 

The notification must have the performer sourced form the item attribute named Performer


Enjoy!


Tuesday, October 6, 2020

The system cannot determine the default tax rate

 Have you encountered the below error when trying to cancel a Sales Order?

"Error: The system cannot determine the default tax rate for tax CITY and tax status STANDARD. Please contact your tax manager either to specify a default tax rate code for this tax status and date &DATE or to define appropriate rate determination rules. (TAX_DET_DATE=20-DEC-19)"

This is an extremely frustrating feature in EBS as the documentation does not give any reasonable clue  why this is happening and besides, what do we care about the tax rate when the order is to be cancelled anyway?

I tried to find where the TAX_DET_DATE comes from and it seemed like this should be the TAX_DATE from OE_ORDER_LINES_ALL.


Well, it is not.


It is PROMISE_DATE from OE_ORDER_LINES_ALL.


This is beyond silly, but when I pointed this out to DEV they just shrugged and said that works as designed. 

Anyway, change the Promise Date on your order lines to be today any you will be able to cancel the order.

Thursday, July 2, 2015

Calling Oracle forms documents form non EBS applications, i.e. OBIEE

Few days ago my colleague while browsing through OBIEE dashboards commented that it would be nice to be able to  click on an invoice on a detailed view and open the actual document.

Well, there is a way to do just that. 

Oracle has a nice document "Integrating Oracle Business Intelligence Applications with Oracle E-Business Suite (Doc ID 555254.1)"

It says:
"In order to generate an Action Link you will first have to do the following:
  1. Identify the Oracle E-Business Suite Application page/function that you want to link to. Obtain the function_id of that page and identify the querystring parameters required by that page. This will have to be done by going through Oracle E-Business Suite documentation."
That's good to know, but ... there is no documentation on it.

In the same note they describe function 

FND_RUN_FUNCTION.get_run_function_url(form function id, resp appl id, resp id, sg id, form params)

Which generates URL to a specific document that can be opened from browser window. That link can be added as a hyperlink to a non web based application and clicking it will bring up the  specific document within EBS. This is a gold mine, and a way to improve efficiency of work, but how do I know how to pass the parameters?

The first parameter is the form function id. This can be retrieved by running fnd_function.get_function_id('function name'). 

Example:

Select fnd_function.get_function_id('AR_ARXTWMAI_HEADER') from dual

The function name can be found by going to the specific form and following the steps below:

The second and third parameters, resp appl id, resp id can be found by going to:

Help -> Diagnostics -> Examine
Block: $PROFILES$
Fields: RESP_APPL_ID and RESP_ID

The last parameter, form params, is the hardest one, and without it the form will open but will not query a specific record.  Doc ID 555254.1 says to find the values by "going through Oracle E-Business Suite documentation", but no documentation exists in that regard. When I asked Oracle Support about it they were not aware of any.

Here is how I worked around it.

When you open a specific document, go to File menu and see if you can place it in navigator


Most of the major documents have this feature available.

When you do that the form will appear in the Documents tab of the EBS navigator


By itself it is no very useful, but clicking on the document will open the particular invoice. This means that the navigator passes the proper parameters to the form to open it. 

To capture those parameters enable forms trace by using instructions  from note 438652.1 section 2.2.
Those instruction are very comprehensive but most of the time it is sufficient to find the site value of the profile ICX: Forms Launcher and copy it to user profile appended with "?record=collect"

Example: https://hostname.domain:port/OA_HTML/frmservlet?record=collect

After setting up the profile option exit Oracle and log back it again for it to take effect.
Go to the Document added to the Navigator then double click on it. 

When done, retrieve the debug file located in $FORMS_TRACE_DIR 
and look for string "other_params"

Here is what I found for the AR invoice

AR_ARXTWMAI_HEADER open_flag: Y session_flag: Y other_params: FP_CUSTOMER_TRX_ID=647666 

This not only tells me the form parameters but also the name of the function, so I could have skipped the step above to find the name. 

Here is what it is for Purchase Order

Executing function: PO_POXPOEPO open_flag: Y session_flag: Y other_params: PO_HEADER_ID= 213180 ACCESS_LEVEL_CODE="MODIFY" POXPOEPO_CALLING_FORM="DESKTOP" 

Now I have all the needed parameters I can call my function:

For AR Invoice:

 select FND_RUN_FUNCTION.get_run_function_url(fnd_function.get_function_id('AR_ARXTWMAI_HEADER'), 222, 20678, 0,'FP_CUSTOMER_TRX_ID='||CUSTOMER_TRX_ID) from RA_CUSTOMER_TRX_All

This query creates URL to every single customer invoice. You can copy and paste it into browser and it will open the invoice. If you are not logged in it will prompt you to log in before opening the document. You must have access to the responsibility or the document will produce no access error. 

Here is syntax for other sample documents, make sure to replace the resp_appl_id and resp_id before running it on your system:

-- Purchase Order(Purchasing Superuser)
select FND_RUN_FUNCTION.get_run_function_url(fnd_function.get_function_id('PO_POXPOEPO'), 201, 20707, 0, 'po_header_id='||po_header_id||' ACCESS_LEVEL_CODE=MODIFY POXPOEPO_CALLING_FORM=DESKTOP') from po_headers_all

Note that I am passing all 3 required parameters for PO.  'po_header_id='||po_header_id||' ACCESS_LEVEL_CODE=MODIFY POXPOEPO_CALLING_FORM=DESKTOP'

Passing just the PO_HEADER_ID will not open the record

-- Quick Sales Order (Order Management Superuser)
select FND_RUN_FUNCTION.get_run_function_url(fnd_function.get_function_id('ONT_OEXOETEL'),660, 21623, 0, 'desktop_header_id='||header_id) from oe_order_headers_all 

Note that the correct parameter is "'desktop_header_id" not "header_id" which is an existing and intuitive parameter but using it will not open the form

--AP Invoices (Payables Manager)
 select FND_RUN_FUNCTION.get_run_function_url(fnd_function.get_function_id('AP_APXINWKB'), 200, 20639, 0,'INVOICE_ID='||invoice_id) from
AP_INVOICES_ALL

Even when a particular function cannot be placed on navigator, it may be still possible to get direct link to it, but it is harder to determine the right parameters. They can be determined by opening the form in Forms Developer and reading them from the triggers.  
The Org items and Material Workbench are examples of this scenario.

--Org Items (Inventory)
select FND_RUN_FUNCTION.get_run_function_url(fnd_function.get_function_id('INV_INVIDITM_ORG'), 401,  20634, 0,'chart_of_accounts_id=50348 org_id='||mp.organization_id||' item_id='||inventory_item_id||' org_code='||mp.organization_code)
 from mtl_system_items_b msi , mtl_parameters mp where msi.organization_id=mp.organization_id

--Material Workbench for an item and org (Inventory)
select FND_RUN_FUNCTION.get_run_function_url(fnd_function.get_function_id('INV_INVMATWB'), 401,  20634, 0,
'ORGANIZATION_ID='||organization_id||' INVENTORY_ITEM_ID='||inventory_item_id||' OWNING_QUERY_MODE=1 G_QUERY_FIND=FALSE')
from mtl_onhand_quantities
-- skip the organization_id if you want availability across orgs 

--Material transactions for Sales Order Issue, for specific item that not older than 3 months and not newer than one month (Inventory). This is an interesting example as it involves dates

SELECT FND_RUN_FUNCTION.get_run_function_url (
          fnd_function.get_function_id ('INV_INVTVTXN'),
          401,
          20634,
          0,
             'MODULE=CST FORM_USAGE_MODE=DSLA_DRILLDOWN INVTVTXN_GO_DETAIL=Y ORG_ID=102 INVTVTXN_INVENTORY_ITEM_ID='
          || inventory_item_id
          || ' INVTVTXN_TRXN_TYPE_ID=33 INVTVTXN_FROM_DATE='
          || SUBSTR (fnd_date.date_to_canonical (SYSDATE - 90),
                     1,
                       INSTR (fnd_date.date_to_canonical (SYSDATE - 90),
                              ' ',
                              1,
                              1)
                     - 1)
          || ' INVTVTXN_TO_DATE='
          || SUBSTR (fnd_date.date_to_canonical (SYSDATE - 30),
                     1,
                       INSTR (fnd_date.date_to_canonical (SYSDATE - 30),
                              ' ',
                              1,
                              1)
                     - 1))
  FROM mtl_system_items_b
 WHERE organization_id = 102 AND segment1 = 'XFR0150R240R'


--WiP Jobs

SELECT FND_RUN_FUNCTION.get_run_function_url (
          fnd_function.get_function_id ('WIP_WIPDJMDF_DEFINE'),
          706, 20560,
          0,
             'GID="'
          || wip_entity_id
          || '" ORG_ID="'
          || organization_id
          || '"')

  FROM wip_discrete_jobs where trunc(creation_date)=trunc(sysdate)


Using those URLs you can integrate third party applications with EBS, but you can also use the same parameters to create personalizations to call one Oracle form another from using the "Launch a Function" builtin.

Here is a proof of concept, you can use variables instead of the fixed values but the principle remains the same.


Click on Apply Now and the item form will open


A problem with calling Oracle from from an URL is that if you already have EBS open the URL may hijack the current session and hard close all the documents on the screen while keeping them locked.

To avoid that use this syntax when calling the URL from a third application:

href="http://url_from_above_queries_here"
target="_blank" name="Option" value="-noframemerging"


Opening the URL with the -nonframemerging option in IE will force a new EBS window to open.



Sunday, May 17, 2015

Email override for concurrent request Delivery Options

R12 offers a great feature sending emails directly from Concurrent Requests using Delivery Options.
Gone are the days where one had to build a new workflow and create a role to email a document to external party. With BI publisher the emails can be very fancy containing colors, graphics end everything else as long as MS Word and HTML can take it.

But adding this new great feature oracle broke something else. The Workflow Email Override Address does not  apply to the Delivery Options and that means that one can send external emails form a cloned test instance. As you can imagine the consequences can be disastrous, i.e. we have a scheduled concurrent requests that email customers the AR invoices. Now if someone started testing AR invoices in the clone instance...

Some companies worked around the issue by blocking the email functionality on clone instances. That works but now one can't properly test the new workflow emails nor concurrent request emails. So there are 3 options:

- you can deploy new functionality related to workflow or concurrent request emailing but risk that critical documents such as invoices get emailed to customers from the cloned instance
- do not deploy any new functionality
- deploy in PROD but without complete test cycle

Surprisingly, Oracle DEV being the siloed organization they are, looked only at the Delivery Option side of the coin and recognized the need as an enhancement request #9308736. ERs are nice but they can take years to deploy, it is 2015 and and couple of  ERs I created in 2002 and 2004 were just recently released.

If waiting 10 or 15 years is not an option for your organization here is how you can fix the issue:


  • Create an email address where the string is equal to the SID of the test database and get access to the email box. It would have this format: TEST@yourcompanyname.com. Replace the values with proper SID and domain names
  • Add this script to the cloning process. The script needs to be incorporated as integral part of the cloning process and executed after the SID has been already changed form prod to the test instance, but before concurrent manager is brought up:
-- this script must be executed
-- after the NAME column has been set in V$DATABASE to the correct clone instance
-- but before concurrent managers get up

--This section sets the workflow email override address to
--be equal to the SID of the test instance @yourcompanyname.com

update fnd_svc_comp_param_vals set parameter_value=(select name from v$database)||'@'||'yourcompanyname.com'
where parameter_id =(select parameter_id from fnd_svc_comp_params_tl where display_name = 'Test Address' );

commit;

-- This section updates all the concurrent requestsincluding those that are scheduled or were being
-- executed when the instance was cloned

-- It updates the email address of every request to eb that of the workflow email override
  update  fnd_conc_pp_actions set argument3=
  ( select fscpv.parameter_value  -- go to workflow override email address
    from fnd_svc_comp_params_tl fscpt
    ,fnd_svc_comp_param_vals fscpv
    where fscpt.display_name = 'Test Address'
    and fscpt.parameter_id = fscpv.parameter_id),
    argument4=( select fscpv.parameter_value  -- go to workflow override email address
    from fnd_svc_comp_params_tl fscpt
    ,fnd_svc_comp_param_vals fscpv
    where fscpt.display_name = 'Test Address'
    and fscpt.parameter_id = fscpv.parameter_id),
    argument5=null,
    argument6=null,
    argument8=null,
    argument9=null,
    argument10=null
    where ACTION_TYPE =7 ;

commit;


  • Create trigger on the FND_CONC_PP_ACTIONS table to update any email specified by user during new request submission to the email override. This script needs to be executed at the same time as the previous one

CREATE OR REPLACE TRIGGER XX_EMAIL_OVERRIDE
BEFORE INSERT or UPDATE
   ON fnd_conc_pp_actions
  FOR EACH ROW
DECLARE

-- This trigger overrides send to  email address  on non production Oracle servers
v_override_email varchar2(100);
v_instance varchar2(100); -- name of the oracle instance 

BEGIN

select name 
into v_instance
 from v$database;
If v_instance!='PROD' -- name of production instance, replace as appropriate
-
 then 
 BEGIN

if :new.ACTION_TYPE =7 -- if the request output is going to be emailed in post processing
 --if :new.ARGUMENT1 ='E' -- if the request output is going to be emailed in post processing
 then
 select decode(fscpv.parameter_value,'NONE',null,fscpv.parameter_value)  -- go to workflow override email address
 into v_override_email 
    from fnd_svc_comp_params_tl fscpt 
    ,fnd_svc_comp_param_vals fscpv 
    where fscpt.display_name = 'Test Address' 
    and fscpt.parameter_id = fscpv.parameter_id;
    
        
   -- if v_override_email!='NONE' then -- if workflow override email is set

:new.ARGUMENT3:=v_override_email; --override sender address
:new.ARGUMENT4:=v_override_email;  -- send the email to the work from over
:new.ARGUMENT5:=null;--delete any addtional cc email addresses
:new.ARGUMENT6:=null;
:new.ARGUMENT7:=null;
:new.ARGUMENT8:=null;
:new.ARGUMENT9:=null;
:new.ARGUMENT10:=null;



--end if; -- end for override email is !='NONE'

end if; -- end of if the output is to be emailed 

EXCEPTION
   WHEN OTHERS
   then null;
   
END;

end if; -- end if the instance is not PROD

EXCEPTION
   WHEN OTHERS
   then null;
   
END;