Blitz Report Development / Report Development
Report Development Rules
report-development
Blitz Report XML rules: parameters, LOVs, anchors, lexicals, templates, assignments, columns, the report engine.
When to use
Use whenever creating or editing a Blitz Report XML or its SQL.
Use
Part of blitz-report-ai-skills. Claude loads these rules automatically when a task needs them.
/plugin marketplace add enginatics/ai-skills
/plugin install blitz-report-ai-skills@enginaticsSkill contents
SKILL.md
# Report Development
## 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.
- A parameter default value or custom LOV may reference only a parameter, LOV or Oracle value set name of its own report, always with the leading colon (`:$flex$.x`); only shared and Oracle LOVs may name parameters a report lacks. The setup form refuses to save anything else (FSG reports excepted, whose function LOVs take binds from the add-in) and carries the references along when a parameter is renamed; `import_xml` does not check, so an XML with a dead token imports and the default or dependent list silently never fires.
- 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. `'` 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>`; the templates and `<ASSIGNMENTS>` come only with `p_include_templates=>'Y'` and `p_include_assignments=>'Y'`. 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.").
```xml
<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:
```sql
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, as plain text with no word boundary: `x.oh_row1=1` holds the anchor `1=1` and fails with `ORA-00904: "X"."OH_ROWWDJ"...`; use another anchor such as `3=3` then). Use a different anchor only for genuinely different SQL positions (separate UNION branches).
- `&lexical` anchors: `&` in XML. `build_sql` replaces every occurrence of a lexical anchor, and concatenates the `SQL_TEXT`s 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.
- Every `&word` of the SQL is registered as a lexical anchor, inside quoted aliases, string literals and comments included. When the SQL is built, a registered lexical that no parameter or upload SQL of the report owns is removed from the code only (a stale lexical) and kept as text everywhere else, so `"S&D Cost"`, `"&BC"`, `'R&D'` and `x."S&D"` come out as written; a lexical a parameter owns is replaced or removed wherever it occurs, quoted aliases included (`"&p_to_currency_code New Material Value"`), and a report with a `DB_PACKAGE` has every remaining lexical removed, since its package fills them. The setup form's SQL validation removes the lexicals of the code and of string literals before parsing and keeps those of quoted identifiers.
- `: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.
- A before report trigger (`DB_PACKAGE` `beforereport`/`afterpform`, a data template's `beforereport`/`afterparameterform`) that returns false stops the run: the request ends in Error with no output, as it does on any exception, which also gives the full stack. A null return continues, and the return value of `afterreport` is ignored because the output exists by then. A selection that merely finds no data returns true. 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 true 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. Id or Value = Value on the parameter (`<ID_OR_VALUE>value</ID_OR_VALUE>`) binds the display value instead, so one shared LOV serves both; such a parameter's run value (`xxen_api.set_parameter_value_`) is the display value, its template defaults stay the id, and its dependents still read the id through `:$flex$.<name>` and the display value through `:$flex$.<name>.value`.
- Adding an `id` column to a shared LOV changes what every parameter and upload column on it passes, customers' own included: set Id or Value = Value on the parameters whose SQL compares the bind with the display text, rewrite their `:$flex$.<name>` dependents to `.value`, set Value on upload parameters mapped to such a column without its own value-to-id query, and ship a one-time migration for customer content (`upgrade_post.sql` before the content import, `convert_lov_id_defaults` in `install.sh` after it).
- Saved user and template defaults of a parameter on an LOV with an id hold the id; a display value there fails the run screen with `Error validating value '<value>' ... ORA-01722: invalid number` against `x.id=:p_bind`. `import_xml` converts the saved defaults of a parameter that the import moves from an LOV without an id to one with an id (its own LOV changed, or the shared LOV gained an id), through the LOV query in the importing session, so a value that the importing responsibility's LOV does not return stays unconverted.
- 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 parameter that is not free text 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. To bind a value the list rejects (a schedule-only copy of a report), set Free Text on the parameter. 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 parameter owns at most one bind, assigned across the whole report by `xxen_report.bind_map`: `:bind` anchors first, then `1=1`-type rows, then lexicals, displayed before hidden, by display sequence, each taking the first bind of its own `SQL_TEXT` that no earlier parameter took; a bind no parameter owns is bound null. So a fragment that references another parameter's bind takes it over when that parameter sorts later or owns none, and the test reads the wrong value (the request log's `Parameter (:bind) = value` lines and `xxen_api.bind_map(<report id>)` show the assignment). To make one parameter's fragment depend on another parameter, give the controlling parameter a hidden sub-row that writes a lexical and test its text (`'&lot_mode' is not null`).
- A `:own_bind is not null` predicate in a parameter's own `SQL_TEXT` is always true, as the fragment is added only when the parameter has a value: remove it, unless it is what makes the parameter own the bind - another parameter's fragment reads that bind (GL Account Analysis: `Show Segments with Descriptions` owns `:p_show_seg_desc` for the contra account lexical), or the parameter's text holds another parameter's bind it would take over without it. Compare `bind_map` before and after the removal.
- 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).
- A custom LOV returns a `description` column next to `value` (`null description` when there is none): the run screen opens it as `select null id, x.value, x.description from (<lov>) x`, so a value-only list fails with `The LOV query is not valid. ORA-00904: "X"."DESCRIPTION"`. Test a list inside that wrapper; the query on its own runs fine.
### 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)`.
- A hidden parameter that resolves an id for a lexical from a `:$flex$` reference (the chart of accounts id driving `&chart_of_accounts` segment columns) is typed `Number` without an LOV, and derives it from a parameter the user sets (`fbc.book_type_code=:$flex$.book`), never from a session profile such as `MFG_ORGANIZATION_ID`, which an FA or GL user has not set. An LOV parameter validates its evaluated default against the LOV installed on the target instance, which can lack the `id` column the repo file carries, and a default that fails validation is dropped silently, so the lexical generates no columns.
- `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`). Validation belongs to the parameter or upload column, never to the LOV: it validates against its LOV unless `<FREE_TEXT>Y</FREE_TEXT>` sets Free Text. Filter before display is decided per instance by the concurrent program Blitz Report Update LOV Filter Before Display (more than 100,000 rows, or a count still running after 3 seconds), which the installer submits and which recounts only LOVs whose query changed; a seeded content load keeps an instance's setting. Exported `LOV_QUERY_DSP`/`FILTER_BEFORE_DISPLAY_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, Free Text), `X starts with`.
- Grey-out until a parent is set: a flex-dependent LOV without Free Text, 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.
- The import only adds template parameter exclusions: a `PARAMETER_EXCLUSION_ROW` left out of the file stays on an existing template, while its columns, pivot and defaults are replaced. To show a hidden parameter, remove it under Excluded Parameters in the Templates window, or delete the template (or the report) before importing. The exclusion insert matches by parameter name, so it hides every parameter carrying that name.
- An upgrade keeps what users changed on a shipped template's sharing (Restrict included) and parameter defaults (Locked included), but replaces its name, description, owner, columns, pivot and workbook and brings back a shipped sharing row or default a user deleted. Copy a shipped template to change its layout: the Templates window locks a shipped template's layout until Edit is ticked, and the Excel add-in and the web page show it read only. A template imported from a file carrying the ENGINATICS marker counts as shipped.
- 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).
- Secure each row once, by the entity that owns it, against the global temporary table `xxen_report_org_access`, never against `org_access_view`, `mo_glob_org_access_tmp` or the GL access set directly: inventory organization data `mp.organization_id in (select xroa.id from xxen_report_org_access xroa where xroa.access_type='INV')`, operating unit documents `<document>.org_id in (... xroa.access_type='OU')`, ledger data `gl.ledger_id in (... xroa.access_type='LEDGER')`, FA books `book_type_code in (select xroa.code ... xroa.access_type='FA_BOOK')`. `xxen_report_ebs.mo_global_init` fills it per run, for every responsibility of the user with profile Blitz Report Organization Access All Responsibilities, and for an upload always from the current responsibility.
- A row with an inventory organization (items, costs, on-hand, material, WIP and receiving transactions, PO shipments to an organization) carries only the INV filter, no operating unit or ledger filter on that organization: profiles Blitz Report Use Operating Unit Security and Blitz Report Use Ledger Security (responsibility level, reports only) restrict the INV set to the organizations whose operating unit or ledger the responsibility can access, so a report filtering by all three returns nothing where a customer's operating unit or GL setup does not match the inventory access. The operating unit and ledger parameters of such a report use the shared lists `INV Operating Unit` and `GL Ledger (inv org related)`, which list those of the accessible organizations.
- Decision: setup and configuration reports (account setups, overheads, resources, shipping networks, intercompany relationships, period status, interface and process monitoring) carry no organization security and use the unrestricted lists (`INV Organization Code (all)`, `HR Operating Unit (all)`, `GL Ledger (all)`, `GL Chart of Accounts (inv org related)`): IT and analysts need the global view, and a responsibility's organization access often covers only its own organizations, which hides the other side of an inter-organization setup. A list switched in for another must pass the same kind of value: `GL Chart of Accounts (all)` returns the structure id, so a chart of accounts parameter on it makes the segment column generators find no structure.
- MOAC: pick ONE — (1) the OU filter above + optional OU parameter (preferred), (2) required OU parameter with restricted LOV, (3) Ledger/OU advanced-required. `SQL_TEXT` binds `haouv.name=:operating_unit`. The organization filter goes into 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.
- Purchasing document security (Setup > Purchasing > Document Types > Security Level): `xxen_util.po_document_security(<document type>,<alias>)` returns the predicate the Purchasing summary forms apply (`po_document_checks_pvt.po_security_check`) for `REQUISITION` (`preparer_id`, `type_lookup_code`), `PO` (`agent_id`, `type_lookup_code`; covers the PO and PA document types, a header of another type such as an RFQ passes) and `RELEASE` (`agent_id`, `release_type`); `xxen_util.has_po_document_security(<document type>)` is `Y` when a document type of that kind is not Public somewhere and profile `XXEN_REPORT_DISABLE_PO_DOCUMENT_SECURITY` is not `Y`. Apply it only through a hidden `PO Document Security` parameter (own lexical anchor, default `x`, `SQL_TEXT` `select xxen_util.po_document_security(...)||' and' from dual where xxen_util.has_po_document_security(...)='Y'`), on the line after the `where` of the query block whose driving table is the document; a block with the header and an outer-joined release gets `'('||xxen_util.po_document_security('PO','pha')||' and pra.po_release_id is null or '||xxen_util.po_document_security('RELEASE','pra')||') and'`, a separate release branch its own parameter on `RELEASE`. Where that release is joined through the shipment (`plla.po_release_id=pra.po_release_id(+)`) and the block joins many more tables, key the release part on the shipment instead: `'(plla.po_release_id is null and '||xxen_util.po_document_security('PO','pha')||' or plla.po_release_id is not null and exists (select null from po_releases_all pra2 where plla.po_release_id=pra2.po_release_id and '||xxen_util.po_document_security('RELEASE','pra2')||')) and'`; a predicate naming the outer-joined release alias makes the optimizer join the release early and run the later lookups per line instead of per header (`PO Headers and Lines`), with identical rows, so compare the buffer gets with the document security active. Never put it on an outer-joined lookup of a document (the originating requisition of a PO shipment): a predicate on an outer-joined alias drops the driving row. Other documents on the same row follow the row. Requisitions have no action-history leg, as in the forms (the decode of the object type has no requisition entry); the access level is ignored, the forms apply it to modifying only.
- 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.
- `kff_segment_lov` also returns a value set's parent values, to which nothing is posted, so behind a plain `<segment>=:bind` filter they return no rows. Where such a filter is a report's main selection, carry `flex_value_set_id` out of the driving view and exclude them: `x.value not in (select /*+ no_unnest*/ ffv.flex_value from fnd_flex_values ffv where x.flex_value_set_id=ffv.flex_value_set_id and ffv.summary_flag='Y')`.
## 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.
- **Free Text**: unchecked (no `FREE_TEXT` element), so the parameter validates against its LOV; check it only for a parameter taking wildcards (`X like`).
- **Filter Before Display**: leave it unchecked in content; each instance sets it from the LOV's row count (Blitz Report Update LOV 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_api.default_value_exec` turns `fnd_profile.value('ORG_ID')`, `fnd_global.*`, `xxen_util.*`, `sys_context(...)`, `:$flex$.*` expressions, `sysdate` arithmetic on a Date parameter, a one-line `case … end` and calls of the standard SQL functions (`xxen_report.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. Write a conditional default as an expression: `nvl2(:$flex$.x,null,'Y')` for `select 'Y' from dual where :$flex$.x is null`, `decode(:$flex$.x,'Y','A')` for `... where :$flex$.x='Y'`. Keep the select for a call of another package (`gl_ledger_utils_pkg.*`), which `is_function` does not recognise, and for a Date parameter's `sysdate`, which only the bare form converts to the user's time zone. A template's parameter default is never wrapped, so an expression there stays a select.
- **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 `&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`). Validated against the LOV (no wildcards).
- **`X like`** — LIKE operator (`col like :x`). User types wildcards manually (`ABC%`). Free Text must be on.
- **`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 payload in the export: NEVER strip it from exported content.** When a column or parameter resolves a shared LOV (or an Oracle value set), its `filter_before_display` / `lov_query` are stored **null** and the shared LOV governs; validation is the column's or parameter's own `free_text`. The import 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.
- **`<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 flag, and the column/parameter imports as a self-contained **custom LOV**. 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.
- Importing a report or upload therefore never changes a shared LOV the instance already has. When master changes one (an `id` column added), import its `content/r12/lovs/<name>.xml` on the test instance as well, or the column keeps the instance's older behaviour: a list there without the `id` passes the display value to the procedure (a supplier site upload then fails with `Invalid Country.`).
- 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`.
- Wildcards need no other LOV: set Free Text on the parameter or column. For other behavior than the shared LOV provides, 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 validates against its LOV (an LOV type with Free Text off — `set_single_lov_value` is only *called* for such a parameter when `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` without Free Text, and write the LOV so it returns rows only when the parent qualifies, e.g.:
```sql
-- 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 validation, a Free Text (or plain `Number`) parameter is **never** greyed. You cannot have both "disabled until parent set" and "type any arbitrary value":
- Want the **grey-out**? Use a flex-dependent LOV without Free Text. 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 Free Text) 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:**
```sql
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), ...)`):
```sql
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)
- A multi-select parameter that chooses output columns without filtering rows takes two sub-rows. The displayed row carries the LOV and `x.column_name=:display_columns` on a lexical anchor placed in a comment of the main SQL (`/*&display_columns_list*/`): that row is what makes the run screen offer multiple values, and the in-clause it generates lands inside the comment, where its `:run_id` is not bound. The second row, on a select-list lexical (`&display_columns`), is a `select` returning one `<expression> <alias>,` line per chosen column from an inline list of names and expressions, filtered by `xxen_util.contains(:display_columns,x.column_name)='Y'` (a dynamic lexical sees the raw `<multiple_values>A;B` value). A blank parameter removes both anchors, so the optional columns are then absent; declare both anchors in `<ANCHORS>`.
**Reference:** [Blitz Report Developer Guide - Multiple Values](https://www.enginatics.com/blitz-report-developer-guide/#multiple_values)
## SQL patterns
- A clickable link column returns either a bare URL (`http://`, `https://`, `ftp://`, `file://`, `mailto:`, `www.`, without whitespace) or the expression `'HYPERLINK("'||<url>||'","'||<display text>||'")'` with no leading `=`: the engine writes the expression as a formula cell that shows the display text, and the Excel add-in converts the expression and bare `http(s)://` values to `=HYPERLINK()` formulas. With a leading `=` the value is literal text in the XLSX output and in the add-in. Reference: the `url` column of `FND Attached Documents`.
- 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 LOV parameter without Free Text and 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.
- Fixed Assets keeps its lookups in `fa_lookups` (`fa_lookups_b`/`_tl`), not in `fnd_lookup_values`, so `xxen_util.meaning(code,'ASSET TYPE',140)` returns null: join `fa_lookups fl` on `fl.lookup_type` and `fl.lookup_code` for the meaning, and give an asset type parameter the Oracle value set `FA_ASSET_TYPE` (`LOV Oracle`, id and value are the code), as `FA Asset Additions` and `FA Asset Summary (Germany)` do. Both exist on 11i.
- 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.
- Read an operating unit's name for display with a scalar subquery on the document's `org_id` rather than a join to `hr_operating_units`: the view is estimated at one row, so the optimizer starts from it and scans every document of the operating unit instead of starting from the report's selective restriction.
- `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.
- Change an existing schedule in place, never by copy and cancel: in View > Requests select the pending scheduled request, View Details, click the Parameters field, change the runtime option or parameter (Output Format, Server File Name, a report parameter), OK, then Update; the request keeps its schedule. A template's pivot is created only in XLSX output, so a schedule that must deliver the pivot needs Output Format XLSX; a Blitz Report CSV holds the data rows only.
- To run an expensive correlated subquery only for the rows that pass a cheap test, put both in one predicate and hint the subquery: `col=case when <cheap test> then (select /*+ push_subq */ ...) end`. Written as two predicates, `col=(select ...)` on an indexed column becomes the access key and runs before the cheap test, and without `push_subq` a subquery filter runs after every join of its query block, so the joins it was meant to gate run for every row. Prove it with row-source statistics (`statistics_level=all`, `v$sql_plan_statistics_all`): the subquery's starts must equal the rows passing the test.
- To switch a joined inline view off with an optional parameter while the select list keeps referencing its columns, give the parameter a `:bind` anchor and put `:bind='Y'` in the view's where clause: Oracle evaluates a bind-only predicate as a startup filter, so the view reads nothing when the parameter is blank and the outer join leaves its columns null. A lexical cannot do this, because a blank parameter's lexical anchor is removed with no fallback text.
- `ORA-00600: internal error code, arguments: [kkqcscorcbk: correlated string not found.]` at hard parse on a 19c database is an Oracle optimizer bug in join predicate push-down while unnesting a correlated `not in` subquery, typically in a list of values; put `/*+ no_unnest */` on that subquery's select, which parses without changing the result. The database fix is an Oracle SR quoting the incident.
- `po_line_locations_all` by `po_line_id` (`PO_LINE_LOCATIONS_N1`) returns every release shipment of a blanket agreement line, often thousands; restrict by `po_release_id` and keep such a lookup behind a cheap per-row test.
- A PO shipment the buyer adds after the PO is created, typically to split a quantity across promised dates, has no requisition reference: its distribution's `req_distribution_id` is null, the requisition line keeps pointing at the original shipment, and receipts against the added shipment never reach the requisition's received and delivered quantities. Decision: a report that infers the link does so only behind an opt-in parameter and marks the inferred rows (`PO Purchase Requisitions with PO Details`, Show Possible Split Shipments).
## 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.
- The segment columns of a `&chart_of_accounts` / `&segment_cols` lexical ARE exported, under the exporting instance's segment prompts (`Company`, `Department`): a template that lists them without `DYNAMIC_COLUMNS=Y` hides every other instance's segment columns. Set `DYNAMIC_COLUMNS=Y` and leave the segment names out of the template.
- 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.
- Formulas on an extra template sheet that read the data sheet: the template upload strips their saved results (`scan_and_replace`), and when such a sheet has formulas the output sets `fullCalcOnLoad="1"` on `<calcPr>` of the copied `workbook.xml`, so Excel recalculates on open. A build without that (the `XXEN_REPORT` body in `user_source` contains no `fullCalcOnLoad=`) shows the template's own results wherever the strip missed them, an empty result (`<v/>`) or a value with `xml:space="preserve"`: rows that were blank in the template stay blank in an output with more data rows. Workaround there: run `ActiveWorkbook.ForceFullCalculation = True` in the template's VBA Immediate window, save as .xlsx (Excel keeps `forceFullCalc="1"` on save, unlike `fullCalcOnLoad`) and upload the template again; Ctrl+Alt+F9 recalculates an output already generated, F9 and Shift+F9 do not.
- 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`. Columns added to the template column list without this remap leave the workbook on the old layout: the column formats land on the wrong columns and Excel drops pivot data fields on refresh. Detect it by comparing the `cacheField` count with the template column count.
- `<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. A formatted column (date, number with a format) without an entry gets the engine's own per-run style number, which points at an unrelated style of the template, so a date column needs an explicit date style. 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:
```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. A Server File Name is cut only to the 255 bytes a Linux file name holds and Excel's 207 characters; profile Blitz Report Output Filename Length Limit (160 bytes when empty) caps the path of the output file in the concurrent output directory, which the purge program reads, and so also the copy named after it.
- The email subject comes from the FND message named in profile Blitz Report Email Subject Message in both delivery modes, the body from the one named in Blitz Report Email Body Message in the advanced delivery only, as the standard delivery sends no body text (names start with `XXEN_REPORT_EMAIL_SUBJECT` / `_BODY`, 30 characters at most). The profiles have no report level, so text for one report needs its own responsibility or user, or tokens. Subject and body have no parameter tokens: to show a parameter value, put its token in the Output File Name (`<Ledger>`) and `&OUTPUT_FILE_NAME` in the message. Tokens (`xxen_bes.replace_subject_tokens`): `&REPORT_NAME`, `&TEMPLATE_NAME`, `&OUTPUT_FILE_NAME` (the attachment name 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.
- A join written `to_char(<number id>)=<varchar column>` (an order line `line_id` against `ra_customer_trx_lines_all.interface_line_attribute6`) lets the optimizer reach the number side only through another key, so for every AR line it reads all order lines of the customer. Add `to_number(<varchar column> default null on conversion error)=<number id>` and keep the original comparison as a filter, and drive the statement from the selective side: put that table in a `/*+ no_merge */` inline view carrying the date filter and name it in `leading()` (on a mergeable view `leading` is ignored with `hint on view cannot be pushed into view`).
- A customer view with `distinct` joined to every row can run as `VIEW PUSHED PREDICATE` once per row, including any full scan inside it: a salesperson view outer-joining a GL account view ran `gl_security_pkg.validate_access` on every code combination for each invoice line. `no_push_pred(<alias>)` builds it once for a hash join.
- To get the plan of a statement a read-only account cannot `explain` (`ORA-01031` on views whose base tables it has no direct grant on), parse and execute it with `dbms_sql` without fetching, then read `dbms_xplan.display_cursor` for its sql_id: the cursor is built but no rows are produced. In a session without an operating unit the MOAC policy turns into `NULL IS NOT NULL`, which makes the secured tables look empty to the optimizer, so check the join order a hint forces rather than trusting the plan it chose.
## 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`.
## Trade Management (OZF) accruals
- The accrual liability of an offer is its `ozf_funds_utilized_all_b` rows with `plan_type='OFFR'` and `utilization_type` `ACCRUAL` or `ADJUSTMENT` posted to GL (`nvl(gl_posted_flag,'Y')='Y'`; signed `acctd_amount`, by `gl_date`), less what closed claims paid: `ozf_claim_lines_util_all` by `utilization_id`, joined to `ozf_claims_all` with `status_code='CLOSED'`, by the claim's `gl_date`. Relieve the liability with `nvl(utilized_acctd_amount,acctd_amount)`, the claim's amount at the accrual's rate (the difference is FX gain or loss); that column does not exist on 11i. `acctd_amount_remaining` is already reduced by claims that are still open, so it is not an as-of balance. A GL date does not mean posted: a row whose posting failed (`gl_posted_flag='F'`) keeps its `gl_date` and never reaches GL. `OZF Accrual Balance Reconciliation` is the reference.
- `qp_list_headers_b` carries the VPD policy `ORG_SEC` (`QP_ORG_SECURITY`): in a MOAC session it shows only headers with `global_flag='Y'` or an `orig_org_id` the session can access, while a plain SQL session sees all of them. Where the rows are secured by another entity (the accrual's `org_id`), read the offer name from `qp_list_headers_tl`, which has no policy. `ozf_offers` and the `_ALL` OZF tables have no policy; the non-`_ALL` OZF synonyms do.
- The Utilized, Earned and Paid amounts that Oracle stores on `ozf_funds_all_b` are the `ozf_funds_utilized_all_b` rows of `utilization_type` `UTILIZED`, `ACCRUAL`, `ADJUSTMENT`, `CHARGEBACK`, `LEAD_ACCRUAL` and `LEAD_ADJUSTMENT` in the budget currency (`amount`): Earned only where `nvl(gl_posted_flag,'Y')='Y'`, Paid as `amount-amount_remaining` for `UTILIZED` (off-invoice) and otherwise the `util_curr_amount` of closed claims' `ozf_claim_lines_util_all` rows. The offer currency equivalents are `plan_curr_amount` and the claim's `plan_curr_amount`; an offer's Committed is its approved `ozf_act_budgets` requests less transfers back. `ozf_object_checkbook_v` counts `SALES_ACCRUAL` (`gl_posted_flag='X'`) and leaves out `LEAD_ADJUSTMENT`, so it does not tie to the budget columns. `OZF Budget Utilization` and `OZF Offers` are the references.
- A utilization's `product_id` with `product_level_type='PRODUCT'` is an item id: resolve it in the master organization of its operating unit (`oe_sys_parameters_all`, `MASTER_ORGANIZATION_ID`). An item can exist in several master organizations, so a join to all of them multiplies the row.
- `ozf_claims_all.cust_billto_acct_site_id` and `cust_shipto_acct_site_id` hold site use ids despite their names.
## 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. Decision: a site-use profile is reported only for `BILL_TO`, `STMTS` and `DUN` site uses. The form allows no other, and the 12.2 site use api creates one only for those, but the 12.1 api created one for every purpose, so older data carries Ship To profiles that nothing uses (Vision has 410 active ones in OU 204). Join it as `case when hcsua.site_use_code in ('BILL_TO','STMTS','DUN') then hcsua.site_use_id end=hcp.site_use_id(+)`.
- `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.
- `hz_customer_profiles` has three levels: party (`cust_account_id=-1`), account (`site_use_id` null) and site use, with amounts per currency in `hz_cust_profile_amts`. Report a party profile once for each account of the party (join `hz_cust_accounts` on `party_id`), and name the level in a Profile Level column: `xxen_util.meaning('SITE'|'ACCOUNT','HZ_CONS_BILL_LEVEL',222)` and `xxen_util.meaning('PARTY','REQUESTED_BY',222)`. `HZ_CONS_BILL_LEVEL` does not exist on 11i, so wrap its two meanings in `nvl` with the English label in dual-use content.
- 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: `&dff_columns` lexical placeholder
2. In GROUP BY (if aggregating): `&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 `&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`. `p_table_name` is the flexfield's registered `application_table_name` in `fnd_descriptive_flexs` (`FA_ADDITIONS_B` for the `FA_ADDITIONS` flexfield), not the flexfield name: a name matching no registration returns null without an error, and the report runs without DFF columns.
5. Sub-row (no DISPLAY_SEQUENCE) for GROUP BY anchor: same parameter name, anchor `&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`.
**Put a dynamic-column lexical (`&category_columns`, `xxen_util.dff_columns`) in the outermost select, never inside an inline view read through `x.*`.** Generated names repeat whenever two Category Set parameters use the same flexfield structure or segment prompts (`Family`, `Class`). A top-level select list accepts that; an inline view fails every run, with or without a template, with `ORA-00918: column ambiguously defined` at `x.*`. When wrapping a report's SQL in an outer select, move the lexical out with it and pass `p_table_alias=>'x'`; the inline view must expose the item and organization id columns.
**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.
## Templates and snippets
**Report XML skeleton**
```xml
<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**
```sql
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)**
```sql
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**
```sql
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**
```sql
-- 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)**
```sql
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**
```sql
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**
```xml
<!-- 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>
<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**
```xml
<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**
```sql
-- 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**
```sql
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`.