Report Development Rules Copy link

Description
Category: Report Development
Plugin: Blitz Report Development (report-development)
Works with: Claude Code
Type: Rules loaded automatically when a task needs them

Blitz Report XML rules: parameters, LOVs, anchors, lexicals, templates, assignments, columns, the report engine.

When to use it

  • Use whenever creating or editing a Blitz Report XML or its SQL.
Install

Claude Code

  1. Add the Enginatics marketplace in Claude Code.
    /plugin marketplace add enginatics/ai-skills
  2. Install the plugin.
    /plugin install blitz-report-ai-skills@enginatics
  3. Run the environment setup once; it describes your EBS development instance (never production).
    /blitz-report-ai-skills:blitz-environment-setup
  4. Nothing to run: Claude loads these rules automatically when a task needs them.
  5. Keep it current: enable auto-update for the enginatics marketplace, or update manually.
    /plugin marketplace update enginatics

Requirements: Blitz Report installed on your Oracle EBS development instance, and database access from Claude Code to that instance (Oracle SQLcl MCP server, SQLcl or SQL*Plus). Never point the skills at production.

Import and deploy

  • Import only via $XXEN_TOP/bin/import_xml (scp to /tmp first). Never sqlplus spool for export (pads CLOBs). Never direct DML on xxen_* content tables.
  • import_xml derives parameter and upload dependencies from :$flex$./:$column$. references; do not put PARAMETER_DEPENDENCIES/UPLOAD_DEPENDENCIES in the XML. Verify each intended dependency formed (xxen_report_param_dependencies / xxen_upload_dependencies) — a token matching no parameter/column NAME silently binds NULL. For type='U' it also rebuilds the upload view (xxen_upload.create_upload_view), so columns added through the XML are usable on the target immediately; it does not re-derive the column list from the main SQL (see ebs-11i-compatibility skill).
  • $flex$/$column$ resolve by parameter/column NAME (spaces→underscores, case-insensitive), never the anchor/bind name. Keep anchor and name aligned.
  • Every new/changed report is imported and E2E-tested on the development instance as OPERATIONS with restrictive parameters before reporting done. Baseline before, rerun after with identical parameters, diff all shared columns and key sums.
  • Report SQL is a CLOB and not capped at 32k, on either release or in the 11i export.
  • The setup form greys out a report SQL longer than 32,767 characters. Tell a customer to double-click it, which downloads the full SQL as a file, then load the edited file back through Tools > Upload Large SQL (UTF-8 if it contains non-ASCII characters; developer guide section 6.8, anchor Upload_SQL), not through an XML export and import.
  • XML Upload and bin/import_xml take a .zip: .xml entries are imported in zip order and the first failing entry stops the import (named in the error, earlier entries stay imported); workbook_<doc_id>.eex entries go to the Discoverer staging table and .rdl entries to SSRS. FNDGFU stores application/x-compress for any .zip and text/xml; charset=UTF-8 for an .xml whatever its CONTENT_TYPE argument, so import_xml_file recognises a zip by name, while the form's validate_xml gates on the browser's type (application/%zip%, text/xml): a test that loads files with FNDGFU must set the browser's content type before calling validate_xml. Make files sent to customers 7-bit ASCII (replace en dashes); LPX-00217 invalid character 0 on import = a NUL byte injected in transit, not an encoding/charset issue.
  • xxen_report.sql_to_xml with a multi-row cursor needs a non-null rowset tag.

