Report Development Rules Copy link
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.
Claude Code
- Add the Enginatics marketplace in Claude Code.
/plugin marketplace add enginatics/ai-skills - Install the plugin.
/plugin install blitz-report-ai-skills@enginatics - Run the environment setup once; it describes your EBS development instance (never production).
/blitz-report-ai-skills:blitz-environment-setup - Nothing to run: Claude loads these rules automatically when a task needs them.
- 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(scpto/tmpfirst). Never sqlplus spool for export (pads CLOBs). Never direct DML onxxen_*content tables. import_xmlderives parameter and upload dependencies from:$flex$./:$column$.references; do not putPARAMETER_DEPENDENCIES/UPLOAD_DEPENDENCIESin the XML. Verify each intended dependency formed (xxen_report_param_dependencies/xxen_upload_dependencies) — a token matching no parameter/column NAME silently binds NULL. Fortype='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 (seeebs-11i-compatibilityskill).$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
OPERATIONSwith 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_xmltake a.zip:.xmlentries are imported in zip order and the first failing entry stops the import (named in the error, earlier entries stay imported);workbook_<doc_id>.eexentries go to the Discoverer staging table and.rdlentries to SSRS. FNDGFU storesapplication/x-compressfor any.zipandtext/xml; charset=UTF-8for an.xmlwhatever itsCONTENT_TYPEargument, soimport_xml_filerecognises a zip by name, while the form'svalidate_xmlgates on the browser's type (application/%zip%,text/xml): a test that loads files with FNDGFU must set the browser's content type before callingvalidate_xml. Make files sent to customers 7-bit ASCII (replace en dashes);LPX-00217 invalid character 0on import = a NUL byte injected in transit, not an encoding/charset issue. xxen_report.sql_to_xmlwith a multi-row cursor needs a non-null rowset tag.
XML structure
GUIDrequired.'in SQL. Report name inREPORT_TRANSLATIONS_ROW, parameter names inPARAMETER_TRANSLATIONS_ROW.<DESCRIPTION>goes insideREPORT_TRANSLATIONS_ROW(elsewhere it is silently ignored), broken into short flush-left paragraphs with blank lines, max 4000 bytes incl. CRLF (ORA-01706on 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_hlogs 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 whensql_textchanged; to fix a comment on an already-imported version, updatexxen_reports_h.commentsof the latest row directly (the one sanctioned exception).SORT_ORDERis the parameter upsert key. Never renumber existing rows; add new parameters atmax(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: no1=1/&lexicalanchor parameter with blankSQL_TEXT.- Copying a
TEMPLATES_ROWbetween reports needs a fresh GUID (templates are matched by GUID globally; reuse overwrites the source's template). RemapDEFAULT_TEMPLATES_ROW/TEMPLATE_GUIDtoo. - A template without a
TEMPLATE_SHARINGrow 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_reportexports only the session language and omits<APPS_INITIALIZE_DATA>and<ASSIGNMENTS>. Setnls_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_bynot -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_TEXTis the bare predicate (never prefixed withand). 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).&lexicalanchors:&in XML.build_sqlreplaces every occurrence of a lexical anchor, and concatenates theSQL_TEXTs of several parameters on the same lexical anchor with no separator (a1=1anchor getsandappended, a lexical nothing), so give each parameter its own lexical anchor. A lexical anchor may live inside another lexical parameter'sSQL_TEXT— liveness is transitive; never judge "dead anchor" from the main SQL alone, and match on word boundaries.:bindanchors bind the value directly, noSQL_TEXT. A self-referencing no-op anchor takesnvl(:b,'Y')=nvl(:b,'Y')only when the parameter is optional.- Multi-anchor parameters: same name, sub-rows without
DISPLAY_SEQUENCE; sub-rows never carryDEFAULT_VALUE, LOV settings orREQUIRED. Hidden parameter = negativeDISPLAY_SEQUENCE(not omitted). - All anchors used must be declared in
<ANCHORS>, WHERE anchors included: the import fillsxxen_report_parameter_anchorsfrom that section andbuild_sqlinjects a parameter'sSQL_TEXTonly for a registered anchor. A newn=nanchor on an existing report whose<ANCHORS>lacks it imports cleanly, the run executes the literaln=nand the filter is silently ignored; the executed SQL in the output's Parameters sheet is the proof. - A filter
SQL_TEXTnaming 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, asORA-00904: "X"."<COLUMN>": invalid identifierwithORA-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’sSQL_TEXTcolumn against the columns actually produced, and test a run WITH each filter populated, not just the unfiltered one. - On a
DB_PACKAGEreport, a selection parameter the package already filters on STILL keeps its1=1anchor andcol=:bindSQL_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:bindanchor silently drops the run screen’s multiple-values option, since the engine offers that only for ann=nanchor with a straightforwardcol=:bind(verify withxxen_api.multiple_values_allowed).QP Customer Pricing Engine Requestkeepsx.pricing_item_code=:p_itemandx.pricing_category_segments=:p_categoryfor exactly this reason, even thoughXXEN_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 ascategory_segmentsfor display beside the generated Family/Class columns, and the requested item’s aspricing_category_segmentsfor 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 losslessexistssemi-join on the inner1=1for pruning. - A
DB_PACKAGEreport 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), sobeforereportmust copy the globals into private body variables and null the public ones immediately; nulling them inafterreportis 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 throughxxen_xdo.value) stays public, is set at the start of everybeforereport, and needs a hiddenNumberparameter 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 maskDD.MM.RRRRan add-in run binds process id 10075232 as 10-JUL-5232,ORA-00932). - The engine's package binds and
afterreportcall take theDB_PACKAGElast set byxxen_xdo.db_package(<report id>): by building a report's SQL, and byxxen_api.db_package(<report id>)on a result-cache miss. Abeforereportthat parses other reports switches it to theirs, so it must end withxxen_xdo.clear_db_package(asXXEN_REPORT_COLSdoes), which also skips its own package binds andafterreportfor the rest of that run. - The engine discards a
DB_PACKAGEtrigger's return value (xxen_xdo.execute_triggerassigns it to an unused local), soreturn falsedoes 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, callfnd_concurrent.set_completion_status('WARNING',<message>)and return from the trigger; the status is written autonomously, so arollbackinafterreportdoes 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 fromxxen_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, defaultxxen_util.jai_installed) + one&il_*lexical sub-row per fragment.jai_party_regsrows serve suppliers and customers (supplier_flag/customer_flagrequired in every join;party_site_id=vendor_site_idorcust_acct_site_id). Parse both branches: on a JAI instance and a non-JAI one.
LOV binding
- A LOV with an
idcolumn passes the id toSQL_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 isn=n, it is not Date/matching-value, andSQL_TEXTis a straightforwardcol=:bind(also inside compound/OR/in (select...)shapes). Verify withxxen_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) rnand correlate on the key outside it. The two-level form (an ordered inline view withrownuminside aninlist 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, orcol=f(:bind); column on the left;1=1/lexical anchors only. Neverxxen_util.containsinSQL_TEXT. Lookup-code parameters:col=to_number(xxen_util.lookup_code(:p,'TYPE',app)), never wrap the column inmeaning(). - Bind naming: no
p_prefix;_idsuffix 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 withORA-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 byxxen_report_ebs.validate_parameter_values, so a scheduled run of such a template silently runs without that filter.validate_from_list_dspisnvl(<LOV's flag>,<parameter's flag>): a shared LOV'sYcannot 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_typeChar,lov_idnull). Detect a dropped default by the empty value inxxen_report_run_param_vals_tmpafterinitialize_parameter_values. - A parameter's
SQL_TEXTmay reference ANOTHER parameter's bind (e.g.:period_name_frominside 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 inMSC Pegging Hierarchy) is a hidden parameter with a:bindanchor used directly in each block, not onen=nsub-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=nanchor into an inline view, check every bind against the parameters' bind names (a rate view binding:p_conversion_date1while the parameter binds:p_curr_conv_date1finds no rate and converts at 1). xxen_api.set_parameter_value_stores raw values (no LOV resolution): pass ids for id LOVs andDD-MON-YYYYdates; to test a multiple-values run, prefix the delimited list with the engine’s marker (<multiple_values>A;B) — a bareA;Bis stored as one literal value and silently matches nothing; it does not evaluate hidden defaults — feed them by evaluating each hiddenDEFAULT_VALUEwith:$flex$substituted (filterdisplay_sequence<0).- Every visible parameter needs a
PARAMETER_TYPE_DSP(a null type disables the multi-valuelikeexpansion).
Matching values, defaults, dependencies
- Different WHERE logic per LOV value → one
PARAMETERS_ROWperMATCHING_VALUE(primary withDISPLAY_SEQUENCE, sub-rows without), same anchor;MATCHING_VALUEis the LOV id. Neverdecode/casein oneSQL_TEXT. - From/To pairs: To defaults from From (
:$flex$.from_param; dateslast_day(:$flex$.date_from)). - A template parameter default with
LOCKED=Y(xxen_report_param_default_vals.locked) is fixed for template users (XXEN_REPORT_ACCESSD/S exempt):initialize_parameter_valuesgreys it (required='N', the same sentinel as an empty dependent LOV),xxen_report.apply_locked_defaultsforces 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_VALUEis evaluated single-row (execute immediate ... into): 0 or 2+ rows → no default; useselect distinctwhen a value can repeat; nocount(...)=1guard. 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 Oracleparameter takes filter before display from the value set's Long List flag (fnd_flex_value_sets.longlist_flag,XXEN_REPORT_PARAMETERS_V); the import dropsFILTER_BEFORE_DISPLAY_DSPfor it. A value set listing a large master table (all suppliers) is therefore aLOV customwith the value set's id/value columns andFILTER_BEFORE_DISPLAY_DSP=Y, or the add-in downloads the whole list on every open. LOV Oraclevalue sets:PARAMETER_TYPE_DSP=LOV Oracle+LOV_NAME. Yes/No parameters use theYes_Novalue set (matching-value pattern for Y/null flags:Y→col='Y',Nsub-row →col is null); the shared single-valueYesLOV 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=Yby default;FILTER_BEFORE_DISPLAYonly for 100k+ LOVs (Item LOVs always). ExportedVALIDATE_FROM_LIST_DSP/LOV_QUERY_DSPnext to a shared LOV are the fallback payload for a target lacking it — never strip them; only a literalNis invalid. - Custom LOV over a security-scoped master table: take the shipped LOV's
whereclause verbatim, change only the select list. Standard LOVs: HR Operating Unit8E2FF36EDEB979D2E0530100007F1FF2, FND User Name8E2FF36EDE8479D2E0530100007F1FF2, FND Responsibility Name8E2FF36EDE9E79D2E0530100007F1FF2, GL Ledger8E2FF36EDEB879D2E0530100007F1FF2, Yes8E2FF36EDEA679D2E0530100007F1FF2(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 bothidandvaluereturning 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_ROWname 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; runxxen_report.update_parameter_dependencies(report_id)there after import.
Access
<ASSIGNMENTS>is a top-level sibling of<REPORTS>(nested = silently ignored), rows keyed byREPORT_NAME. Levels are full words (Site,Application,Request Group,Responsibility,User,Form,Function,Report);APPLICATION_SHORT_NAMErequired for Request Group/Form; omit<DISABLED>for active rows. Not round-tripped byexport_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_TEXTbindshaouv.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_secis the APPS synonym offa.fa_book_controlsunder policyFA_POLICY(fa_security.build_predicate), which addsorg_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 noFA_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 tofa_book_controlswithfnd_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(...), nevergl_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 applyflex_ccid_accessibleonly through the hiddenGL Flex Value Securityparameter (&gl_flex_value_securitylexical, defaultx,SQL_TEXTreturning the predicatefrom dual where xxen_util.has_flex_value_security='Y'), never inline: an inline call, even OR-gated onhas_flex_value_security, runs per row for responsibilities without rules (cache-based, needs init, Discoverer-only on 11i). Acceptance:Blitz Report Text Searchforgl_security_pkgreturns 0 rows. - KFF segment LOVs:
xxen_util.kff_segment_lov(...)(returnsid~value~description), neverfnd_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 checkcontent/lovs.xmlfor an existing predefined Blitz Report LOV or Oracle standard LOV that matches. UsePARAMETER_TYPE_DSP>LOV+LOV_NAME+LOV_GUIDto reference it. Only createLOV customwhen 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_lovsfor 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
idcolumn, theDEFAULT_VALUEcan 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 correspondingvaluecolumn 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_VALUEon its own — never wrap it inselect … from dual.xxen_report.default_value_queryturnsfnd_profile.value('ORG_ID'),fnd_global.*,xxen_util.*,sys_context(...),:$flex$.*expressions,sysdatearithmetic on a Date parameter, acase … endand calls of the standard SQL functions (is_function) intoselect <expression> from dualitself; 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 ... endto 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_VALUEfor 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.
- You cannot write
- A
DEFAULT_VALUEthat is a SELECT query defaults ONLY when it returns exactly ONE row — nocount(...)=1guard needed; useselect distinctwhen a value can legitimately repeat. The framework evaluates a query-form default viaexecute 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(...) …)=1single-occurrence guard — it is redundant with theexecute … intosingle-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 distinctso those duplicates collapse to one row and the default still applies. When the multiple rows are genuinely different values (an ambiguous key),distinctwon'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).
- Never wrap the query in a
- 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 Codefiltering 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 theDEFAULT_VALUEunset —REQUIRED=Ywill surface the choice to the user per row. - Hidden parameters: Use a negative
DISPLAY_SEQUENCE(e.g.-10). Do NOT omitDISPLAY_SEQUENCE— that makes it a sub-row, not a hidden parameter. - SQL_TEXT required for lexical and WHERE anchors: Parameters using
1=1/2=2or&lexicalanchors must haveSQL_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 needSQL_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_queryare stored null and the shared LOV governs. That is enforced by the import itself, not by convention:import_xml_clobderiveslov_idascoalesce(match by LOV_GUID, match by LOV_NAME)(value set by name forLOV Oracle) and then storesnvl2(lov_id,null,<..._dsp>)— same expression for upload columns and parameters. Verified:validate_from_listis 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 instancelov_idresolves to null, thenvl2stores 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 theY-or-null storage convention (see rule #41 incoding-standardsskill). Shipped content currently holds these two elements 9,982 times, alwaysY. - 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 incontent/lovs.xml, or switch toLOV 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 aLOV_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:
- the parameter is
VALIDATE_FROM_LIST='Y'(this is the gating condition —set_single_lov_valueis only called whenvalidate_from_list='Y' and filter_before_display is null), AND - it is flex-dependent (its
LOV_QUERY_DSPreferences:$flex$.Parent) — so the LOV result changes with the parent's value, AND - its LOV currently returns zero rows (
no_data_found), AND - it is not a required parameter (
requiredis 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.nThe 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_variableThe 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=:bindworks;:bind=columndoes NOT - Only works with
1=1or&lexicalanchors, not:bindanchors - Do NOT use
xxen_util.containsin SQL_TEXT — that pattern has the column wrapped in a function on the right side, which defeats auto-detection xxen_util.containsis for LOV_QUERY_DSP dependent filtering only (:$flex$.paramsyntax)
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_ratesholds 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_columnsaliases 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_absallocation-basis column is the near-miss. A null adjustment source renders blank, which is faithful. DYNAMIC_COLUMNS=Yis for raw templates only, never a workbook-backed template (checkxxen_report_template_files); a template's grain comes from its workbook, not its column list.- A template controls grain:
Distinctand per-columnAggregationwrap the SQL (group byevery un-aggregated displayed column); columns absent fromTEMPLATE_COLUMNSare hidden.MAXon 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. WithDYNAMIC_COLUMNS=Yan unlisted column (category or&lexicalcolumns) 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_entitiesjoins:nvl(xte.source_id_int_1,-99)=idplusledger_idandentity_code(matches the function-based index). Reference it asxla.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 ismax(created_by) keep (dense_rank first order by creation_date).- Derived totals reconcile to their components with the same predicate (AR Aging
Past Due Amountuses the aging-basis decode). - A
row_number()=1gate 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 oneWINDOW 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'sar_distributionsrow: receipt UNAPP lines are often linked to the cash receipt history distributions instead, so that test drops accounted receipts. Transactions use theRECEIVABLEline linked to the RECra_cust_trx_line_gl_distrow (AR AgingExclude Unaccountedmirrors Oracle's By Account report this way). - A chargeback payment schedule must not be joined to
ar_adjustmentsonchargeback_customer_trx_id: the creating adjustment and itsREVERSE_ADJUSTMENTboth carry it, so the join returns two rows per chargeback. Read the originating transaction through a scalar subquery withdistinct(AR Aging's salesperson of a chargeback). xla_flex_pkg.get_flex_value_meaningraises for an unknown constant — usenvl((select max(flex_value_meaning) from fnd_flex_values_vl ...),constant).no_mergeon 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 aFILTERabove theCOLLECTION ITERATOR. Put/*+ no_merge*/on the inline view's select;no_unnestthere has no effect, since it only acts inside the subquery it is written in. - For an empty
VALIDATE_FROM_LIST=Yparameter without filter-before-display that is required or$flex$-dependent, the engine runs the LOV query withwhere rownum<3on every run-screen open and again when a parent changes; value validation runs it withrownum=1. Both use the text without its trailingorder by, whichxxen_util.remove_order_byfinds as the FIRSTorder byin the query: an earlier one (an analytic orkeep (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 distinctorgroup byabove it fetches the whole value set first, including any per-value security calls. Keep such LOVs free of blocking operators: in akff_segment_lovLOV over several charts of accounts, deduplicate the driving structures instead, one per value set (group by fifs.flex_value_set_idin theno_mergeview, preferring a segment without Security Enabled withnvl(min(decode(fifs.security_enabled_flag,'N',fifs.id_flex_num)),min(fifs.id_flex_num))), as theGL SegmentLOVs do. - A parameter fragment's
not existssubquery 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_idhas 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 distinctbefore 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_cursoron theXXEN_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 withleading(<driving alias>)in that query block, and addno_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_voverxxen_upload_columns_v), the inner view is built for all rows and filtered afterwards (plan:VIEW PUSHED PREDICATEabove a fullVIEWwith a filter on the join key). Putpush_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 byof the view (ONT Orders and Lines: the order line view partitions byheader_id). A parameter that reaches such a view only through another key (invoice lines joined online_id) leaves the optimizer two plans, both over every line: build the whole view, or cartesian-join all headers of the operating unit to pushheader_id. Add a lossless<header alias>.header_id in (select ...)semi-join to that parameter'sSQL_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 comparebuffer_getsfor both shapes. - Do not join a lookup table at the outer level of a report built on a multi-branch
union allview (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 comparev$sqlbuffer gets of the concurrent run before and after. - A
connect bywhose source is a join, or an inline view joined to another table, builds that join over the whole source before the hierarchy, and underCONNECT BY WITH FILTERINGonce per level: restrict the source inside the inline view (plan and instance formsc_full_pegging), not only instart with. Never walk each row's subtree in a correlatedconnect byscalar subquery (rows × subtree size); enumerate all subtrees once withconnect_by_rootandgroup bythe root, asMSC Pegging Hierarchydoes for its QOH fulfillment status. - Run every
n=nparameter of aconnect byreport 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 withORA-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. Instart withon a column computed by a correlated scalar subquery it returns roots that do not match (null values pass thein): produce that column through an outer-joined inline view instead. oe_price_list_linesis a synonym forqp_price_list_lines_v, whoseinventory_item_idis 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',operandis the list price) toqp_pricing_attributesonlist_line_idwithproduct_attribute_context='ITEM',product_attribute='PRICING_ATTRIBUTE1'andproduct_attr_value=to_char(<item id>), plusmtl_system_items_bfororganization_id=qp_util.get_item_validation_org, which the view applies too.- Every lookup view outer-joined per line runs as
VIEW PUSHED PREDICATEwith its ownSORT GROUP BYper 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 aVW_SSQpushed view and the per-value cache is lost — and the key column, not the looked-up value, goes into thegroup 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 withORA-03113, so wrap a parameter-driven one incase when :p is not null then ... end. ItsDESCRIPTIONmode 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 withxxen_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, socoalescethe 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. Acount(*)test hides the cost because the optimizer drops the unreferenced calls.- Item cost "last update" =
max(cicd.last_update_date)fromcst_item_cost_details, not the header. org_access_viewis every inventory organization times everyfnd_responsibilityrow (end-dated included), each pair tested with anorg_accessprobe, so an unrestricted query costs organizations × responsibilities (68 million buffer gets on 38,000 responsibilities). Every responsibility sees the organizations that have noorg_accessrows, so evaluate the view once for those (any responsibility, minus organizations present inorg_access) and per responsibility only for responsibilities present inorg_access, asFND Responsibilitiesdoes. 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_filesjoined todba_tablespacesanddba_free_space,dba_userswithdba_users_with_defpwdanddba_ts_quotas, andgv$undostatwithgv$tablespaceandgv$parametereach 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 groupedno_mergeinline views instead of correlated scalar subqueries, and time the whole statement, never the views one by one. Ano_mergeon the wrong view makes it worse (ondba_tablespaces: 0.6 s to 12 s). v$resource_limitreturns no rows inside a pluggable database, and EBS on 19c always runs in one. A report on it needs a fallback for that case: countgv$process/gv$sessionagainst theprocesses/sessionsvalues ingv$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 asMax 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_ROWthat carries theCOLUMN_NAMEand NODISPLAY_SEQUENCE. Thehiddencolumn onxxen_report_template_columnsdoes 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 hasdynamic_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_vfilters 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 tablexxen_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 throughdynamic_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 setcompact="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_PIVOTcannot put subtotals on SOME levels:PIVOT_LAYOUT='T'writesdefaultSubtotal="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_TYPEisFILTER,ROW,COLUMNorVALUE-PAGEcounts towardspageFieldsbut produces neither anaxisnor apageFieldchild, so the pivot loads empty. import_xmlDELETES a template's workbook when the importedTEMPLATES_ROWcarries no<FILE_DATA>(xxen_report_template_filesgoes 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 overxxen_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 seededENGINATICS/SYSTEM_ADMINISTRATOR/SYSADMINmarker. 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 withRPC_E_CALL_REJECTEDwhile 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,AutoFitso the sheet gets its<cols>block, a defined nameSourceData=OFFSET('<data sheet>'!$A$1,0,0,COUNTA('<data sheet>'!$A:$A),COUNTA('<data sheet>'!$1:$1)), the pivot built on that name withRefreshOnFileOpenset, and aParameterssheet last. Register withxxen_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:
xlDoNotRepeatLabelsis 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.Positiondoes not reorder page fields, andHasAutoFormat=Falsealso 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 setShowDrillIndicators=False(the in-cell +/- buttons) andWorkbook.ShowPivotTableFieldList=False.DisplayFieldCaptionscannot 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 noxl/calcChain.xml(strip it and its declarations), or the generated file is malformed. Diagnose withET.fromstringper 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 asxxen_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.xmlnumbering follows neither tab order nor creation order; resolve a sheet by name throughworkbook.xmland its rels. - Adding a column to a workbook-backed template shifts five positional places: the data sheet
<cols>,cacheFieldorder (each carrying its ownnumFmtId) andfieldGroup/@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 becauseimport_xmlloads it verbatim and onlyupload_excel_templatere-derives it from the workbook. Verify by name resolution — every@x/@fld/@baseFieldmust resolve to the same column name before and after and everynumFmtIdmust be unchanged. A date rendering as a raw serial after an insert is itsnumFmtIdleft on the wrongcacheField. <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 parsedxxen_report_sql_columnsdata types, excluding numerics that are identifiers or rates. A dynamicSourceDataname (OFFSET(..,COUNTA(..),COUNTA(..))) sizes itself to new columns and beats a literalref, 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_refreshsetsrefreshOnLoadontype="100"connections whenxl/model/item.dataexists (never ontype="5"); aWorksheetConnection_…PQSOURCE model table cannot refresh — rebuild the pivot on the query-fed table; aworksheetSource 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); setHasAutoFormat=Falseon 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
usableall check out, has a leading/trailing blank intemplate_name. Forms trims the value when it is returned into the item;populate_templateandtemplate_idthen matchtemplate_namewith a plain=, find nothing, andquery_templateskips the query. The importer trims (xxen_xdotrim(substrb(doc_name…))) butimport_xmlstoresTEMPLATE_NAMEverbatim, so the padding arrives through a hand-built or tool-built XML, or a name typed with a trailing space in the form. Diagnose withtemplate_name<>trim(template_name); fix by trimming (the unique indexxxen_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 byis only emitted when at least one column carries an aggregation. - A column with no
TEMPLATE_COLUMNS_ROWat all is hidden, unless the template setsDYNAMIC_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 noDISPLAY_SEQUENCEis hidden either way. - Aggregated columns are auto-labelled in the output header, e.g.
Start Quantity (Sum). MAXon a VARCHAR2-typed numeric column is a STRING comparison ('9' > '10'). Check the parseddata_typebefore choosing MAX — see coding-standards #65 for thedecode(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, andRXi: <name>only formats that table according to the attribute set picked on its run screen. Convert the extraction, never the formatter.fa_rx_reportsgives the interface table and the concurrent program; the extraction is the concurrent program's executable (FARX_C_<x>.<proc>delegating toFARX_<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 theafter_fetchtrigger instead, and a null interface column means it is fetched but never stored.fa_rx_util_pkg.From_Clause/Where_Clausehold the rest of the statement;build_selectadds nodistinctand noorder by. - Attribute sets are the templates:
fa_rx_attrsets_b/_tl(one per set) andfa_rx_rep_columns_b/_tl(display_status,attribute_counterfor the displayed order,ordering,break/break_group_level). Map displayed columns inattribute_counterorder toTEMPLATE_COLUMNS.DISPLAY_SEQUENCE, andASCENDINGtoSORT_ORDER. - Baseline an RXi report without the run screen: submit
RX-only: <name>to fill the interface table, then submit the RXi program withPUBLISH <the RX-only request id> <report id> <attribute set> HTMLas its first five arguments (FARXPBSHalso takesDIRECT <report id> <attribute set> (TEXT|HTML|CSV) [args]). Wrong arguments complete withInvalid Argumentsand 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_reportsjoined tofnd_concurrent_programs/fnd_executablesforapplication_id=140- 30 rows, each naming its extraction procedure and interface table, with the attribute set count fromfa_rx_attrsets_b. Three further RX-only/RXi pairs have nofa_rx_reportsrow of their own (RXFAACA/RXFAADA/RXFACCA, the Asset Cost / Accum Deprn / CIP Cost Balance "(Alternate)" programs): they are not extra reports,FA_RX_ALT_BALcalls the samefarx_bl.*_balances_rptroutines asFARX_C_BLand 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 incustom-reports-migrationskill (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_descriptionwalks 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 withORA-01006. - The FA balance reports (FASCOSTS, FASRSVES and their Blitz conversions on
XXEN_FA_FAS_XMLP) takefa_adjustments.source_type_codethroughfa_balrep_pkgunchanged, so a RESERVE or REVAL RESERVE row can carry sourceADJUSTMENT(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
caprevalformulafills 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_XMLPstages the balance rows intomsc_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) fillFA_BALANCES_REPORT_GTwith their own copy of the extraction, notfa_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-costBdeprn 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 nofa_adjustmentsrows, 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_valuenull), 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_hostonly 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, whichreport_hostwrites 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.progandXXEN_UPLOAD.progreport 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 allview 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_xplanwithADVANCED):USE_CONCATrepeated per branch withOR_PREDICATES(n)in the tens, alongsideOPT_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 (
0000tozzzz) 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 plaincol between :from and :to.- Correlated scalar subqueries carrying
and rownum=1are 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_SETSholds 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, withSR_INSTANCE_IDof -1 andSR_CATEGORY_SET_IDset to the same sequence value asCATEGORY_SET_ID(Oracle's own comment: "dummy value to satisfy the unique constraint"). Every later collection then UPDATEs that shared row, overwritingSR_INSTANCE_ID,DEFAULT_FLAG,DESCRIPTIONandCONTROL_LEVELwith the instance being collected.- So
msc_category_sets.sr_instance_idrecords 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_flagis 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 sharedcategory_set_id).msc_category_sets.sr_category_set_idis the dummy and always equalscategory_set_id; never join it tomtl_category_sets. msc_item_categories.category_set_idis the shared surrogate and itssr_instance_idis 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 bymic.sr_instance_id, which is a real instance column, and joinmic.category_set_id = mcs.category_set_idwith no instance predicate onmcs.category_set_idis globally unique (MSC_CATEGORY_SETS_U1), so a lookup keyed oncategory_set_namealone returns exactly one row (norownum), and a scalar subquery keyed oncategory_set_idcannot raiseORA-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_LOADinall_source), never against the development instance, a 12.2 instance or an ASCP Vision instance. - A
:$flex$reference reaches the SQL assys_context('XXEN_FLEX',...), which Oracle evaluates locally and never ships across a database link. A@A2M_DBLINKLOV or default that filters a remote table by it inside an unnestedexistsor join pulls the WHOLE remote table across the link (msc_item_categoriesfor 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 withexists (select /*+ no_unnest */ ...)correlated on its columns, or, where the remote rows are the result (Category LOV), join withleading(<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 withdbms_xplan.display(null,null,'BASIC REMOTE')over a loopback link; aREMOTEline without aWHEREis the defect. - MSC content reaches the planning server through
&a2m_dblinkwhen Blitz Report runs on the EBS source. Oracle then executes the query on the source and makes every per-row lookup andmsc_get_name&a2m_dblinkcall 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, asMSC Pegging Hierarchydoes. Every function goes through the link (xxen_util.meaning&a2m_dblinktoo; Blitz Report is installed there). No subquery inSTART 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 aconnect 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 withexplain planthrough a loopback link on the development instance (create database link ... usingits own service, dropped afterwards): the block must show as oneREMOTEoperation, and every parameter'sSQL_TEXTmust 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
nvlacross 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_termswithsite_use_id is null— the AR profile term at account level (Customer form Profile tab), what AR invoices on.hz_customer_profiles.standard_termswithsite_use_id is not null— the same at site-use level. The form allows a profile only onBILL_TO,DUNandSTMTSsite uses, but seeded and API-created data carries one on other purposes (Vision has 410 activeSHIP_TOones in OU 204 alone), so outer-join onsite_use_idand 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 Requestusescustomer_/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 Termpopulated on a Ship To row while every otherSite-prefixed profile column on that row is blank. Counting how many rows of eachSitecolumn 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 asoe_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 andPricing Payment Termdisagreeing withCustomer 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:
- In SELECT:
&dff_columnslexical placeholder - In GROUP BY (if aggregating):
&dff_columns_grplexical placeholder (must NOT be last — put a real column after it) DFF Displayparameter: shared LOVBlitz Report DFF Display(V/C/S options), defaultfnd_profile.value('XXEN_REPORT_DFF_DISPLAY'), anchor&dff_columns. Leaving the parameter blank hides DFF columns entirely (performance escape hatch).- 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 - Sub-row (no DISPLAY_SEQUENCE) for GROUP BY anchor: same parameter name, anchor
&dff_columns_grp, SQL_TEXT also passesp_display_mode=>:dff_displayand usesp_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
- 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.
- 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.
- 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.
- Import the concurrent program first — use
xxen_api.import_concurrent_programto 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 to1=1anchor style where multiple values support is desired. - API testing with ID parameters —
set_parameter_value_bypasses LOV resolution and passes raw display values. For LOVs withidcolumns (e.g.organization_id id, organization_code value), pass the numericidvalue (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_sqlwraps the authored LOV as an inline view and appendsparameter_idoutside it, so aUNIONLOV is safe on the web run screen.ORA-01789/JBO-27122from 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 ' 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 fixDescription 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))>=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<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.nKFF 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
2kff_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.