XML structure

  • GUID required. &apos; in SQL. Report name in REPORT_TRANSLATIONS_ROW, parameter names in PARAMETER_TRANSLATIONS_ROW.
  • <DESCRIPTION> goes inside REPORT_TRANSLATIONS_ROW (elsewhere it is silently ignored), broken into short flush-left paragraphs with blank lines, max 4000 bytes incl. CRLF (ORA-01706 on import). Content: only what the reader cannot infer from the run screen or column headers — grain, derived/as-of columns, cost/date basis, why a column is blank.
  • <VERSION_COMMENTS> immediately after </SQL_TEXT>, fresh on EVERY import (omitting it carries the previous comment forward), max 4000 bytes. Decision: a description-only change keeps the previous VERSION_COMMENTS unchanged — version comments describe SQL/parameter changes only. xxen_reports_h logs the OUTGOING version at each import, so the newest history row shows the replaced version's comment, not the current one. The history row is written only when sql_text changed; to fix a comment on an already-imported version, update xxen_reports_h.comments of the latest row directly (the one sanctioned exception).
  • SORT_ORDER is the parameter upsert key. Never renumber existing rows; add new parameters at max(sort_order)+1 (query it first). The import re-sequences to document order, so a mid-file insert re-keys everything after it — verify with a set comparison of (name, anchor, normalised sql_text), never positionally. For substantial restructures: xxen_api.delete_report(id,'Y') then fresh import. Post-import check: no 1=1/&lexical anchor parameter with blank SQL_TEXT.
  • Copying a TEMPLATES_ROW between reports needs a fresh GUID (templates are matched by GUID globally; reuse overwrites the source's template). Remap DEFAULT_TEMPLATES_ROW/TEMPLATE_GUID too.
  • A template without a TEMPLATE_SHARING row can be picked only by its owner and template super users, and a global default template applies only to users who can pick it: give every delivered template a Site sharing row.
  • export_report exports only the session language and omits <APPS_INITIALIZE_DATA> and <ASSIGNMENTS>. Set nls_language='AMERICAN' before exporting on a client whose locale maps to an uninstalled language.
  • Every shipped import file containing <REPORTS> must carry <USER_NAME>ENGINATICS</USER_NAME> (seeded marker), or the upgrade import deletes every customer-added parameter. Customer-specific parameters on a seeded report survive upgrades only when imported through the frontend as a real user (created_by not -1/ENGINATICS) from a full-copy XML without the marker.

Report / Upload DESCRIPTION (and parameter text) — use line breaks, NEVER one run-on paragraph

A report or upload <DESCRIPTION> (and any multi-sentence parameter description) MUST be broken into short paragraphs separated by blank lines — never a single run-on block of text. This has been raised more than once and keeps regressing — treat it as mandatory on every report/upload, not a polish step.

Why: the description is rendered verbatim in the Forms "Blitz Reports Setup" description editor (a plain multi-line text box with no auto-paragraphing) and on the website report-library page. A wall of text with no breaks is unreadable in both places — the editor shows one long wrapped line.

Placement — <DESCRIPTION> goes INSIDE <REPORT_TRANSLATIONS_ROW>, next to <REPORT_NAME> — NOT loose in REPORTS_ROW. The description is a translated field (stored per language in xxen_reports_tl.description). A <DESCRIPTION> placed directly under REPORTS_ROW (as a sibling of SQL_TEXT / VERSION_COMMENTS) is silently ignored on import — it parses fine, the report/upload imports "successfully", but xxen_reports_tl.description stays NULL and the Forms setup screen and website library page show no description. It must sit in the same translation row as the (translated) report name. Same for uploads (TYPE=U).

Content — only what the reader cannot work out for themselves. A description states what the report is for and the non-obvious mechanics behind its numbers; it never repeats what the run screen and the column headers already say. Do NOT document: that a parameter is optional/required or what it defaults to (both visible on the run screen), org-security or any other standard EBS behaviour, which columns a parameter feeds, or the rationale for an obvious design choice. DO document: the grain of a row, a column that is as-of/derived rather than live, the cost or date basis of a value, why a column is legitimately blank, and anything a user would otherwise raise as a bug. Descriptions run long far more often than short — the review question is always "which sentences can go?", never "what else could I add?".

xxen_reports_tl.description is varchar2(4000). A description already near the limit makes an import fail with ORA-01706: user function result value was too large, raised from XXEN_REPORT_EBS.import_xml_file — the file upload succeeds and only the import step fails, so it reads as a corrupt file rather than an oversized field. Measure the stored description (select lengthb(xrt.description) from xxen_reports_tl xrt where xrt.report_id=... and xrt.language='US') before adding a sentence to a long one.

Descriptions are part of every enhancement, never a follow-up. A change that adds a parameter or alters user-visible behaviour updates, in the same edit: the new/changed parameter’s <DESCRIPTION> (inside its US PARAMETER_TRANSLATIONS_ROW, next to PARAMETER_NAME — the run-screen tooltip; 1–2 sentences covering only what the name and LOV values do not already say), and the report <DESCRIPTION> when the behaviour deserves a sentence there. A shipped feature nobody can find or understand from the run screen is half-delivered.

How to apply in the XML: put real newlines inside the <DESCRIPTION> element, with a blank line between paragraphs. Keep continuation lines flush-left (no indentation) — leading spaces in the XML become literal leading spaces in the stored text (mirror how <SQL_TEXT> lines sit flush-left). One idea per paragraph: what it lists/does → the why/cause/caveat → any usage hint (e.g. "Use the Category parameter to limit the scope.").

  <REPORT_TRANSLATIONS>
   <REPORT_TRANSLATIONS_ROW>
    <LANGUAGE>US</LANGUAGE>
    <REPORT_NAME>My Report</REPORT_NAME>
    <DESCRIPTION>First sentence - what the report lists or does.
Second paragraph - the why / cause / important caveat.
Final short line - any usage hint.</DESCRIPTION>
   </REPORT_TRANSLATIONS_ROW>
  </REPORT_TRANSLATIONS>

The description is capped at 4000 bytes (xxen_reports_tl.description varchar2(4000)) — a longer <DESCRIPTION> makes the whole import_xml fail with ORA-01706: user function result value was too large (an unhelpful stack inside XXEN_REPORT/XXEN_REPORT_EBS with no mention of the description). The stored value keeps the XML's CRLF line breaks (2 bytes per break). When extending an already-long description, count the unescaped bytes including CRLF and trim wordy prose to stay under 4000 — don't drop content.

<VERSION_COMMENTS> is capped at 4000 bytes too (xxen_reports_h.comments varchar2(4000)) and fails the SAME way, with the SAME stack — so ORA-01706 on an import does NOT always mean the description. The stack is identical to the description case (XXEN_REPORT ~4469/4952 → XXEN_REPORT_EBS ~1112), so it names neither element. Measure BOTH elements before hunting — python -c "import re;d=open(f,'rb').read();[print(t,len(re.search(b'<'+t+b'>(.*?)</'+t+b'>',d,re.S).group(1))) for t in (b'VERSION_COMMENTS',b'DESCRIPTION')]" — and note this bites hardest when a review adds a "what changed and why" paragraph on top of an already-detailed comment, i.e. exactly when the comment matters most. Trim the prose; never drop the record of the change.

Verify after import — a NULL/empty description (or no row) means wrong placement (the <DESCRIPTION> wasn't inside REPORT_TRANSLATIONS_ROW); a non-null description with newlines=0 is the run-on regression:

select dbms_lob.getlength(xrt.description) desc_len, regexp_count(xrt.description,chr(10)) newlines
from xxen_reports_tl xrt, xxen_reports_v xrv
where xrt.report_id=xrv.report_id and xrt.language='US' and xrv.report_name='<Report Name>';

Applies to /build-upload, /review-upload, report conversion, and every hand-authored report/upload XML — write the description with breaks the first time.

Parameters

Anchors
  • 1=1/2=2... WHERE anchors: SQL_TEXT is the bare predicate (never prefixed with and). The anchor must be unique in the SQL (the framework injects at every literal occurrence). Use a different anchor only for genuinely different SQL positions (separate UNION branches).
  • &lexical anchors: &amp; in XML. build_sql replaces every occurrence of a lexical anchor, and concatenates the SQL_TEXTs of several parameters on the same lexical anchor with no separator (a 1=1 anchor gets and appended, a lexical nothing), so give each parameter its own lexical anchor. A lexical anchor may live inside another lexical parameter's SQL_TEXT — liveness is transitive; never judge "dead anchor" from the main SQL alone, and match on word boundaries.
  • :bind anchors bind the value directly, no SQL_TEXT. A self-referencing no-op anchor takes nvl(:b,'Y')=nvl(:b,'Y') only when the parameter is optional.
  • Multi-anchor parameters: same name, sub-rows without DISPLAY_SEQUENCE; sub-rows never carry DEFAULT_VALUE, LOV settings or REQUIRED. Hidden parameter = negative DISPLAY_SEQUENCE (not omitted).
  • All anchors used must be declared in <ANCHORS>, WHERE anchors included: the import fills xxen_report_parameter_anchors from that section and build_sql injects a parameter's SQL_TEXT only for a registered anchor. A new n=n anchor on an existing report whose <ANCHORS> lacks it imports cleanly, the run executes the literal n=n and the filter is silently ignored; the executed SQL in the output's Parameters sheet is the proof.
  • A filter SQL_TEXT naming a column the main SQL does not produce imports cleanly and parses on every run that leaves the parameter EMPTY, because the engine injects a parameter’s fragment only when it has a value. It fails the moment someone uses the parameter, as ORA-00904: "X"."<COLUMN>": invalid identifier with ORA-06512: at "SYS.DBMS_SQL" and the fragment quoted in the message. So a parameter can ship broken for years and every regression test still passes. After changing a report’s select list, check each parameter’s SQL_TEXT column against the columns actually produced, and test a run WITH each filter populated, not just the unfiltered one.
  • On a DB_PACKAGE report, a selection parameter the package already filters on STILL keeps its 1=1 anchor and col=:bind SQL_TEXT, and the report must produce that column. The outer predicate is redundant as a filter, but it is what makes the parameter multi-select: moving it to a :bind anchor silently drops the run screen’s multiple-values option, since the engine offers that only for an n=n anchor with a straightforward col=:bind (verify with xxen_api.multiple_values_allowed). QP Customer Pricing Engine Request keeps x.pricing_item_code=:p_item and x.pricing_category_segments=:p_category for exactly this reason, even though XXEN_QP_PREQ’s item-selection cursors already apply both. The column the filter names must be the one for the entity the package filters on, not the one shown to the user: that report carries the priced item’s category as category_segments for display beside the generated Family/Class columns, and the requested item’s as pricing_category_segments for the filter, because the engine selects items by the requested item’s category and the two differ on a promotional-goods row. So this is the one place a predicate that cannot change the result set is correct rather than a #50 violation — do not "simplify" it away.
  • Filtering a scalar-subquery-derived column: visible row on a new anchor in the outermost WHERE (x.derived>=:b) plus a hidden sub-row with a lossless exists semi-join on the inner 1=1 for pruning.
  • A DB_PACKAGE report writes each parameter value into a package variable named after the parameter's ANCHOR, not the name chosen in the package spec, and only for a parameter that HAS a value. An optional parameter's global therefore keeps the previous run's value whenever the session is reused (concurrent manager process reuse, add-in session pool), so beforereport must copy the globals into private body variables and null the public ones immediately; nulling them in afterreport is not enough, as it does not run when the main SQL fails. A value the main SQL binds from the package (a per-run process or sequence id, bound through xxen_xdo.value) stays public, is set at the start of every beforereport, and needs a hidden Number parameter on its bind without a value: the engine binds an untyped package value as a DATE whenever it parses under the session date mask (with a user mask DD.MM.RRRR an add-in run binds process id 10075232 as 10-JUL-5232, ORA-00932).
  • The engine's package binds and afterreport call take the DB_PACKAGE last set by xxen_xdo.db_package(<report id>): by building a report's SQL, and by xxen_api.db_package(<report id>) on a result-cache miss. A beforereport that parses other reports switches it to theirs, so it must end with xxen_xdo.clear_db_package (as XXEN_REPORT_COLS does), which also skips its own package binds and afterreport for the rest of that run.
  • The engine discards a DB_PACKAGE trigger's return value (xxen_xdo.execute_trigger assigns it to an unused local), so return false does not stop the run, and any exception ends the request in Error with the full stack. For an input the user has to correct, end the request in Warning instead: write one log line, call fnd_concurrent.set_completion_status('WARNING',<message>) and return from the trigger; the status is written autonomously, so a rollback in afterreport does not undo it. A run outside a request (Excel add-in, fnd_global.conc_request_id=-1) has no status, so raise the message there: the add-in shows it from xxen_report_runs.completion_message. Reference: XXEN_BOM.warning.
  • India Localization columns: hidden Char parameter (1=1, SQL_TEXT :India_Localization=:India_Localization, MATCHING_VALUE Y, default xxen_util.jai_installed) + one &il_* lexical sub-row per fragment. jai_party_regs rows serve suppliers and customers (supplier_flag/customer_flag required in every join; party_site_id = vendor_site_id or cust_acct_site_id). Parse both branches: on a JAI instance and a non-JAI one.
LOV binding
  • A LOV with an id column passes the id to SQL_TEXT, $flex$ and bind anchors; without one, the display value. Compare ids to id columns.
  • Multi-select parents in a dependent LOV: xxen_util.contains(:$flex$.P, col)='Y'. A parameter is multi-select when its anchor is n=n, it is not Date/matching-value, and SQL_TEXT is a straightforward col=:bind (also inside compound/OR/in (select...) shapes). Verify with xxen_api.multiple_values_allowed(parameter_id).
  • A scalar subquery that needs the latest N child rows (notes, comments): use an uncorrelated inline view with row_number() over (partition by <key> order by <id> desc) rn and correlate on the key outside it. The two-level form (an ordered inline view with rownum inside an in list correlated to the outer row) parses standalone on 19c but crashes the session (ORA-03113, empty request log, Program exited with status 1) once embedded in a large report SQL.
  • Multiple values: col=:bind, <>, like, not like, or col=f(:bind); column on the left; 1=1/lexical anchors only. Never xxen_util.contains in SQL_TEXT. Lookup-code parameters: col=to_number(xxen_util.lookup_code(:p,'TYPE',app)), never wrap the column in meaning().
  • Bind naming: no p_ prefix; _id suffix when the LOV returns an id; plain name otherwise. Never name a bind after an Oracle reserved word (:resource, :level, :size, :mode, :comment, :file): the run fails with ORA-01745: invalid host/bind variable name, but only when that parameter has a value, so unfiltered test runs pass.
  • A template default of a validate-from-list parameter is checked against the LOV when the run is initialised (set_default_and_validate: a value the LOV does not return is dropped without a message - an organization LOV lists only the organizations the submitting responsibility can access) and again at run time by xxen_report_ebs.validate_parameter_values, so a scheduled run of such a template silently runs without that filter. validate_from_list_dsp is nvl(<LOV's flag>,<parameter's flag>): a shared LOV's Y cannot be switched off on the parameter; to bind a value the list rejects (a schedule-only copy of a report), remove the LOV from the parameter (parameter_type Char, lov_id null). Detect a dropped default by the empty value in xxen_report_run_param_vals_tmp after initialize_parameter_values.
  • A parameter's SQL_TEXT may reference ANOTHER parameter's bind (e.g. :period_name_from inside a different parameter's predicate) — the engine binds every bind in the assembled SQL by name across all active fragments. A foreign bind is null when its parameter is empty, so guard each one (:b is null or ...) unless the fragment should only act when that parameter is set. The same holds for the main SQL, and a bind absent from the assembled SQL is skipped: a restriction needed in several query blocks (plan and instance in MSC Pegging Hierarchy) is a hidden parameter with a :bind anchor used directly in each block, not one n=n sub-row per block.
  • A bind in the main SQL takes the value of the parameter whose bind variable has that name, whether or not that parameter's anchor occurs in the SQL; a bind that matches no parameter's bind variable is bound null without an error. So after renaming a bind in the main SQL, or moving a filter out of an n=n anchor into an inline view, check every bind against the parameters' bind names (a rate view binding :p_conversion_date1 while the parameter binds :p_curr_conv_date1 finds no rate and converts at 1).
  • xxen_api.set_parameter_value_ stores raw values (no LOV resolution): pass ids for id LOVs and DD-MON-YYYY dates; to test a multiple-values run, prefix the delimited list with the engine’s marker (<multiple_values>A;B) — a bare A;B is stored as one literal value and silently matches nothing; it does not evaluate hidden defaults — feed them by evaluating each hidden DEFAULT_VALUE with :$flex$ substituted (filter display_sequence<0).
  • Every visible parameter needs a PARAMETER_TYPE_DSP (a null type disables the multi-value like expansion).
Matching values, defaults, dependencies
  • Different WHERE logic per LOV value → one PARAMETERS_ROW per MATCHING_VALUE (primary with DISPLAY_SEQUENCE, sub-rows without), same anchor; MATCHING_VALUE is the LOV id. Never decode/case in one SQL_TEXT.
  • From/To pairs: To defaults from From (:$flex$.from_param; dates last_day(:$flex$.date_from)).
  • A template parameter default with LOCKED=Y (xxen_report_param_default_vals.locked) is fixed for template users (XXEN_REPORT_ACCESS D/S exempt): initialize_parameter_values greys it (required='N', the same sentinel as an empty dependent LOV), xxen_report.apply_locked_defaults forces it onto the run values whatever the client submitted (one request-log line per changed parameter), and the required parameters condition (check_required_params_missing) is evaluated on the final values after the lock. Shipped example: BOM Standard Operations Upload.
  • Dependency handling: (:$flex$.P is null or col=:$flex$.P).
  • DEFAULT_VALUE is evaluated single-row (execute immediate ... into): 0 or 2+ rows → no default; use select distinct when a value can repeat; no count(...)=1 guard. It is the display value for id LOVs when the id is passed directly. Keep default and LOV restrictions aligned. Don't reuse a multi-value download filter as an upload-column default.
  • A LOV Oracle parameter takes filter before display from the value set's Long List flag (fnd_flex_value_sets.longlist_flag, XXEN_REPORT_PARAMETERS_V); the import drops FILTER_BEFORE_DISPLAY_DSP for it. A value set listing a large master table (all suppliers) is therefore a LOV custom with the value set's id/value columns and FILTER_BEFORE_DISPLAY_DSP=Y, or the add-in downloads the whole list on every open.
  • LOV Oracle value sets: PARAMETER_TYPE_DSP=LOV Oracle + LOV_NAME. Yes/No parameters use the Yes_No value set (matching-value pattern for Y/null flags: Y → col='Y', N sub-row → col is null); the shared single-value Yes LOV is for on/off filters. Genuinely custom two-option labels stay custom.
  • Shared LOVs first (content/lovs.xml; LOV+LOV_NAME+LOV_GUID); VALIDATE_FROM_LIST=Y by default; FILTER_BEFORE_DISPLAY only for 100k+ LOVs (Item LOVs always). Exported VALIDATE_FROM_LIST_DSP/LOV_QUERY_DSP next to a shared LOV are the fallback payload for a target lacking it — never strip them; only a literal N is invalid.
  • Custom LOV over a security-scoped master table: take the shipped LOV's where clause verbatim, change only the select list. Standard LOVs: HR Operating Unit 8E2FF36EDEB979D2E0530100007F1FF2, FND User Name 8E2FF36EDE8479D2E0530100007F1FF2, FND Responsibility Name 8E2FF36EDE9E79D2E0530100007F1FF2, GL Ledger 8E2FF36EDEB879D2E0530100007F1FF2, Yes 8E2FF36EDEA679D2E0530100007F1FF2 (all name-only).
  • Parameter names signal the operator: X (exact), X like (user wildcards, validate off), X starts with.
  • Grey-out until a parent is set: VALIDATE_FROM_LIST=Y + flex-dependent LOV with both id and value returning zero rows until the parent qualifies; incompatible with free typing.
  • Excluding a parameter per template requires it non-required; enforce its effective default in the consuming column (nvl(:$flex$.p,'X')); PARAMETER_EXCLUSION_ROW name must match exactly.
  • A report XML exported by a current build (no <PARAMETER_DEPENDENCIES> element) imported into an older build that still reads it wipes that report's dependencies; run xxen_report.update_parameter_dependencies(report_id) there after import.
Access
  • <ASSIGNMENTS> is a top-level sibling of <REPORTS> (nested = silently ignored), rows keyed by REPORT_NAME. Levels are full words (Site, Application, Request Group, Responsibility, User, Form, Function, Report); APPLICATION_SHORT_NAME required for Request Group/Form; omit <DISABLED> for active rows. Not round-tripped by export_report.
  • Reports: derive from the module's data — Application level only where no low-privilege responsibility is owned by it; request groups the normal responsibilities use; major-country localization groups; HR via request groups, never Application=Human Resources. Uploads: Function level only, update-enabled type='FORM' functions (never query-only, never OAF/JSP — such uploads get no assignment).
  • MOAC: pick ONE — (1) org_id in (select organization_id from mo_glob_org_access_tmp union select fnd_global.org_id from dual where fnd_release.major_version=11) + optional OU parameter (preferred), (2) required OU parameter with restricted LOV, (3) Ledger/OU advanced-required. SQL_TEXT binds haouv.name=:operating_unit.
  • INV data with an optional Organization Code parameter: add mp.organization_id in (select oav.organization_id from org_access_view oav where oav.resp_application_id=fnd_global.resp_appl_id and oav.responsibility_id=fnd_global.resp_id) to the main SQL (not the Success SQL).
  • FA book security is a VPD policy, not a view predicate: fa_book_controls_sec is the APPS synonym of fa.fa_book_controls under policy FA_POLICY (fa_security.build_predicate), which adds org_id is null or org_id in (select organization_id from per_organization_list where security_profile_id=<FA: Security Profile>) and nothing at all when the responsibility has no FA_SECURITY_PROFILE_ID. FA has no security tables of its own; the profile is an HR security profile. To evaluate book access for a responsibility other than the session's, apply that predicate to fa_book_controls with fnd_profile.value_specific('FA_SECURITY_PROFILE_ID',user,responsibility,application).
  • GL flex value security: (<segment alias>.security_enabled_flag='N' or xxen_util.flex_value_accessible(resp_appl_id,resp_id,value_set_id,value)='Y') / flex_ccid_accessible(...), never gl_security_pkg. Oracle applies a value set's security rules only through segments with Security Enabled, so a value set shared by a secured segment (company) and an unsecured one (intercompany) restricts only the secured one; always test the flag of the segment the list belongs to. In main SQL apply flex_ccid_accessible only through the hidden GL Flex Value Security parameter (&gl_flex_value_security lexical, default x, SQL_TEXT returning the predicate from dual where xxen_util.has_flex_value_security='Y'), never inline: an inline call, even OR-gated on has_flex_value_security, runs per row for responsibilities without rules (cache-based, needs init, Discoverer-only on 11i). Acceptance: Blitz Report Text Search for gl_security_pkg returns 0 rows.
  • KFF segment LOVs: xxen_util.kff_segment_lov(...) (returns id~value~description), never fnd_flex_values_vl (empty for table-validated sets; over-lists dependent segments). Pass all sibling segments as :$column$ for uploads; shared report LOVs stay parent-less by design.

Parameter LOV Defaults

  • Reuse existing LOVs first: Before creating a LOV custom, always check content/lovs.xml for an existing predefined Blitz Report LOV or Oracle standard LOV that matches. Use PARAMETER_TYPE_DSP>LOV + LOV_NAME + LOV_GUID to reference it. Only create LOV custom when no suitable predefined LOV exists. This applies to both reports and uploads.
  • Validate From List: Always checked (Y) by default on all parameter LOVs
  • Filter Before Display: Unchecked by default. Only set it for LOVs with very large datasets (100K+ records) — large master tables (customers, suppliers, items) or large transaction tables (SO orders, AR invoices, etc.). Do NOT set it for small/medium LOVs (form functions, lookups, organizations, etc.) as it adds unnecessary navigation steps. When in doubt, scan xxen_report_parameter_lovs for existing LOVs on the same data source to see if they use Filter Before Display.
  • Default values with ID LOVs: When the parameter LOV has an id column, the DEFAULT_VALUE can be the ID directly (e.g. fnd_profile.value('PER_BUSINESS_GROUP_ID')) — no need for a subquery to resolve the display name. The framework displays the corresponding value column automatically. The engine validates an evaluated default of an id/value LOV as the id first and falls back to the display value, so a literal default must be the display value.
  • A function call is a valid DEFAULT_VALUE on its own — never wrap it in select … from dual. xxen_report.default_value_query turns fnd_profile.value('ORG_ID'), fnd_global.*, xxen_util.*, sys_context(...), :$flex$.* expressions, sysdate arithmetic on a Date parameter, a case … end and calls of the standard SQL functions (is_function) into select <expression> from dual itself; only text it does not recognise as a function or a select is taken as a literal.
  • DEFAULT_VALUE evaluation is single-row, not multi-row — the framework wraps the expression in a query that effectively does select <expr> from (<param values>) where rownum=1. The expression NEVER sees the full multi-value list at once. Two consequences:
    • You cannot write case when length(:$flex$.X) - length(replace(:$flex$.X,',')) > 0 then ... end to detect multi-value selection inside a DEFAULT_VALUE — the substitution gives you a single value, the comma count is always zero. Such conditionals are non-functional.
    • If a multi-value report parameter is used as DEFAULT_VALUE for an upload column (e.g. :$flex$.organization_code), the framework picks ONE of the selected values for the default. That may not be what the user expects.
  • A DEFAULT_VALUE that is a SELECT query defaults ONLY when it returns exactly ONE row — no count(...)=1 guard needed; use select distinct when a value can legitimately repeat. The framework evaluates a query-form default via execute immediate <default_query> into <single_var>, so: 0 rows → NO_DATA_FOUND → no default; 1 row → that value is the default; 2+ rows → TOO_MANY_ROWS → no default (it does NOT pick an arbitrary first row). Consequences:
    • Never wrap the query in a (select count(...) …)=1 single-occurrence guard — it is redundant with the execute … into single-row enforcement. Just write the plain query.
    • When the query CAN return the same value more than once (a join fans out duplicate rows of the same value), add select distinct so those duplicates collapse to one row and the default still applies. When the multiple rows are genuinely different values (an ambiguous key), distinct won't collapse them and no-default is the correct outcome.
    • The default query's restrictions/joins should match its column's LOV query — same customer/entity universe — so the auto-defaulted value is always one the LOV would also offer. Keep the two in sync (a default keyed off hz_cust_accounts.status='A' while its LOV uses a party-usage restriction can default a value the LOV then rejects).
  • Don't reuse a download-filter parameter as an upload-column default — report parameters whose purpose is to restrict the download set (e.g. Organization Code filtering which existing locators appear in the template) are a different concept from "what should a brand-new upload row default to". They look interchangeable but they aren't: a multi-value download filter doesn't have a well-defined "default value" semantic, and forcing one (by inheriting the parameter) silently picks one of several. For Required upload columns where no single sensible default exists, leave the DEFAULT_VALUE unset — REQUIRED=Y will surface the choice to the user per row.
  • Hidden parameters: Use a negative DISPLAY_SEQUENCE (e.g. -10). Do NOT omit DISPLAY_SEQUENCE — that makes it a sub-row, not a hidden parameter.
  • SQL_TEXT required for lexical and WHERE anchors: Parameters using 1=1/2=2 or &amp;lexical anchors must have SQL_TEXT — these are substituted into the main query at runtime. For parameters not used as query restrictions (e.g. hidden helper params), use a self-referencing identity: :p_param_name=:p_param_name. Parameters using bind anchors (:p_param_name) do NOT need SQL_TEXT — the value is bound directly.
  • Parameter name must reflect its operator behavior — the user-facing name signals what the parameter does in SQL:
    • X (plain) — exact match (col=:x). Validate-from-list enforced (no wildcards).
    • X like — LIKE operator (col like :x). User types wildcards manually (ABC%). Validate-from-list must be off.
    • X starts with — LIKE with auto-appended % (col like :x||'%'). No explicit wildcards needed by user.
    • Applies when renaming Item From/To → Item, Supplier From/To → Supplier, etc. If you switch the operator (e.g. = → like), rename the parameter accordingly.
  • Shared-LOV flags are authoritative in the DATABASE — but the export XML deliberately carries them, so NEVER strip them from exported content. When a column or parameter resolves a shared LOV (or an Oracle value set), xxen_report_parameters.validate_from_list / filter_before_display / lov_query are stored null and the shared LOV governs. That is enforced by the import itself, not by convention: import_xml_clob derives lov_id as coalesce(match by LOV_GUID, match by LOV_NAME) (value set by name for LOV Oracle) and then stores nvl2(lov_id,null,<..._dsp>) — same expression for upload columns and parameters. Verified: validate_from_list is null for all 998 shared-LOV upload columns.
    • <VALIDATE_FROM_LIST_DSP> / <FILTER_BEFORE_DISPLAY_DSP> / <LOV_QUERY_DSP> next to <LOV_NAME>+<LOV_GUID> in an export are NOT an override — they are the fallback payload. When the shared LOV does not exist on the target instance lov_id resolves to null, the nvl2 stores the exported query and flags, and the column/parameter imports as a self-contained custom LOV that still validates. Deleting them would silently remove that fallback, so a target system missing the LOV would import the column with no query and no validation. The export is intentionally a superset of the database row.
    • So the only genuinely invalid value is N: on the fallback path it is persisted, breaking the Y-or-null storage convention (see rule #41 in coding-standards skill). Shipped content currently holds these two elements 9,982 times, always Y.
    • If you need different behavior than the shared LOV provides (wildcards, filter-before-display (FBD) off, etc.), either find a different shared LOV (query xxen_report_parameter_lovs), fix the shared LOV in content/lovs.xml, or switch to LOV custom (no shared-LOV reference at all — column/parameter owns its own LOV completely).
    • Linter rule L4 therefore flags only the literal value N, on both PARAMETERS_ROW and UPLOAD_COLUMNS_ROW. It previously flagged the mere presence of these elements on any row carrying a LOV_NAME, i.e. it read the export as if it were database state — 527 false positives across 69 of the 95 shipped uploads, with zero true positives available to it, and "fixing" them would have stripped the fallback.

Conditionally Greying Out (Disabling) a Run-Screen Parameter

A run-screen parameter is greyed out / disabled (the Forms field becomes non-editable, FLEX_NOT_EDITABLE) only when its computed required value is 'N'. The engine sets required:='N' in exactly one place — set_single_lov_value — and the conditions are strict:

  1. the parameter is VALIDATE_FROM_LIST='Y' (this is the gating condition — set_single_lov_value is only called when validate_from_list='Y' and filter_before_display is null), AND
  2. it is flex-dependent (its LOV_QUERY_DSP references :$flex$.Parent) — so the LOV result changes with the parent's value, AND
  3. its LOV currently returns zero rows (no_data_found), AND
  4. it is not a required parameter (required is null).

The form reads xxen_report_run_param_vals_tmp.required: 'N' → greyed + alterable off; 'Y' → required colour; null/other → normal editable.

To gate a parameter (grey it out until a parent is set): make it a LOV custom with VALIDATE_FROM_LIST_DSP=Y, and write the LOV so it returns rows only when the parent qualifies, e.g.:

-- greys out until "Include accounts with open invoices" = Yes, then offers 1..999
select to_char(d.n) id, to_char(d.n) value, null description
from (select level n from dual connect by level<=999) d
where :$flex$.Include_accounts_with_open_invoices='Y'
order by d.n

The LOV must expose both id and value columns — the engine runs select x.value, x.id from (<lov>) x, so a value-only LOV raises ORA-00904 and the grey-out never fires.

Grey-out and free-text typing are mutually exclusive. Because the grey-out path requires VALIDATE_FROM_LIST='Y', a validate-from-list-OFF (free-text / plain Number) parameter is never greyed. You cannot have both "disabled until parent set" and "type any arbitrary value":

  • Want the grey-out? Use VALIDATE_FROM_LIST=Y + a flex-dependent LOV. The user picks from the list — but if the LOV enumerates the full sensible range (e.g. 1..999), the user can still type any in-range value directly (it validates against the list), which covers most "any number" needs.
  • Want free typing of any value? Use a plain Number (or validate-OFF) parameter. It will not grey out, but a value that only matters under another parameter is harmless if the main SQL ignores it (e.g. nvl(:include,'N')<>'Y' or :days is not null and ...) — document that in the report description.

Multiple Values Parameters

For parameters that support selecting multiple values, use simple column=:bind SQL_TEXT. The framework auto-detects these patterns and shows a "multiple values" checkbox. At runtime it replaces column=:bind with column IN (...).

Supported SQL_TEXT patterns:

column_name=:bind_variable
column_name<>:bind_variable
column_name like :bind_variable
column_name not like :bind_variable

The bind may also be wrapped in a single-argument function and multi-value still works (the framework expands to column in (function(v1), function(v2), ...)):

column_name=function(:bind_variable)

e.g. mil.inventory_location_type=to_number(xxen_util.lookup_code(:locator_type,'MTL_LOCATOR_TYPES',700)) — see "Lookup-Code Parameters" rule below for why this form is preferred over xxen_util.meaning(column)=:bind.

Rules:

  • Column must be on the LEFT side: column=:bind works; :bind=column does NOT
  • Only works with 1=1 or &lexical anchors, not :bind anchors
  • Do NOT use xxen_util.contains in SQL_TEXT — that pattern has the column wrapped in a function on the right side, which defeats auto-detection
  • xxen_util.contains is for LOV_QUERY_DSP dependent filtering only (:$flex$.param syntax)

Reference: Blitz Report Developer Guide - Multiple Values

SQL patterns

  • Decision: a currency conversion to another currency never defaults a missing rate to 1. The rate is 1 when the source and target currency are the same (gl_daily_rates holds no same-currency rows); otherwise a missing rate leaves the rate and the converted columns blank and the row is still reported. A changes-only filter that compares converted values falls back to the source currency values when a rate is missing, so the row is not dropped.
  • Add columns on nested inline views in the OUTERMOST query; xxen_util.dff_columns aliases cannot pass through an inline view. Regression proof: additive diff + unchanged row count.
  • A once-per-transaction column is gated on the report's grouping discriminator, not just its row number. Where the SQL unions invoice and adjustment branches that discriminator includes the adjustment id, so the column carries the adjustment's own figures: a new amount column must branch on it and read the adjustment's own source column, never a subquery keyed on the transaction id, which on an adjustment row is the invoice's and double-counts on sum. Grep the SQL for the measure name first — a matching c_adj_* column is the tell the report already models it, and an _abs allocation-basis column is the near-miss. A null adjustment source renders blank, which is faithful.
  • DYNAMIC_COLUMNS=Y is for raw templates only, never a workbook-backed template (check xxen_report_template_files); a template's grain comes from its workbook, not its column list.
  • A template controls grain: Distinct and per-column Aggregation wrap the SQL (group by every un-aggregated displayed column); columns absent from TEMPLATE_COLUMNS are hidden. MAX on a varchar2-typed number is a string compare.
  • Freeze panes: <FREEZE_PANES>Y</FREEZE_PANES> on the one template column that becomes the first scrollable column; the columns to its left and the header row are frozen. To freeze a few key columns of a wide report, list them first in the template. With DYNAMIC_COLUMNS=Y an unlisted column (category or &lexical columns) is placed directly after the nearest listed column that precedes it in the SQL, and unlisted columns ahead of the first listed one go to the very front, inside the frozen area. Seed a freeze on a report template only when it is wider than a screen (about 15 columns) and starts with identifying columns (item, assembly, document number, bank account, customer); a prefix of ledger, operating unit, period or account segments is context, not a key.
  • xla_transaction_entities joins: nvl(xte.source_id_int_1,-99)=id plus ledger_id and entity_code (matches the function-based index). Reference it as xla.xla_transaction_entities — the unqualified APPS name is a secured view that silently returns zero rows outside a set XLA security context. AP validation date/user come only from XLA %VALIDATED% events; the user is max(created_by) keep (dense_rank first order by creation_date).
  • Derived totals reconcile to their components with the same predicate (AR Aging Past Due Amount uses the aging-basis decode).
  • A row_number()=1 gate that undoes the row multiplication of an outer join (e.g. SLA lines) must partition by the identity of the row before that join, never by a parent key: a parent key zeroes the amounts of every other legitimate row of the same parent (AR Aging receipts on two accounts, or a receipt that is both unapplied and at risk). Amounts that exist once per parent (original amount) keep a parent-level rank; order that rank by the row-level partition columns so Oracle computes both with one WINDOW SORT.
  • R12 AR "accounted" test for receipt applications and cash receipt history: posting_control_id<>-3 (set by final Create Accounting, not only by GL transfer). Do not test for an SLA line linked to the application's ar_distributions row: receipt UNAPP lines are often linked to the cash receipt history distributions instead, so that test drops accounted receipts. Transactions use the RECEIVABLE line linked to the REC ra_cust_trx_line_gl_dist row (AR Aging Exclude Unaccounted mirrors Oracle's By Account report this way).
  • A chargeback payment schedule must not be joined to ar_adjustments on chargeback_customer_trx_id: the creating adjustment and its REVERSE_ADJUSTMENT both carry it, so the join returns two rows per chargeback. Read the originating transaction through a scalar subquery with distinct (AR Aging's salesperson of a chargeback).
  • xla_flex_pkg.get_flex_value_meaning raises for an unknown constant — use nvl((select max(flex_value_meaning) from fnd_flex_values_vl ...),constant).
  • no_merge on inline views to protect a cheap PL/SQL function can cost more than it saves — measure.
  • A pipelined table function driven by an inline view (from (select ...) s, table(xxen_util.kff_segment_lov(... s.id_flex_num ...)) l) is called for every row of the merged view when the view's filter contains a subquery: the optimizer places that subquery as a FILTER above the COLLECTION ITERATOR. Put /*+ no_merge*/ on the inline view's select; no_unnest there has no effect, since it only acts inside the subquery it is written in.
  • For an empty VALIDATE_FROM_LIST=Y parameter without filter-before-display that is required or $flex$-dependent, the engine runs the LOV query with where rownum<3 on every run-screen open and again when a parent changes; value validation runs it with rownum=1. Both use the text without its trailing order by, which xxen_util.remove_order_by finds as the FIRST order by in the query: an earlier one (an analytic or keep (dense_rank first order by ...)) leaves the sort in place. A pipelined LOV function stops after the rows these checks need, but a sort, select distinct or group by above it fetches the whole value set first, including any per-value security calls. Keep such LOVs free of blocking operators: in a kff_segment_lov LOV over several charts of accounts, deduplicate the driving structures instead, one per value set (group by fifs.flex_value_set_id in the no_merge view, preferring a segment without Security Enabled with nvl(min(decode(fifs.security_enabled_flag,'N',fifs.id_flex_num)),min(fifs.id_flex_num))), as the GL Segment LOVs do.
  • A parameter fragment's not exists subquery with (:bind is null or ...) guards is never unnested — it runs as a per-row FILTER — so its correlation must be an index probe (PK/unique, or the row's own columns), never an unindexed column of a large table (gl_je_headers.accrual_rev_je_header_id has no index: correlating on it scans every header of the period per outer row). If a per-row shape cannot express the test, drop the OR guards (nvl/coalesce sentinels) so the subquery stays unnestable, and verify the plan shape with explain plan, not just the result.
  • Measure a join rewrite at the report's widest realistic scope with a full fetch, never a count or a narrow sample: pre-aggregating a large view with select distinct before an outer join blocks join-predicate pushdown, so it can win on a few rows and lose by an order of magnitude at full scope. Then run it as a report and read the run cursor's plan (dbms_xplan.display_cursor on the XXEN_REPORT - <report> module): EBS runs with bind peeking off, so the bind plan can differ from the literal test's — typically the optimizer starts from a small lookup table (organizations) and scans the driving table's date range once per lookup row; pin it with leading(<driving alias>) in that query block, and add no_merge(<lookup view>) for an inline view whose merged tables the optimizer would otherwise cartesian-join to a driving set it estimates at one row (a price list view joined as all its lines times every transaction); check the hinted statement under literal dates as well as binds, since each exposes a different join order.
  • The optimizer pushes a report's join predicate into an outer-joined view only one level deep: if that view outer-joins another non-mergeable view (e.g. xxen_upload_parameters_v over xxen_upload_columns_v), the inner view is built for all rows and filtered afterwards (plan: VIEW PUSHED PREDICATE above a full VIEW with a filter on the join key). Put push_pred(<inner alias>) inside the outer view; a standalone test of the view filtered by its key does not show the problem.
  • A join predicate is pushed into an inline view with window functions only on a column that is in every partition by of the view (ONT Orders and Lines: the order line view partitions by header_id). A parameter that reaches such a view only through another key (invoice lines joined on line_id) leaves the optimizer two plans, both over every line: build the whole view, or cartesian-join all headers of the operating unit to push header_id. Add a lossless <header alias>.header_id in (select ...) semi-join to that parameter's SQL_TEXT, driven by an indexed column of the restriction, so the plan starts from the matching headers. A semi-join on an unindexed column (schedule ship date) leaves the plan unchanged, and one placed inside the view is re-executed per pushed header.
  • A count(*) test evaluates neither the select list nor its cost; materialise the columns (set autotrace traceonly statistics) and compare buffer_gets for both shapes.
  • Do not join a lookup table at the outer level of a report built on a multi-branch union all view (mrp_orders_sc_v, the MSC order views): the optimizer can OR-expand the view (VW_ORE_* in the plan) and probe its branches once per lookup row instead of pushing the plan/organization predicate in once. Fetch per-row values keyed on a unique index as correlated scalar subqueries, which keeps the view's plan, and compare v$sql buffer gets of the concurrent run before and after.
  • A connect by whose source is a join, or an inline view joined to another table, builds that join over the whole source before the hierarchy, and under CONNECT BY WITH FILTERING once per level: restrict the source inside the inline view (plan and instance for msc_full_pegging), not only in start with. Never walk each row's subtree in a correlated connect by scalar subquery (rows × subtree size); enumerate all subtrees once with connect_by_root and group by the root, as MSC Pegging Hierarchy does for its QOH fulfillment status.
  • Run every n=n parameter of a connect by report once with Multiple Values ticked: its expansion (col in (select xrrpv.value_char from xxen_report_run_param_values xrrpv ...)) meets two 19c optimizer defects there that a single-value run never shows. In an inline view that is itself a source of the hierarchical join it can end the session with ORA-07445 [kkocfbMarkBindFlags] (the request ends 'Program exited with status 1' with an empty log; the incident is in the alert log): put the predicate one inline-view level deeper. In start with on a column computed by a correlated scalar subquery it returns roots that do not match (null values pass the in): produce that column through an outer-joined inline view instead.
  • oe_price_list_lines is a synonym for qp_price_list_lines_v, whose inventory_item_id is a PL/SQL function of the list line, so a join on it cannot use an index: the optimizer joins the price list by cartesian product and evaluates the function per candidate row (55 million calls in one run). Join the base tables: qp_list_lines (list_line_type_code='PLL', operand is the list price) to qp_pricing_attributes on list_line_id with product_attribute_context='ITEM', product_attribute='PRICING_ATTRIBUTE1' and product_attr_value=to_char(<item id>), plus mtl_system_items_b for organization_id=qp_util.get_item_validation_org, which the view applies too.
  • Every lookup view outer-joined per line runs as VIEW PUSHED PREDICATE with its own SORT GROUP BY per driving row, so sibling views over the same tables (one per tax type, one per discount type) cost one probe and one sort each per row: merge them into one conditional-aggregation view. A lookup keyed on a header-level column (salesrep, customer_trx_id) is cheaper as a scalar subquery with /*+ no_unnest */ — without the hint 12c unnests it into a VW_SSQ pushed view and the per-value cache is lost — and the key column, not the looked-up value, goes into the group by.
  • fnd_flex_xml_publisher_apis.process_kff_combination_1's 7th argument takes a segment qualifier (GL_BALANCING, BASED_CATEGORY) or a segment NUMBER, so a run-screen "which segment" parameter can drive it directly; a NULL there ends the session with ORA-03113, so wrap a parameter-driven one in case when :p is not null then ... end. Its DESCRIPTION mode returns the description CUT to the segment's display width (25 characters on the Vision account segment, 20 on cost center and on both category segments, against category descriptions of up to 53), so read descriptions with xxen_util.segment_description(value, application_column_name, id_flex_num[, parent_value, application_id, id_flex_code]) instead, which returns the whole value; it resolves a dependent value set only when given the parent segment's value, so coalesce the parent form, the parentless form and the flexfield api. Resolve the segments once per combination in a /*+ no_merge */ inline view restricted to the combinations the report uses (in (select ... from <driving table>)), never per row: ten per-row calls over 729 rows ran for minutes, the same report 0.7 s through the inline view. A count(*) test hides the cost because the optimizer drops the unreferenced calls.
  • Item cost "last update" = max(cicd.last_update_date) from cst_item_cost_details, not the header.
  • org_access_view is every inventory organization times every fnd_responsibility row (end-dated included), each pair tested with an org_access probe, so an unrestricted query costs organizations × responsibilities (68 million buffer gets on 38,000 responsibilities). Every responsibility sees the organizations that have no org_access rows, so evaluate the view once for those (any responsibility, minus organizations present in org_access) and per responsibility only for responsibilities present in org_access, as FND Responsibilities does. Restricting the view to the selected responsibilities with a semi-join does not help: the optimizer then joins them to every HR organization before the inventory-class filter.
  • Joins between data dictionary and v$ views get pathological plans because their fixed tables have no useful statistics: dba_data_files joined to dba_tablespaces and dba_free_space, dba_users with dba_users_with_defpwd and dba_ts_quotas, and gv$undostat with gv$tablespace and gv$parameter each took 10-60 s, although each view alone answered in about a second. Wrap the driving view in (select /*+ no_merge*/ ... ), join per-key aggregates as grouped no_merge inline views instead of correlated scalar subqueries, and time the whole statement, never the views one by one. A no_merge on the wrong view makes it worse (on dba_tablespaces: 0.6 s to 12 s).
  • v$resource_limit returns no rows inside a pluggable database, and EBS on 19c always runs in one. A report on it needs a fallback for that case: count gv$process/gv$session against the processes/sessions values in gv$parameter.
  • Decision: a DBA or system administration report gets a parameter that restricts the output to the problem state (Not Running only, Errors only, or a threshold such as Max Used Percent From), so it can be scheduled as an alert. A scheduled run that returns no rows sends no email.

Templates and pivots (engine)

  • A template hides a column with a TEMPLATE_COLUMNS_ROW that carries the COLUMN_NAME and NO DISPLAY_SEQUENCE. The hidden column on xxen_report_template_columns does not exclude anything — it only pushes a column to the end of the sheet. A column with no row at all is pulled IN when the template has dynamic_columns='Y' (XXEN_REPORT.pkb: "excluding only manually hidden ones having xrtc.display_sequence=null"). So a template whose export lists a subset of the columns hides nothing — it only orders the ones it names, and every other column still appears. Judge a template by its sequence-less rows, never by which columns the export lists.
  • xxen_report_template_columns_v filters out the sequence-less rows, so counting hidden columns through the view returns zero for every template on the instance and reads as "nothing is ever hidden". Count against the base table xxen_report_template_columns.
  • Dynamic columns (xxen_util.dff_columns, item_category_columns) resolve to instance-specific names (Family, Inv.Items Description), are stored as template column rows but are NOT exported, and arrive through dynamic_columns='Y'. Never write hidden rows for them: the names change with the parameter that generates them.
  • Pivots are definition-only (refreshOnLoad); Excel renders on open. Tabular layout must set compact="0" outline="0" defaultSubtotal="0" on EVERY field incl. page/unused/value fields (minutes-long refresh otherwise). Compact page/row/column fields carry <items count="1"><item t="default"/></items>; tabular fields none. PDF output and multi-sheet overflow skip the pivot (skip_pivot, warning suggests a Data-Model pivot in an uploaded template). Test pivot changes on >100k rows.
  • The built-in TEMPLATE_PIVOT cannot put subtotals on SOME levels: PIVOT_LAYOUT='T' writes defaultSubtotal="0" on every field, and the default layout subtotals every row level, above the group, with all row fields nested in one compact column. A group-left layout with subtotals on chosen levels only (an RXi attribute set, a Discoverer group sort) needs an uploaded workbook. FIELD_TYPE is FILTER, ROW, COLUMN or VALUE - PAGE counts towards pageFields but produces neither an axis nor a pageField child, so the pivot loads empty.
  • import_xml DELETES a template's workbook when the imported TEMPLATES_ROW carries no <FILE_DATA> (xxen_report_template_files goes to zero rows and the template silently reverts to a raw column list). A hand-edited content file therefore cannot ship a workbook-backed template: hand over xxen_api.export_report(report_id,null,null,'Y','Y') instead, which embeds each workbook as base64, and replace the <APPS_INITIALIZE_DATA> block it writes from the exporting session with the seeded ENGINATICS/SYSTEM_ADMINISTRATOR/SYSADMIN marker. Round-trip the result before handing it over - a re-import must leave the part count unchanged.
  • Building a template workbook from scratch is reliable through Excel automation (New-Object -ComObject Excel.Application; wrap every call in a retry, Excel rejects calls with RPC_E_CALL_REJECTED while it is busy). Copy the shipped shape: data sheet FIRST and named after the report, headers exactly as the engine writes them plus one sample row, AutoFit so the sheet gets its <cols> block, a defined name SourceData = OFFSET('<data sheet>'!$A$1,0,0,COUNTA('<data sheet>'!$A:$A),COUNTA('<data sheet>'!$1:$1)), the pivot built on that name with RefreshOnFileOpen set, and a Parameters sheet last. Register with xxen_report.upload_excel_template(template_id, <fnd_lobs file_id from FNDGFU>). To read back what Excel actually renders, open the output, save a copy and parse that - the generated file holds only the pivot definition.
  • The engine keeps an uploaded template's data-sheet <sheetView>, so freeze panes set in the template survive generation: freeze below the header row on the data sheet and below the pivot's filter and header rows on the pivot sheet, or both scroll out of sight.
  • Excel object model traps when scripting a pivot: xlDoNotRepeatLabels is 1 (2 REPEATS them), a field promoted to the page axis is placed ABOVE the ones already there so the filters are added bottom up, PivotField.Position does not reorder page fields, and HasAutoFormat=False also switches off the column auto-fit on refresh, which leaves every column at the sample row's width.
  • A page field on a parameter the report can only run one value of renders as (All), not that value, so it tells the reader nothing: make such a column the leading ROW field instead, where it shows the value once. For a report-style pivot also set ShowDrillIndicators=False (the in-cell +/- buttons) and Workbook.ShowPivotTableFieldList=False. DisplayFieldCaptions cannot be used to drop the filter drop-downs: in a tabular layout the captions ARE the column headings and go with them.
  • Uploaded template data sheet must contain a <cols> block (auto-fit while sample rows exist) and no xl/calcChain.xml (strip it and its declarations), or the generated file is malformed. Diagnose with ET.fromstring per sheet part.
  • The data sheet is the worksheet named after the template (xxen_report.sheet_name), else the FIRST worksheet; the resolved answer is stored as xxen_report_template_files.sheet_type='data' and in the XML as <DATA_SHEET_FILE_NAME> — read that rather than inferring, because the template-named sheet is often the pivot sheet. sheetN.xml numbering follows neither tab order nor creation order; resolve a sheet by name through workbook.xml and its rels.
  • Adding a column to a workbook-backed template shifts five positional places: the data sheet <cols>, cacheField order (each carrying its own numFmtId) and fieldGroup/@base, the pivot tables' @x/@fld/@baseField, _xlnm._FilterDatabase, and the report XML's <TEMPLATE_STYLES>. Remap the workbook accordingly; <TEMPLATE_STYLES> must be remapped by hand because import_xml loads it verbatim and only upload_excel_template re-derives it from the workbook. Verify by name resolution — every @x/@fld/@baseField must resolve to the same column name before and after and every numFmtId must be unchanged. A date rendering as a raw serial after an insert is its numFmtId left on the wrong cacheField.
  • <TEMPLATE_STYLES> <CELL_IDENTIFIER> is a column letter (whole column) or a header cell (H1), and the workbook <cols> carries widths only, so it is the single source of column formatting: give every new column a header-cell row, an amount style if it is money, and a <cols> width or its header clips. Audit the whole amount set from the parsed xxen_report_sql_columns data types, excluding numerics that are identifiers or rates. A dynamic SourceData name (OFFSET(..,COUNTA(..),COUNTA(..))) sizes itself to new columns and beats a literal ref, but does not fix the positional formatting above.
  • Power Query templates: load to the Data Model only (Table and Data Model reintroduces the 1M ceiling and a second model table); update_connection_refresh sets refreshOnLoad on type="100" connections when xl/model/item.data exists (never on type="5"); a WorksheetConnection_… PQSOURCE model table cannot refresh — rebuild the pivot on the query-fed table; a worksheetSource name="Table1" pivot sees only the first data sheet — use From Data Model. Test with a scratch template (xxen_xdo.copy_template + FNDGFU + upload_excel_template).
  • Dashboard templates: KPI cards read pivots via GETPIVOTDATA (ColumnGrand = bottom total row); set HasAutoFormat=False on a pivot sharing a sheet with hand-placed layout.
  • A template that the LOV lists but that opens as a BLANK Templates window with no error, while its row, columns, sharing and usable all check out, has a leading/trailing blank in template_name. Forms trims the value when it is returned into the item; populate_template and template_id then match template_name with a plain =, find nothing, and query_template skips the query. The importer trims (xxen_xdo trim(substrb(doc_name…))) but import_xml stores TEMPLATE_NAME verbatim, so the padding arrives through a hand-built or tool-built XML, or a name typed with a trailing space in the form. Diagnose with template_name<>trim(template_name); fix by trimming (the unique index xxen_report_templates_u3 (report_id, template_name) rejects a collision).

A template controls ROW GRAIN, not just columns — Distinct and per-column Aggregation

A report template is not only a column chooser. xxen_report.add_template_clause wraps the whole report SQL:

select [distinct] <template columns, aggregated where AGGREGATION is set>
from ( <report sql> ) x
[group by <every displayed column that has NO aggregation>]
[having <&template_having_clause>] [order by ...]

So hiding detail columns alone does NOT collapse rows — but the Distinct checkbox (xxen_report_templates.distinct_flag) and the per-column Aggr. dropdown (xxen_report_template_columns.aggregation, values SUM/COUNT/MIN/MAX/AVG/COUNT_DISTINCT) both do.

  • Every displayed column without an aggregation goes into the GROUP BY, so one un-aggregated detail column re-splits the rows. The group by is only emitted when at least one column carries an aggregation.
  • A column with no TEMPLATE_COLUMNS_ROW at all is hidden, unless the template sets DYNAMIC_COLUMNS=Y, which includes every unlisted column. Leave it off on a summary template or a later SQL change silently re-splits the output. A row that IS listed but has no DISPLAY_SEQUENCE is hidden either way.
  • Aggregated columns are auto-labelled in the output header, e.g. Start Quantity (Sum).
  • MAX on a VARCHAR2-typed numeric column is a STRING comparison ('9' > '10'). Check the parsed data_type before choosing MAX — see coding-standards #65 for the decode(x,0,null,x) idiom that causes numeric columns to be typed varchar2.

Distinct alone is usually the wrong tool for a parent/child report. Where a parent-level value is deliberately emitted on only the FIRST child row — the decode(row_number() over (partition by <parent> order by <child>),1,<value>) idiom — Distinct yields exactly two rows per parent: one carrying the values, one all-null. Verified on a 10,283-row WIP Entities extract: Distinct gave 2 rows for all 1,172 jobs, whereas aggregating the quantity columns gave 1 row per job with the totals unchanged.

WIP operation quantities are additive per job — except on eAM maintenance work orders. Summing the operation-level position quantities (in queue, running, waiting to move, rejected) reconciles exactly to the job's remaining quantity, and operation scrapped reconciles to job scrapped. On discrete and lot-based jobs this holds without exception, but many maintenance jobs exceed their start quantity, because eAM operations are parallel tasks against one asset rather than a serial material flow. wip_operations.quantity_completed is the one genuinely non-additive column on any entity type — it counts a unit again at every operation it clears; its job-level equivalent is wip_discrete_jobs.quantity_completed, which is already a separate report column.

RXi (Report eXchange Designer) reports

  • An RXi report is a pair: RX-only: <name> runs a PL/SQL extraction that fills an interface table, and RXi: <name> only formats that table according to the attribute set picked on its run screen. Convert the extraction, never the formatter. fa_rx_reports gives the interface table and the concurrent program; the extraction is the concurrent program's executable (FARX_C_<x>.<proc> delegating to FARX_<x>).
  • Inside the extraction, fa_rx_util_pkg.assign_column(seq, select_expression, interface_column, plsql_target, type) IS the select list and the ITF column each value lands in - a null select expression means the value is derived in the after_fetch trigger instead, and a null interface column means it is fetched but never stored. fa_rx_util_pkg.From_Clause / Where_Clause hold the rest of the statement; build_select adds no distinct and no order by.
  • Attribute sets are the templates: fa_rx_attrsets_b/_tl (one per set) and fa_rx_rep_columns_b/_tl (display_status, attribute_counter for the displayed order, ordering, break/break_group_level). Map displayed columns in attribute_counter order to TEMPLATE_COLUMNS.DISPLAY_SEQUENCE, and ASCENDING to SORT_ORDER.
  • Baseline an RXi report without the run screen: submit RX-only: <name> to fill the interface table, then submit the RXi program with PUBLISH <the RX-only request id> <report id> <attribute set> HTML as its first five arguments (FARXPBSH also takes DIRECT <report id> <attribute set> (TEXT|HTML|CSV) [args]). Wrong arguments complete with Invalid Arguments and print the usage into the request log. The HTML output is one clean table with the group values blanked and the subtotal rows labelled, which is the layout to reproduce; the TEXT output wraps every row over several lines and is worth reading only for the page header.
  • The OFA RXi population is fa_rx_reports joined to fnd_concurrent_programs/fnd_executables for application_id=140 - 30 rows, each naming its extraction procedure and interface table, with the attribute set count from fa_rx_attrsets_b. Three further RX-only/RXi pairs have no fa_rx_reports row of their own (RXFAACA/RXFAADA/RXFACCA, the Asset Cost / Accum Deprn / CIP Cost Balance "(Alternate)" programs): they are not extra reports, FA_RX_ALT_BAL calls the same farx_bl.*_balances_rpt routines as FARX_C_BL and differs only in the argument layout that sets the group-depreciation drill-down style, which a converted report exposes as a parameter. When judging which are worth converting, classify with the evidence-based taxonomy in custom-reports-migration skill (complete map / partial map / new report) rather than inventing one, and heed its gate: a column-name comparison is not a classification, the data sources and the row grain decide it.
  • Do not reproduce the framework's defects: fa_rx_flex_pkg.get_description walks a value set only until the first value asked for and returns the bare code for every value it did not reach, so its description columns are mostly codes; a segment number tested with <> '' is never true, so the specified-category-segment columns are always null; and choosing a category segment without both value bounds binds variables the statement does not contain and ends the run with ORA-01006.
  • The FA balance reports (FASCOSTS, FASRSVES and their Blitz conversions on XXEN_FA_FAS_XMLP) take fa_adjustments.source_type_code through fa_balrep_pkg unchanged, so a RESERVE or REVAL RESERVE row can carry source ADJUSTMENT (an impairment on a revalued asset, group depreciation). FASRSVES has no bucket for it and flags the row out of balance with the impairment as the amount; FARX_BL (bug 9293000) adds it to its Adjustments figure together with tax and revaluation.
  • FASCOSTS prints one column after Retirements, and caprevalformula fills it with Revaluation for COST but Capitalization for CIP COST, so Oracle's CIP layout never shows revaluation although its out-of-balance test counts it.
  • XXEN_FA_FAS_XMLP stages the balance rows into msc_form_query, a session-scoped global temporary table. To test a movement the instance has no data for, insert synthetic rows for a real asset and distribution in your own session and run the report SQL directly; other sessions cannot see them.
  • RXi's balance reports (FARX_BL) fill FA_BALANCES_REPORT_GT with their own copy of the extraction, not fa_balrep_pkg, so they can differ from FASCOSTS/FASRSVES and the Blitz conversions on edge rows: on the development instance OPS CORP the CIP asset 100335 (one zero-cost B deprn detail row) is listed with zeros by FASCOSTS and Blitz and omitted by RXi, with identical totals. OPS CORP CIP assets 100343, 100401, 100641 and 100642 have no fa_adjustments rows, so all three reports show their ending balance with no addition and flag them out of balance; that is Vision data, not a report defect.

Discoverer-migrated content

  • Hardcoded worksheet conditions become per-template excluded parameters that still apply their template default (engine change in initialize_parameter_values); identification is by origin (parameter_value null), never by name/operator. Their SQL text can carry literal values, which then AND with whatever the user picks on the run screen — a template that silently forces another company or ledger returns no rows for any other selection.
  • Output file name token <template_name> underscores blanks; <template name> keeps them; report_host only recognises the underscore spelling when no template is selected — change both or neither.
  • The Output File Name option names the browser download and the email attachment in both delivery modes; it takes plain text as well as tokens. Without it both carry the default name (report and template by profile Blitz Report File Name Convention, plus the run's date and time); the Server File Name never names them. A date token in the option resolves to the time it is evaluated, not the run start, so two evaluations can differ by a second.
  • The server folder copy is named by the Server File Name option for every format, and by the output file's name (report, template, request id) when the option is empty; the Output File Name never names it.
  • The email subject and body come from the FND message named in profile Blitz Report Email Subject Message / Body Message (name starts with XXEN_REPORT_EMAIL_SUBJECT / _BODY, 30 characters at most), in both delivery modes. Tokens (xxen_bes.replace_subject_tokens): &REPORT_NAME, &TEMPLATE_NAME, &OUTPUT_FILE_NAME (the attachment name of the advanced delivery without its extension), &ROW_COUNT, &SITE_NAME, &DB_NAME, &USER_NAME, &REQUEST_ID. The advanced delivery reads the message at each run; the standard delivery stores the text on the request's delivery option, which report_host writes at the first run and EBS copies to every next occurrence of a repeating request. A schedule therefore keeps the subject of its first run: a changed message or profile reaches it only when it is cancelled and submitted again, so set the subject up before submitting schedules.
  • A failed email ends the request in Warning in both delivery modes: the advanced delivery's mailer exits with status 1 when it cannot send, and XXEN_REPORT.prog and XXEN_UPLOAD.prog report that, and a missing SMTP host or port, as Warning with the mailer's error in the request log. Both modes accept recipients separated by ,, ; or line breaks.
  • A converted report over a multi-branch union all view that burns CPU for hours with almost no I/O and produces no rows has been OR-expanded once per branch. Read the outline data (dbms_xplan with ADVANCED): USE_CONCAT repeated per branch with OR_PREDICATES(n) in the tens, alongside OPT_PARAM('_optim_peek_user_binds' 'false') which EBS sets site-wide so the optimizer never sees that the run-screen values are selective. Both are correctable on the statement alone — select /*+ opt_param('_optim_peek_user_binds','true') no_expand */ — with no change to the view. Check the outline first: AWR stores no predicate information, so it is the only evidence available once the cursor has aged out.
  • A run-screen range filled with a match-everything pair (0000 to zzzz) costs more than leaving it blank, because an unset parameter is dropped from the anchor entirely while a filled one adds a predicate that selects every row. Clear ranges that are not needed rather than widening them.
  • col between decode(:from,'%',col,:from) and decode(:to,'%',col,:to) is a Discoverer carry-over naming the column on both sides of its own predicate, which defeats cardinality estimation and index access. The engine already omits unset parameters, so rewrite it as a plain col between :from and :to.
  • Correlated scalar subqueries carrying and rownum=1 are the usual per-row cost in a converted view and are evaluated once per output row. Only cutting rows cuts them, so make the filters selective instead of rewriting the subqueries.

Supplier Scheduling (CHV)

chv_schedule_headers/_items/chv_item_orders (normalised buckets; RELEASE rows carry document_shipment_id)/chv_horizontal_schedules (7 row_type rows × column1..60, dates as 'YYYY/MM/DD' strings). Un-pivot with table(xxen_util.rowgen(60)) + 60-way decode + max(case row_type ...). On-time-delivery (OTD) spine = release shipments, requested = min(due_date), receipt = earliest RECEIVE in rcv_transactions.

ASCP / MSC planning server data model

  • MSC_CATEGORY_SETS holds ONE row per category set NAME, shared by every source instance -- not one row per instance. MSC_CL_ITEM_ODS_LOAD (run by Planning ODS Load Worker) inserts a row only when no row with that name exists anywhere, with SR_INSTANCE_ID of -1 and SR_CATEGORY_SET_ID set to the same sequence value as CATEGORY_SET_ID (Oracle's own comment: "dummy value to satisfy the unique constraint"). Every later collection then UPDATEs that shared row, overwriting SR_INSTANCE_ID, DEFAULT_FLAG, DESCRIPTION and CONTROL_LEVEL with the instance being collected.
  • So msc_category_sets.sr_instance_id records the LAST instance to collect that name and is NOT ownership. Never restrict a category set by it -- mai.instance_id = mcs.sr_instance_id, mcs.sr_instance_id = mov.sr_instance_id, sr_instance_id = :$flex$.Instance_id. On a multi-source planning server such a predicate silently hides most category sets, and returns nothing at all for any instance that was not collected last. default_flag is global and last-writer-wins for the same reason.
  • The real source id lives ONLY in MSC_CATEGORY_SET_ID_LID (sr_instance_id + sr_category_set_id -> the shared category_set_id). msc_category_sets.sr_category_set_id is the dummy and always equals category_set_id; never join it to mtl_category_sets.
  • msc_item_categories.category_set_id is the shared surrogate and its sr_instance_id is the collecting instance, so a row for instance A legitimately points at a category set row stamped instance B. That is by design, not a collection fault. Scope categories by mic.sr_instance_id, which is a real instance column, and join mic.category_set_id = mcs.category_set_id with no instance predicate on mcs.
  • category_set_id is globally unique (MSC_CATEGORY_SETS_U1), so a lookup keyed on category_set_name alone returns exactly one row (no rownum), and a scalar subquery keyed on category_set_id cannot raise ORA-01427.
  • A single-source dev instance cannot reproduce any of this: there the surrogate, the dummy and the real source id all coincide and every row carries the one instance, so a wrong instance predicate is a silent no-op that tests clean. Check multi-source assumptions against the collection source (MSC_CL_ITEM_ODS_LOAD in all_source), never against the development instance, a 12.2 instance or an ASCP Vision instance.
  • A :$flex$ reference reaches the SQL as sys_context('XXEN_FLEX',...), which Oracle evaluates locally and never ships across a database link. A @A2M_DBLINK LOV or default that filters a remote table by it inside an unnested exists or join pulls the WHOLE remote table across the link (msc_item_categories for the Category Set LOV: minutes per run-screen default and validation on a large planning server). Join the flex-filtered table (msc_apps_instances) in the outer query and probe with exists (select /*+ no_unnest */ ...) correlated on its columns, or, where the remote rows are the result (Category LOV), join with leading(<flex-filtered aliases>) use_nl(<remote table>): nested-loop and correlation values are shipped as binds, so each probe runs as an indexed query on the planning server. Check the remote SQL with dbms_xplan.display(null,null,'BASIC REMOTE') over a loopback link; a REMOTE line without a WHERE is the defect.
  • MSC content reaches the planning server through &a2m_dblink when Blitz Report runs on the EBS source. Oracle then executes the query on the source and makes every per-row lookup and msc_get_name&a2m_dblink call a network round trip (2,652 rows in 70 s). Write the main query block so that it executes on the planning server as a whole, as MSC Pegging Hierarchy does. Every function goes through the link (xxen_util.meaning&a2m_dblink too; Blitz Report is installed there). No subquery in START WITH (ORA-02016): mark the start rows with an outer join to a start-row view instead. No correlated subquery in the source view of a connect by (ORA-02070 on 19.21, accepted on 19.29). No WITH block referenced twice, because it is materialized locally. No select-list expression that computes a DATE (trunc, date arithmetic, to_date, sysdate): return the raw date and truncate it in an enclosing /*+ no_merge(x) */ select. Verify with explain plan through a loopback link on the development instance (create database link ... using its own service, dropped afterwards): the block must show as one REMOTE operation, and every parameter's SQL_TEXT must keep it that way.

AR customer payment terms — four independent fields, never nvl across them

  • A customer carries four unrelated payment term fields. Each report or upload column reports exactly one of them; an nvl across two makes a single column report a different field from row to row, depending only on whether the first source happens to be populated.
    • hz_customer_profiles.standard_terms with site_use_id is null — the AR profile term at account level (Customer form Profile tab), what AR invoices on.
    • hz_customer_profiles.standard_terms with site_use_id is not null — the same at site-use level. The form allows a profile only on BILL_TO, DUN and STMTS site uses, but seeded and API-created data carries one on other purposes (Vision has 410 active SHIP_TO ones in OU 204 alone), so outer-join on site_use_id and let it return nothing rather than restricting by site use code.
    • hz_cust_site_uses_all.payment_term_id — the site use Order Management term, the rung OM defaults an order from.
    • hz_cust_accounts.payment_term_id — obsolete in R12 (decision): never report it and never feed it to the pricing engine.
  • Name the column after its source (account_profile_payment_term, site_profile_payment_term, site_use_payment_term), unless the report already disambiguates by a level prefix — QP Customer Pricing Engine Request uses customer_/ship_to_/bill_to_/price_list_/pricing_, so there the bare name is correct and renaming would only churn its templates.
  • Error signature: a Site Payment Term populated on a Ship To row while every other Site -prefixed profile column on that row is blank. Counting how many rows of each Site column populate per site use code finds it in one query.
  • XXEN_QP_PREQ’s customer cursor builds the pricing request payment term from the same choice, and that value is passed to the engine as oe_order_pub.g_hdr.payment_term_id, so it can move prices when a modifier qualifies on payment terms. A report-only fix leaves the engine input wrong and Pricing Payment Term disagreeing with Customer Payment Term — change the package cursor with the report.

AP XLA journal-line reports

xxen_xla.xla_je_lines_rpt_pkg_init sets p_include_user_trx_id_flag to 'Y' only when a fnd_concurrent_requests row exists for fnd_global.conc_request_id (Oracle's identifier branch crashes outside a request). Never guard by journal source (parse paths run the lexical too; a blank source silently answers a different question). The Posted Payment/Invoice Register XMLs nvl(<Oracle identifier>, <own source>) for supplier name (ap_sla_*_transaction_v.party_name, never ap_suppliers.vendor_name), amount and document; test the identifier for null rather than wrapping the whole expression.

DFF columns in reports

Reports use dynamic DFF columns via lexical parameters — never hardcode table.attribute1, table.attribute2, etc.

Pattern:

  1. In SELECT: &amp;dff_columns lexical placeholder
  2. In GROUP BY (if aggregating): &amp;dff_columns_grp lexical placeholder (must NOT be last — put a real column after it)
  3. DFF Display parameter: shared LOV Blitz Report DFF Display (V/C/S options), default fnd_profile.value('XXEN_REPORT_DFF_DISPLAY'), anchor &amp;dff_columns. Leaving the parameter blank hides DFF columns entirely (performance escape hatch).
  4. SQL_TEXT: select xxen_util.dff_columns(p_table_name=>'table_name',p_table_alias=>'alias',p_column_name_prefix=>'Prefix: ',p_display_mode=>:dff_display) from dual
  5. Sub-row (no DISPLAY_SEQUENCE) for GROUP BY anchor: same parameter name, anchor &amp;dff_columns_grp, SQL_TEXT also passes p_display_mode=>:dff_display and uses p_hide_column_name=>'Y'

A dynamic-column generator hardcodes the table ALIAS, so renaming an alias in the main query breaks it. xxen_util.dff_columns and xxen_util.item_category_columns take p_table_alias as a string in the generator’s own SQL_TEXT, and the columns they emit are prefixed with it. Change an alias in the main select (for example mtl_system_items_b_kfv msik2 → mtl_system_items_vl msiv2 to get a translated description) and the generated text still says msik2., so the report fails at run time with ORA-00904 on a column nobody wrote. The generator rows sit far from the main SQL in the XML and no grep of the main query finds them — after any alias rename, search the whole file for the old alias and check every p_table_alias.

Uploads use static/hardcoded DFF columns (xu.attribute1, xu.attribute2, etc.) because dynamic columns cannot be mapped to upload API package parameters.

Report conversion testing

  1. Match all original parameters — check the original concurrent program's parameter form to identify ALL parameters, especially required ones (yellow background in Oracle Forms). The Blitz version must have the same required parameters.
  2. Never run transaction reports wide open — always specify a date range or other restrictive parameters. Wide-open runs create massively large and slow output files. Find suitable test parameters first — in R12 Vision, dates around 2006-2008 usually have good test data.
  3. Always set restrictive parameters before test runs — at least one filter (date range, organization, delivery ID, etc.) to limit the result set to a reasonable size.
  4. Import the concurrent program first — use xxen_api.import_concurrent_program to auto-create the Blitz report with correct parameters, LOVs, defaults, and required flags. Then update the SQL to flatten/inline formula functions and convert bind-style parameters to 1=1 anchor style where multiple values support is desired.
  5. API testing with ID parameters — set_parameter_value_ bypasses LOV resolution and passes raw display values. For LOVs with id columns (e.g. organization_id id, organization_code value), pass the numeric id value (e.g. 207) not the display value (e.g. M1). The frontend resolves correctly; only the API needs this workaround.

Existing Report Change Testing Rules

Any change to an existing report XML must follow the standard before/after workflow : run the baseline report on the development instance with restrictive parameters, deploy the edited XML, rerun with identical parameters, compare every shared output column, and verify each new or changed column against the source table/API. Do not treat XML parsing or SQL inspection as sufficient testing.

OAF web LOVs

  • xxen_report_oaf.lov_sql wraps the authored LOV as an inline view and appends parameter_id outside it, so a UNION LOV is safe on the web run screen. ORA-01789/JBO-27122 from a web LOV on an old instance = a union LOV on a pre-fix body; workaround is typing the value.

Templates and snippets

Report XML skeleton

<ROOT>
 <APPS_INITIALIZE_DATA>...</APPS_INITIALIZE_DATA>
 <REPORTS>
  <REPORTS_ROW>
   <GUID>hex-guid</GUID>
   <SQL_TEXT>...with &apos; escaping...</SQL_TEXT>
   <ENABLED>Y</ENABLED>
   <REPORT_TRANSLATIONS><REPORT_TRANSLATIONS_ROW><LANGUAGE>US</LANGUAGE><REPORT_NAME>Name</REPORT_NAME></REPORT_TRANSLATIONS_ROW></REPORT_TRANSLATIONS>
   <CATEGORY_ASSIGNMENTS><CATEGORY_ASSIGNMENTS_ROW><CATEGORY>Enginatics</CATEGORY></CATEGORY_ASSIGNMENTS_ROW></CATEGORY_ASSIGNMENTS>
   <ANCHORS>...</ANCHORS>
   <PARAMETERS>...<PARAMETER_TRANSLATIONS>...</PARAMETER_TRANSLATIONS>...</PARAMETERS>
  </REPORTS_ROW>
 </REPORTS>
</ROOT>

Max SORT_ORDER before adding a parameter

select max(xrpv.sort_order) max_sort_order
from xxen_report_parameters_v xrpv
where xrpv.report_id=(select xr.report_id from xxen_reports_v xr where xr.report_name='<Report Name>');

Blank SQL_TEXT on an anchor parameter (broken merge)

select xrpv.parameter_name, xrpv.anchor
from xxen_report_parameters_v xrpv
where xrpv.report_id=xxen_report.report_id('<Report Name>')
and (xrpv.anchor like '%=%' or xrpv.anchor like '&%')      -- WHERE / lexical anchors
and nvl(dbms_lob.getlength(xrpv.sql_text),0)=0;             -- but SQL_TEXT is blank -> broken
-- returns rows => a parameter is broken (re-import partial merge); delete + fresh import to fix

Description imported with line breaks

select dbms_lob.getlength(xrt.description) desc_len, regexp_count(xrt.description,chr(10)) newlines
from xxen_reports_tl xrt, xxen_reports_v xrv
where xrt.report_id=xrv.report_id and xrt.language='US' and xrv.report_name='<Report Name>';

Dependencies formed after import

-- parameter-to-parameter deps (dependent "needs" parent)
select xrpv_dep.parameter_name dependent, xrpv_par.parameter_name needs_parent, xrpd.flex_bind
from xxen_report_param_dependencies xrpd, xxen_report_parameters_v xrpv_dep, xxen_report_parameters_v xrpv_par
where xrpd.report_id=xxen_report.report_id('<Report Name>')
and xrpd.dependent_parameter_id=xrpv_dep.parameter_id and xrpd.parameter_id=xrpv_par.parameter_id;
-- upload column-to-parameter deps (xxen_upload_dependencies: report_parameter_id, column_name, dependent_column_name, flex_bind, dependency_type)
select xud.dependent_column_name, xud.dependency_type, xud.flex_bind, xrpv.parameter_name
from xxen_upload_dependencies xud, xxen_report_parameters_v xrpv
where xud.report_id=xxen_report.report_id('<Upload Name>') and xud.report_parameter_id=xrpv.parameter_id(+);

Uploaded file corrupted in transit (NUL byte)

select fl.file_id, fl.file_name, dbms_lob.getlength(fl.file_data) bytes,
dbms_lob.instr(fl.file_data,hextoraw('00')) first_nul_offset
from fnd_lobs fl where fl.file_name like '<name>%' order by fl.upload_date desc;

Workbook-backed template check

select xrt.template_name, xrt.dynamic_columns,
(select count(*) from xxen_report_template_files f where f.template_id=xrt.template_id) xl_parts,
(select count(*) from xxen_report_template_files f where f.template_id=xrt.template_id and f.file_name like '%pivot%') pivot_parts
from xxen_report_templates xrt where xrt.report_id=<id> order by xrt.template_name;

Matching-value parameter rows

<!-- Primary row: Active -->
<PARAMETERS_ROW>
 <SORT_ORDER>5</SORT_ORDER>
 <DISPLAY_SEQUENCE>50</DISPLAY_SEQUENCE>
 <ANCHOR>1=1</ANCHOR>
 <SQL_TEXT>nvl(hla.inactive_date,trunc(sysdate))&gt;=trunc(sysdate)</SQL_TEXT>
 <PARAMETER_TYPE_DSP>LOV</PARAMETER_TYPE_DSP>
 <LOV_NAME>AR Registry Status</LOV_NAME>
 <LOV_GUID>22FFB94618DB3DA4E0630100007FD7CF</LOV_GUID>
 <VALIDATE_FROM_LIST_DSP>Y</VALIDATE_FROM_LIST_DSP>
 <MATCHING_VALUE>A</MATCHING_VALUE>
 <PARAMETER_TRANSLATIONS>
  <PARAMETER_TRANSLATIONS_ROW>
   <LANGUAGE>US</LANGUAGE>
   <PARAMETER_NAME>Location Status</PARAMETER_NAME>
  </PARAMETER_TRANSLATIONS_ROW>
 </PARAMETER_TRANSLATIONS>
</PARAMETERS_ROW>
<!-- Sub-row: Inactive (no DISPLAY_SEQUENCE) -->
<PARAMETERS_ROW>
 <SORT_ORDER>14</SORT_ORDER>
 <ANCHOR>1=1</ANCHOR>
 <SQL_TEXT>hla.inactive_date&lt;trunc(sysdate)</SQL_TEXT>
 <MATCHING_VALUE>I</MATCHING_VALUE>
 <PARAMETER_TRANSLATIONS>
  <PARAMETER_TRANSLATIONS_ROW>
   <LANGUAGE>US</LANGUAGE>
   <PARAMETER_NAME>Location Status</PARAMETER_NAME>
  </PARAMETER_TRANSLATIONS_ROW>
 </PARAMETER_TRANSLATIONS>
</PARAMETERS_ROW>

Assignments block

<ROOT>
 <CATEGORIES>...</CATEGORIES>
 <REPORTS>
  <REPORTS_ROW>...</REPORTS_ROW>
 </REPORTS>
 <ASSIGNMENTS>
  <ASSIGNMENTS_ROW>
   <REPORT_NAME>My Report</REPORT_NAME>        <!-- REQUIRED: links the row to the report -->
   <INCLUDE_EXCLUDE>I</INCLUDE_EXCLUDE>
   <ASSIGNMENT_LEVEL>Request Group</ASSIGNMENT_LEVEL>  <!-- full words: Application / Request Group / Responsibility / User / Form / Function -->
   <LEVEL_VALUE>All Reports</LEVEL_VALUE>             <!-- the request group / application / responsibility / form / function name -->
   <APPLICATION_SHORT_NAME>SQLAP</APPLICATION_SHORT_NAME> <!-- REQUIRED for Request Group / Form: disambiguates names that repeat across apps (e.g. 'All Reports' exists under SQLAP, PO, CE...). NOT used for Application or Function. -->
   <FORM_PARAMETERS></FORM_PARAMETERS>             <!-- omit <DISABLED> entirely for an active row -->
  </ASSIGNMENTS_ROW>
  <ASSIGNMENTS_ROW>                                 <!-- a Function-level row (uploads): no APPLICATION_SHORT_NAME, function_name in LEVEL_VALUE -->
   <REPORT_NAME>AP Invoice Upload</REPORT_NAME>
   <INCLUDE_EXCLUDE>I</INCLUDE_EXCLUDE>
   <ASSIGNMENT_LEVEL>Function</ASSIGNMENT_LEVEL>
   <LEVEL_VALUE>AP_APXINWKB</LEVEL_VALUE>            <!-- the internal function_name (NOT user_function_name) -->
   <FORM_PARAMETERS></FORM_PARAMETERS>
  </ASSIGNMENTS_ROW>
 </ASSIGNMENTS>
</ROOT>

Grey-out until a parent is set

-- greys out until "Include accounts with open invoices" = Yes, then offers 1..999
select to_char(d.n) id, to_char(d.n) value, null description
from (select level n from dual connect by level<=999) d
where :$flex$.Include_accounts_with_open_invoices='Y'
order by d.n

KFF segment LOV

select
substrb(l.column_value,1,instrb(l.column_value,'~',1,1)-1) id,
substrb(l.column_value,instrb(l.column_value,'~',1,1)+1,instrb(l.column_value,'~',1,2)-instrb(l.column_value,'~',1,1)-1) value,
substrb(l.column_value,instrb(l.column_value,'~',1,2)+1) description
from
(select gl.chart_of_accounts_id id_flex_num from gl_ledgers gl where gl.name=:$column$.ledger_name) s,
table(xxen_util.kff_segment_lov(
p_application_id=>101,
p_id_flex_code=>'GL#',
p_id_flex_num=>s.id_flex_num,
p_application_column_name=>'SEGMENT6',
p_segment1=>:$column$.gl_segment1, ... p_segment15=>:$column$.gl_segment15)) l
order by
2

kff_segment_lov returns its rows unsorted and removes repeated values itself (dependent and table validated value sets), so the LOV sorts and needs no distinct.