From 116aca2269ea9130c9bfe9d844e2408687e5561c Mon Sep 17 00:00:00 2001 From: red1oon Date: Sat, 18 Jul 2026 03:01:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(erp):=20W-POST-TAIL-2=20=E2=80=94=20Cash/I?= =?UTF-8?q?nventory=20close=20=E2=88=85-by-data-state=20(empirically=20dri?= =?UTF-8?q?ven)=20+=20ERPUserGuide=20core=20S&D=20chapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drove the REAL compiled posters on a fresh scratch clone (idempiere_tail) over the 2 real CO cash journals + 3 real inventory drafts, zero seed authoring. Both classes turn out blocked by real engine-level data-state gates, not "never attempted": C_Cash's 2 docs are IsActive=N (CannotPostInactiveDocument, Doc.java:591-605); M_Inventory's 2 drafts have zero lines (@NoLines@), and the 1 completable draft has no cost data anywhere (No Costs for TShirt - Red Large, Doc_Inventory.java:319-336). Neither worked around — mutating IsActive or seeding costs onto an existing document is out of the seed-prep ruling's scope, same boundary the card drew for the @NoLines@ docs. doc_poster.js gained deriveCash/deriveInventory (source-cited, reusable) proven live via falsifier since no oracle-diff exists to claim. Ledger stays 52/17-of-20 posters — Cash and Inventory join Production as named ⛔, each precisely cited. Also adds ERPUserGuide.md's "lay of the land" navigation map and the core S&D standard-flow chapter (Sales Order → Shipment → AR → Allocation → Replenishment → PO/Receipt/Match → GL/Bank → Trial Balance → Financial Reports), each step citing its real proving witness. Addon lenses untouched. Co-Authored-By: Claude Sonnet 5 --- PROGRESS.md | 10 +- build/erp/oracle/post_tail_fixture.json | 1153 +++++++++++++++++ docs/ERPUserGuide.md | 170 +++ docs/internal/ERP_COVERAGE_MATRIX.md | 4 +- prompts/HARDEN_MATRIX.md | 92 ++ prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md | 51 + scripts/capture_post_tail_fixture.js | 160 +++ scripts/doc_poster.js | 113 ++ scripts/generate_post_tail_oracle.sh | 75 ++ scripts/logic_oracle/PostingTailTest.java | 156 +++ scripts/poc_post_tail.js | 92 +- 11 files changed, 2066 insertions(+), 10 deletions(-) create mode 100644 build/erp/oracle/post_tail_fixture.json create mode 100644 scripts/capture_post_tail_fixture.js create mode 100755 scripts/generate_post_tail_oracle.sh create mode 100644 scripts/logic_oracle/PostingTailTest.java diff --git a/PROGRESS.md b/PROGRESS.md index 7a007104f..28d9923ba 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -20,8 +20,8 @@ not push/open a PR unless the user explicitly lifts it for that session. `CLAUDE.md` §⏸ PUSH PAUSE. ## OPEN — to be assigned to sessions (user dispatches from this list, check before starting cold) -- `prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md` — SONNET: finish Cash/Inventory posting oracles - (generator built, reuse) + ERPUserGuide navigation + core S&D-flow chapter. Fresh branch off master. +- `prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md` — §A (Cash/Inventory) CLOSED 2026-07-18, see Archive; + §B (ERPUserGuide navigation + core S&D-flow chapter) still open on this branch. - `prompts/RESUME_HR_BIM_ASSET.md` §2026-07-06c — A/B/C bugs + E decision. - `prompts/RESUME_WORLD_HISTORY_DEDUP_RESTORE.md` §2026-07-06 — G6, Ph3, Pt1 parked. - `prompts/PILL_DRAWER_REORGANIZATION.md` — first-touch flicker. @@ -50,6 +50,12 @@ not push/open a PR unless the user explicitly lifts it for that session. `CLAUDE - ✅ B-3 + W-POST-TAIL posting oracles (2026-07-17/18, Fable 5) — 6 G-seed classes + BankStatement/ MatchPO/Requisition `maxDiff=0c`; ledger 43→52; 17/20 posters fold; Cash/Inventory next, Production ⛔. `prompts/FABLE5_B3_POSTING_ORACLE.md` + `prompts/HARDEN_MATRIX.md §W-POST-TAIL`. +- ✅ §W-POST-TAIL-2 Cash/Inventory CLOSED (2026-07-18, Sonnet) — drove the REAL posters on a fresh + scratch clone over the 5 real remaining docs: BOTH C_Cash docs are `IsActive='N'` (CannotPostInactive- + Document); 2/3 M_Inventory drafts have zero lines (`@NoLines@`), the 1 completable draft has no cost + data anywhere (`"No Costs for TShirt - Red Large"`). Neither worked around (data mutation on existing + docs, out of scope). Ledger STAYS 52/17-of-20 — Cash+Inventory join Production as named ⛔, each + precisely cited, none a placeholder. `prompts/HARDEN_MATRIX.md §W-POST-TAIL` (§W-POST-TAIL-2 band). - ✅ 2026-07-10 marathon branches (2026-07-17 verify) — `fix/grid-tilt-guard`, `fix/dw-rot-units`, `fable/dwprobe-dedup`, `fix/terminal-oracle-source` all confirmed fully superseded (exact fix content already verbatim on main via other commits) and pruned, local+origin, bim-ootb. diff --git a/build/erp/oracle/post_tail_fixture.json b/build/erp/oracle/post_tail_fixture.json new file mode 100644 index 000000000..e9b4108f6 --- /dev/null +++ b/build/erp/oracle/post_tail_fixture.json @@ -0,0 +1,1153 @@ +{ + "db": "idempiere_tail", + "tables": { + "fact_acct": { + "cols": [ + "fact_acct_id", + "ad_client_id", + "ad_org_id", + "c_acctschema_id", + "account_id", + "c_period_id", + "ad_table_id", + "record_id", + "line_id", + "gl_category_id", + "c_tax_id", + "postingtype", + "c_currency_id", + "amtsourcedr", + "amtsourcecr", + "amtacctdr", + "amtacctcr", + "qty", + "m_product_id", + "c_bpartner_id", + "description" + ], + "rows": [] + }, + "c_cashbook": { + "cols": [ + "c_cashbook_id", + "c_currency_id", + "ad_org_id" + ], + "rows": [ + [ + "101", + "100", + "11" + ] + ] + }, + "c_cash": { + "cols": [ + "c_cash_id", + "c_cashbook_id", + "statementdate", + "docstatus", + "posted", + "isactive", + "ad_org_id", + "ad_client_id" + ], + "rows": [ + [ + "100", + "101", + "2002-02-22", + "CO", + "N", + "N", + "11", + "11" + ], + [ + "101", + "101", + "2002-02-22", + "CO", + "N", + "N", + "11", + "11" + ] + ] + }, + "c_cashline": { + "cols": [ + "c_cashline_id", + "c_cash_id", + "cashtype", + "c_currency_id", + "amount", + "c_bankaccount_id", + "c_invoice_id", + "c_charge_id" + ], + "rows": [ + [ + "100", + "100", + "I", + "100", + "50.35", + "0", + "100", + "0" + ], + [ + "101", + "101", + "E", + "100", + "-10.00", + "100", + "0", + "0" + ], + [ + "102", + "101", + "T", + "100", + "-50.00", + "100", + "0", + "0" + ], + [ + "103", + "101", + "R", + "100", + "10.00", + "100", + "0", + "0" + ] + ] + }, + "c_cashbook_acct": { + "cols": [ + "c_cashbook_id", + "c_acctschema_id", + "cb_asset_acct", + "cb_cashtransfer_acct", + "cb_expense_acct", + "cb_receipt_acct", + "cb_differences_acct" + ], + "rows": [ + [ + "101", + "101", + "273", + "288", + "275", + "276", + "274" + ], + [ + "101", + "200000", + "0", + "200065", + "0", + "0", + "0" + ] + ] + }, + "c_bankaccount_acct": { + "cols": [ + "c_bankaccount_id", + "c_acctschema_id", + "b_intransit_acct" + ], + "rows": [ + [ + "100", + "101", + "257" + ], + [ + "100", + "200000", + "200053" + ] + ] + }, + "c_charge_acct": { + "cols": [ + "c_charge_id", + "c_acctschema_id", + "ch_expense_acct" + ], + "rows": [ + [ + "100", + "101", + "267" + ], + [ + "100", + "200000", + "200061" + ], + [ + "101", + "101", + "297" + ], + [ + "101", + "200000", + "200061" + ], + [ + "200000", + "101", + "200017" + ], + [ + "200000", + "200000", + "200061" + ] + ] + }, + "m_inventory": { + "cols": [ + "m_inventory_id", + "docstatus", + "posted", + "movementdate", + "m_warehouse_id", + "c_doctype_id", + "ad_org_id", + "ad_client_id" + ], + "rows": [ + [ + "100", + "CO", + "E", + "2004-08-13", + "103", + "144", + "11", + "11" + ], + [ + "200000", + "IN", + "N", + "2013-03-24", + "50002", + "144", + "50001", + "11" + ], + [ + "200001", + "IN", + "N", + "2013-03-24", + "50002", + "144", + "50001", + "11" + ] + ] + }, + "m_inventoryline": { + "cols": [ + "m_inventoryline_id", + "m_inventory_id", + "m_product_id", + "m_locator_id", + "qtycount", + "qtybook", + "qtyinternaluse", + "m_attributesetinstance_id", + "c_charge_id", + "isactive", + "reversalline_id" + ], + "rows": [ + [ + "100", + "100", + "147", + "101", + "1.00", + "0.00", + "0.00", + "0", + "0", + "Y", + "0" + ] + ] + }, + "c_doctype": { + "cols": [ + "c_doctype_id", + "docsubtypeinv" + ], + "rows": [ + [ + "144", + "PI" + ] + ] + }, + "m_locator": { + "cols": [ + "m_locator_id", + "m_warehouse_id" + ], + "rows": [ + [ + "101", + "103" + ] + ] + }, + "m_warehouse_acct": { + "cols": [ + "m_warehouse_id", + "c_acctschema_id", + "w_differences_acct" + ], + "rows": [ + [ + "103", + "101", + "227" + ], + [ + "103", + "200000", + "200051" + ] + ] + }, + "m_product": { + "cols": [ + "m_product_id", + "producttype", + "isstocked", + "m_product_category_id", + "name" + ], + "rows": [ + [ + "147", + "I", + "Y", + "105", + "TShirt - Red Large" + ] + ] + }, + "m_product_category_acct": { + "cols": [ + "m_product_category_id", + "c_acctschema_id", + "p_asset_acct", + "p_expense_acct" + ], + "rows": [ + [ + "105", + "101", + "231", + "230" + ], + [ + "105", + "200000", + "200038", + "200039" + ] + ] + }, + "m_cost": { + "cols": [ + "m_product_id", + "c_acctschema_id", + "m_costtype_id", + "m_costelement_id", + "currentcostprice" + ], + "rows": [ + [ + "147", + "101", + "100", + "100", + "0" + ], + [ + "147", + "101", + "100", + "105", + "0" + ], + [ + "147", + "101", + "100", + "50000", + "0" + ], + [ + "147", + "101", + "100", + "50001", + "0" + ], + [ + "147", + "101", + "100", + "50002", + "0" + ] + ] + }, + "m_costelement": { + "cols": [ + "m_costelement_id", + "costingmethod", + "costelementtype" + ], + "rows": [ + [ + "100", + "S", + "M" + ], + [ + "101", + "", + "M" + ], + [ + "102", + "F", + "M" + ], + [ + "103", + "A", + "M" + ], + [ + "104", + "I", + "M" + ], + [ + "105", + "S", + "R" + ], + [ + "50000", + "S", + "B" + ], + [ + "50001", + "S", + "O" + ], + [ + "50002", + "S", + "X" + ] + ] + }, + "m_costdetail": { + "cols": [ + "m_costdetail_id", + "m_product_id", + "processed", + "amt", + "qty", + "c_orderline_id", + "c_invoiceline_id" + ], + "rows": [] + }, + "c_acctschema": { + "cols": [ + "c_acctschema_id", + "c_currency_id", + "costingmethod", + "m_costtype_id" + ], + "rows": [ + [ + "101", + "100", + "A", + "100" + ], + [ + "200000", + "102", + "A", + "100" + ] + ] + }, + "c_validcombination": { + "cols": [ + "c_validcombination_id", + "account_id" + ], + "rows": [ + [ + "219", + "698" + ], + [ + "220", + "699" + ], + [ + "221", + "724" + ], + [ + "222", + "631" + ], + [ + "223", + "771" + ], + [ + "224", + "600" + ], + [ + "225", + "741" + ], + [ + "226", + "776" + ], + [ + "227", + "439" + ], + [ + "228", + "442" + ], + [ + "229", + "758" + ], + [ + "230", + "431" + ], + [ + "231", + "742" + ], + [ + "232", + "445" + ], + [ + "233", + "430" + ], + [ + "234", + "518" + ], + [ + "235", + "750" + ], + [ + "237", + "749" + ], + [ + "238", + "588" + ], + [ + "239", + "532" + ], + [ + "240", + "427" + ], + [ + "241", + "448" + ], + [ + "242", + "765" + ], + [ + "243", + "712" + ], + [ + "244", + "722" + ], + [ + "245", + "767" + ], + [ + "246", + "723" + ], + [ + "247", + "598" + ], + [ + "248", + "533" + ], + [ + "249", + "652" + ], + [ + "250", + "567" + ], + [ + "251", + "568" + ], + [ + "252", + "760" + ], + [ + "253", + "597" + ], + [ + "254", + "535" + ], + [ + "255", + "596" + ], + [ + "256", + "534" + ], + [ + "257", + "509" + ], + [ + "258", + "508" + ], + [ + "259", + "661" + ], + [ + "260", + "705" + ], + [ + "261", + "717" + ], + [ + "262", + "510" + ], + [ + "263", + "711" + ], + [ + "264", + "769" + ], + [ + "265", + "710" + ], + [ + "266", + "721" + ], + [ + "267", + "702" + ], + [ + "268", + "703" + ], + [ + "269", + "423" + ], + [ + "270", + "521" + ], + [ + "271", + "424" + ], + [ + "272", + "587" + ], + [ + "273", + "515" + ], + [ + "274", + "690" + ], + [ + "275", + "700" + ], + [ + "276", + "701" + ], + [ + "277", + "508" + ], + [ + "282", + "444" + ], + [ + "283", + "447" + ], + [ + "284", + "426" + ], + [ + "285", + "441" + ], + [ + "286", + "511" + ], + [ + "287", + "589" + ], + [ + "288", + "516" + ], + [ + "290", + "419" + ], + [ + "291", + "635" + ], + [ + "292", + "515" + ], + [ + "293", + "516" + ], + [ + "294", + "700" + ], + [ + "295", + "690" + ], + [ + "296", + "701" + ], + [ + "297", + "778" + ], + [ + "298", + "781" + ], + [ + "299", + "780" + ], + [ + "300", + "736" + ], + [ + "50000", + "50000" + ], + [ + "50001", + "50005" + ], + [ + "50002", + "50006" + ], + [ + "50003", + "50002" + ], + [ + "50004", + "50001" + ], + [ + "50005", + "50003" + ], + [ + "50006", + "50004" + ], + [ + "50007", + "50007" + ], + [ + "50008", + "50009" + ], + [ + "50009", + "50010" + ], + [ + "50010", + "50011" + ], + [ + "50011", + "50014" + ], + [ + "50012", + "50016" + ], + [ + "50013", + "50017" + ], + [ + "200000", + "200000" + ], + [ + "200001", + "501" + ], + [ + "200002", + "742" + ], + [ + "200003", + "574" + ], + [ + "200004", + "574" + ], + [ + "200005", + "714" + ], + [ + "200006", + "726" + ], + [ + "200007", + "646" + ], + [ + "200008", + "558" + ], + [ + "200009", + "726" + ], + [ + "200010", + "556" + ], + [ + "200011", + "569" + ], + [ + "200012", + "558" + ], + [ + "200013", + "563" + ], + [ + "200014", + "564" + ], + [ + "200015", + "575" + ], + [ + "200016", + "647" + ], + [ + "200017", + "434" + ], + [ + "200018", + "600" + ], + [ + "200019", + "698" + ], + [ + "200020", + "724" + ], + [ + "200021", + "635" + ], + [ + "200022", + "736" + ], + [ + "200023", + "50000" + ], + [ + "200024", + "741" + ], + [ + "200025", + "712" + ], + [ + "200026", + "722" + ], + [ + "200027", + "767" + ], + [ + "200028", + "723" + ], + [ + "200029", + "587" + ], + [ + "200030", + "423" + ], + [ + "200031", + "427" + ], + [ + "200032", + "448" + ], + [ + "200033", + "765" + ], + [ + "200034", + "518" + ], + [ + "200035", + "749" + ], + [ + "200036", + "750" + ], + [ + "200037", + "532" + ], + [ + "200038", + "742" + ], + [ + "200039", + "431" + ], + [ + "200040", + "781" + ], + [ + "200041", + "780" + ], + [ + "200042", + "430" + ], + [ + "200043", + "758" + ], + [ + "200044", + "445" + ], + [ + "200045", + "444" + ], + [ + "200046", + "447" + ], + [ + "200047", + "426" + ], + [ + "200048", + "50003" + ], + [ + "200049", + "50017" + ], + [ + "200050", + "200000" + ], + [ + "200051", + "439" + ], + [ + "200052", + "508" + ], + [ + "200053", + "509" + ], + [ + "200054", + "589" + ], + [ + "200055", + "511" + ], + [ + "200056", + "717" + ], + [ + "200057", + "705" + ], + [ + "200058", + "596" + ], + [ + "200059", + "534" + ], + [ + "200060", + "760" + ], + [ + "200061", + "702" + ], + [ + "200062", + "567" + ], + [ + "200063", + "568" + ], + [ + "200064", + "508" + ], + [ + "200065", + "516" + ], + [ + "200066", + "419" + ], + [ + "1000000", + "596" + ] + ] + } + } +} \ No newline at end of file diff --git a/docs/ERPUserGuide.md b/docs/ERPUserGuide.md index 413848949..3d29b67df 100644 --- a/docs/ERPUserGuide.md +++ b/docs/ERPUserGuide.md @@ -55,6 +55,47 @@ Both are marked *CONCEPT* and run fully offline. --- +## The lay of the land + +Before the window-by-window detail, here is the map: what's where, and why. Every box below is a real +surface with its own section later in this guide — this is navigation only, not a feature tour. + +```mermaid +flowchart TD + A["bubbles front door
(erp.html)"] -->|long-press / double-tap| B["Login
(§1) — pick a role"] + B --> C["Bottom Pill Bar
(§3) — the one entry point for every tool"] + C --> D["Windows / Tabs / Fields
(§4, §5) — role-pruned menu → records → forms"] + D --> E["the Process Button
(§6) — the ▶ on any doc-bearing form"] + E --> F["where documents live
Sales Order · Purchase Order · Shipment · Invoice · Payment"] + F --> G["where the books live
Posting Preview · Trial Balance · Financial Reports (§8)"] +``` + +**How to read it, left to right:** + +- **The front door** (§Quick start above) is the bubble launcher — playful, but every bubble is a real + AD entity. A long-press or double-tap is the bridge into the classic renderer described below; if you + don't want the bubbles at all, open `idempiere.html` directly and skip straight there. +- **Login (§1)** picks the *role* — GardenUser, Admin, WebService — and the menu you see next is pruned + live by that role's real `AD_Window_Access`/`AD_Process_Access` grants, not a mock. +- **The Bottom Pill Bar (§3)** is the one control surface you return to for everything else: opening the + menu (⌂), finding a record (🔍), the process/plugin/history tools, and Help. Everything past this + point is reached FROM the pill bar, not around it. +- **Windows → Tabs → Fields (§4, §5)** is the drill-down: a role-pruned window list, a record list per + window, a detail form per record, tabs/fields on that form driven by real `AD_Tab`/`AD_Field` rows + (DisplayLogic, mandatory flags — nothing hand-styled). +- **The Process Button (§6)** is how a document *moves* — Complete, Void, Close, whatever the current + `AD_Docfsm` state legally allows. This is the same button whether you're on a Sales Order, a Purchase + Order, a Payment, or a GL Journal — one dispatch spine, not one-off per window. +- **Where documents live** is simply "which window": Sales Order, Purchase Order, Shipment (`M_InOut`), + Invoice (`C_Invoice`), Payment (`C_Payment`) — each a normal AD window reached via ⌂, nothing special + about its URL or location. +- **Where the books live** is the destination every document eventually posts to: open **Posting + Preview** on any completed document to see its journal before/after it posts, or jump straight to + **Trial Balance**/the **Financial Reports** (§8) to see the ledger as a whole. **"The standard + flow"** section below walks this entire left-to-right path as one continuous story, with the demo data. + +--- + ## Initial Tenant Setup — born a new client *(LIVE)* This is iDempiere's **Initial Client Setup**, on our engine. Open **genesis.html**, give the new @@ -560,6 +601,135 @@ No traditional row-update happens; the current state is the fold of all ops on t --- +## The standard flow — order to cash, procure to pay, books to reports + +Everything above (§3-§6) is *mechanism* — the pill bar, the menu, the form, the process button. This +section is the *trade cycle itself*, told once as a continuous story over the GardenWorld demo data: +one sale pulls stock down, stock falling triggers a purchase, the purchase brings stock back, and every +step along the way lands in the books. Each step below is already **live and oracle-proven** — the +aside after each one names the witness that proves it, so this walkthrough inherits that credibility +instead of re-arguing it. (Addon lenses — POS, Kitchen, Warehouse Walk, Tenancy, BIM-4D scheduling, +Ninja mode — ride this SAME cycle from a friendlier surface; they are documented in their own sections +and are not repeated here.) + +```mermaid +flowchart LR + SO["1 Sales Order"] --> SH["2 Shipment"] + SH --> INV["3 Customer Invoice → AR"] + INV --> AL["4 Receipt & Allocation"] + SH -.on-hand falls.-> RP["5 Replenishment"] + RP --> PO["6a Purchase Order"] + PO --> RC["6b Receipt"] + RC --> VI["6c Vendor Invoice"] + VI --> MI["6d Match"] + AL --> FA["7 Final accounts"] + MI --> FA + FA --> FR["8 Financial reporting"] +``` + +### 1 · Sales Order + +Open **Sales Order** from the menu (⌂ → Sales), create a line or two, and press the **Process** button +(§6) → **Complete**. Completion is not just a status flip: `completeIt` fans out into the shipment and +invoice that follow it, in the SAME signed op-group. + +> *Proven by* **W-FOLD-COMPLETE** (`poc_fold_complete.js`) — the whole Order→Ship→Invoice fan-out folds +> against the real `m_inoutline`/`c_invoiceline`/`fact_acct` rows, `maxDiff=0c`. + +### 2 · Shipment + +The **Shipment** (`M_InOut`) that Complete generated is a real window of its own (⌂ → Inventory → +Shipment) — open it to see the same document from the warehouse side. On-hand for every shipped product +drops immediately; the shipment also carries its own GL leg (Cost of Goods Sold against Inventory), not +just a paper movement. + +> *Proven by* **W-FOLD-QTYONHAND** (on-hand = the signed movement ledger, 28/28 real movements +> reconstructed, `maxDiff=0`) and **W-FOLD-INOUTGL** (the shipment's COGS/Inventory posting, folded as +> part of W-FOLD-COMPLETE's `fact_acct(319)` diff). + +### 3 · Customer Invoice → AR + +The **Invoice** the same Complete generated posts DR **Receivable** / CR **Revenue** (per line) / CR +**Tax Due**. Open **Posting Preview** on the invoice (any completed document has this) to see that exact +journal before you take our word for it — it is the SAME derivation the guide's Posting-Preview panel +runs live, not a separate report-only calculation. + +> *Proven by* **W-POST-HARDEN** (4/4 sales invoices resolve iDempiere's exact accounts, 3/4 to the cent — +> the 4th is a named post-posting source edit, not a derivation gap) and **W-DOC-POSTER** (the shipped +> `derivePostings` verb Posting Preview actually calls, oracle-anchored to `fact_acct(318)`). + +### 4 · Receipt & Allocation + +Record the customer's **Payment** (⌂ → Sales → Payment) against the invoice, then run **Allocation** +to match payment to invoice. A real allocation is rarely an exact match — it can carry a discount, a +write-off, and (because tax rides the discounted portion too) a VAT correction, all in the same posting. + +> *Proven by* **W-FOLD-PAYMENT** (`Doc_Payment` receipt, `fact_acct(335)` `maxDiff=0c`) and +> **W-FOLD-ALLOC** / **W-FOLD-ALLOC-FX** (the allocation incl. discount/write-off + the proportional +> tax-correction sub-cents, `fact_acct(735)` `maxDiff=0c` in both the home and the foreign-currency +> accounting schema). + +### 5 · Replenishment + +The sale just dropped on-hand. Rather than reorder on every sale, **Replenishment** is the staged, +reviewable action that watches on-hand against each product's stock policy and proposes what to order — +walked in detail in the POS section's **§P-4** (the same engine, reached from a friendlier panel there; +not repeated here). + +> *Proven by* **W-FOLD-REPLENISH** (the proposal == iDempiere's own `ReplenishReport` formula-SQL, +> 8/8 products, `maxDiff=0`). + +### 6 · Purchase Order → Receipt → Vendor Invoice → Match + +Accepting a replenishment proposal (or creating one by hand, ⌂ → Purchasing → Purchase Order) and +completing it is the buy-side mirror of step 1. It flows the same way in reverse: + +- **Purchase Order** — completes; under this seed's configuration (commitment accounting off) it posts + no commitment/reservation entry — an honest, config-derived ∅, not a gap. +- **Receipt** (`M_InOut`, incoming) — stock arrives, on-hand rises. +- **Vendor Invoice** — posts DR **Inventory Clearing** / CR **Vendor Liability**. +- **Match** (`M_MatchInv`) — clears the Inventory-Clearing/Not-Invoiced-Receipts pair the receipt and + invoice each opened, including any invoice-price-variance split against on-hand at match time. + `M_MatchPO`'s own leg stays an honest ∅ under this seed's Average costing (the variance-posting branch + only fires under Standard costing) — a config fact, proven by flipping it in the witness, not assumed. + +> *Proven by* **W-MORDER-POST** (the PO's zero-set is config-derived, the order-posting chain diffs per +> fact line against the real oracle), **W-FOLD-AP-INVOICE** (4/4 vendor invoices, exact accounts, +> `maxDiff=0c`), **W-FOLD-MATCHINV** / **W-FOLD-MATCHINV-FX** (18/18 matches incl. the IPV split, both +> accounting schemas, `maxDiff=0c`), and **W-POST-TAIL** (`M_MatchPO`'s ∅ over all 37 real matched docs, +> falsified by flipping the costing method to prove the gate — not the witness — is what closes it). + +### 7 · Final accounts + +Two more postings close the period: a **GL Journal** for anything that isn't a document (a manual +accrual, an inter-org transfer) and a **Bank Statement** reconciling the bank's own records against +what the ledger expects. Once every document for the period is posted, the period's `fact_acct` **is** +the ledger — there is no separate "close" step that recomputes anything; Trial Balance is a live fold +of the same rows, and it balances to the cent. + +> *Proven by* **W-FOLD-GLJOURNAL** (manual journal incl. per-org intercompany balancing, both schemas, +> `maxDiff=0c`) and **W-POST-TAIL**'s `C_BankStatement` band (the real 13-row statement, `maxDiff=0c` +> incl. the currency-balancing residual). Trial Balance: `ΣDr==ΣCr=46574.97` over the real GardenWorld +> ledger (`test_report_fin.js`). + +### 8 · Financial reporting + +From here, everything is read-only fold: **Balance Sheet**, **Income Statement**, **Cash Flow**, and +**Trial Balance** are all oracle-equivalent to the real ledger (§8 *Financial Reporting*, below, has the +per-report cell counts), and the same ledger is what the **NinjaExcel** workbook lens (§8, "Excel +Report") binds into your own spreadsheet layout. + +### Also in the books: fixed assets and projects + +Six more document classes — asset addition, transfer, revaluation, and disposal, plus depreciation +entries and project-issue postings — fold to the cent against the real compiled posters the same way +(oracle-generated on a scratch clone over a GardenWorld-model seed, since this demo tenant carries no +source documents for them out of the box). They ride the same Posting-Preview/Trial-Balance surfaces as +everything above; see `docs/internal/ERP_COVERAGE_MATRIX.md` (B-3, W-POST-B3) for the per-class detail — +this guide's walkthrough stays the trade cycle. + +--- + ## 7. POS — Point of Sale **Prerequisite:** log in as **GardenUser** (the POS station `c_pos_id=1` is scoped to that role). diff --git a/docs/internal/ERP_COVERAGE_MATRIX.md b/docs/internal/ERP_COVERAGE_MATRIX.md index f4dd3d516..46100f9dc 100644 --- a/docs/internal/ERP_COVERAGE_MATRIX.md +++ b/docs/internal/ERP_COVERAGE_MATRIX.md @@ -110,9 +110,9 @@ denominator, not 496 classes / 735k LOC). | ✅ **oracle-equivalent** | **0-seed posting oracles — the 6 G-seed classes (B-3, W-POST-B3, ledger 43→49)** — `poc_post_b3.js` diffs the EXTENDED `doc_poster.derivePostings` (per-class manifests: `fa-addition`/`fa-depreciation`/`fa-reval`/`fa-transfer`/`fa-disposal`/`project-issue`, each citing its Doc_*.java lines) against a **GENERATED oracle**: the REAL compiled posters driven inside iDempiere's own OSGi test harness (`scripts/logic_oracle/PostingOracleTest.java` under `org.idempiere.test` tycho-surefire — resolves the G-3 "no headless OSGi" landmine) on a SCRATCH clone `idempiere_b3` of `idempiere_test`, over a GardenWorld-model seed (vendor FixedAssetsTest recipe; USER RULING 2026-07-17: seed INPUT prep sanctioned, facts never hand-authored). Capture = `scripts/generate_post_oracle.sh` → TEXT fixture `build/erp/oracle/post_b3_fixture.json` (34 fact rows, ΣDR=ΣCR=81360.56, both schemas 101+200000). Result: **all 6 classes `maxDiff=0c` per document × schema × (account,side)** — `§B3-POST`: A_Asset_Addition (4 docs incl. the A_CapvsExp=Exp arm-Z doc posting ZERO rows, gate=Doc_AssetAddition.java:67-72, **+ the fx-rate pick MConversionRate:243-252 the diff CAUGHT** — engine had picked the seed's 0.8006 twin row where the real poster picks 0.85; root cause = the twin is `IsActive='N'` and `:251` filters it (first mis-read as client-rank ordering, corrected 2026-07-18 when poc_alloc_fx §FALSIFIER-B exposed the capture-side filter masking it); `fxRate` now carries the verbatim clauses and the inactive row stays captured) · A_Depreciation_Entry (per-depexp DR/CR; schema-200000 = the other-schema ∅ branch) · A_Asset_Reval (2 delta pairs vs the seeded A_Reval_Cost_Offset_Acct) · A_Asset_Transfer (workfile cost old→new combos; the completeIt validfrom time-slice keeps pre-transfer docs derivable) · A_Asset_Disposed (from the per-schema A_Asset_Change 'DIS' row) · C_ProjectIssue (PJ_WIP vs Product.Asset at the schema costing-method cost — the W-FOLD-MOVEMENT hop). 2 §FALSIFIERs load-bearing (gate-flip Cap→Exp → 2→0 lines; double one depexp expense → ΣDR 55556c→111112c) + the 2 triage falsifiers. The 2 ∅-by-design classes (C_BankTransfer/C_DepositBatch — no Doc_ in the factory) stay structurally CLOSED. Regressions green after the doc_poster extension: W-POST-HARDEN · W-FACTACCT-DOC · W-DOC-POSTER · W-MORDER-POST · test_report_fin 46574.97/300 rows intact. `build/erp/poc_post_b3.log` · `build/erp/generate_post_oracle.log`. | -| ✅ **oracle-equivalent** | **Doc_* poster tail — BankStatement + the config-∅ pair (W-POST-TAIL, 2026-07-18, ledger 49→52)** — `poc_post_tail.js` closes 3 more of the last 6 posters, each against a REAL seed oracle (no generation needed): **C_BankStatement (392)** — the `bank-statement` manifest (Doc_BankStatement.createFacts:200-280: per line {Bank.Asset}=+StmtAmt · {Bank.InTransit}=−TrxAmt · charge >0→CR/<0→DR · interest <0→InterestExp else Rev; DOC-currency legs converted per schema + the Fact.balanceAccounting CurrencyBalancing residual — the REAL 0.01 DR on schema 200000) **== the 13 posted fact rows, maxDiff=0c** per doc × schema × (account, side). **M_MatchPO (473)** — the REAL engine posted the EMPTY set for all **37** posted docs: PPV gated on StandardCosting (Doc_MatchPO.java:429), seed costs at 'A' → engine ∅ == oracle ∅ across the full population; §FALSIFIER flips costingmethod→'S' + cost≠price → the PPV path opens. **M_Requisition (702)** — 1 posted doc, ∅ by `MAcctSchema.isCreateReservation` (:662-669, commitmenttype='N'); §FALSIFIER flips 'N'→'B' → expense+commitment lines appear (the W-MORDER-POST twin). 3rd falsifier: doubling a stmt line breaks the bank diff (14850c→24650c). Additive captures only (c_bankstatementline amounts/charge, bank interest accts, c_charge_acct, m_matchpo, m_requisitionline, rate isactive) — whole posting bundle + TB 46574.97/300 re-verified green. **Named skips:** C_Cash (2 real CO docs never posted → post on scratch clone next), M_Inventory (3 drafts → complete+post next), M_Production (0 docs + no component costs → ⛔, costs not synthesized). `build/erp/poc_post_tail.log`. | +| ✅ **oracle-equivalent** | **Doc_* poster tail — BankStatement + the config-∅ pair (W-POST-TAIL, 2026-07-18, ledger 49→52)** — `poc_post_tail.js` closes 3 more of the last 6 posters, each against a REAL seed oracle (no generation needed): **C_BankStatement (392)** — the `bank-statement` manifest (Doc_BankStatement.createFacts:200-280: per line {Bank.Asset}=+StmtAmt · {Bank.InTransit}=−TrxAmt · charge >0→CR/<0→DR · interest <0→InterestExp else Rev; DOC-currency legs converted per schema + the Fact.balanceAccounting CurrencyBalancing residual — the REAL 0.01 DR on schema 200000) **== the 13 posted fact rows, maxDiff=0c** per doc × schema × (account, side). **M_MatchPO (473)** — the REAL engine posted the EMPTY set for all **37** posted docs: PPV gated on StandardCosting (Doc_MatchPO.java:429), seed costs at 'A' → engine ∅ == oracle ∅ across the full population; §FALSIFIER flips costingmethod→'S' + cost≠price → the PPV path opens. **M_Requisition (702)** — 1 posted doc, ∅ by `MAcctSchema.isCreateReservation` (:662-669, commitmenttype='N'); §FALSIFIER flips 'N'→'B' → expense+commitment lines appear (the W-MORDER-POST twin). 3rd falsifier: doubling a stmt line breaks the bank diff (14850c→24650c). Additive captures only (c_bankstatementline amounts/charge, bank interest accts, c_charge_acct, m_matchpo, m_requisitionline, rate isactive) — whole posting bundle + TB 46574.97/300 re-verified green. **C_Cash + M_Inventory closed 2026-07-18 (§W-POST-TAIL-2, empirically driven, ledger STAYS 52):** drove the REAL compiled posters on a FRESH scratch clone `idempiere_tail` (`scripts/generate_post_tail_oracle.sh` + `scripts/logic_oracle/PostingTailTest.java`, zero seed authoring — every document already existed) over the 2 real CO cash journals + 3 real inventory drafts. Result: **both classes are ∅-by-DATA-STATE, not ∅-by-config** — a new, distinct category, each precisely engine-cited: **C_Cash (407)** — BOTH docs (100/101) carry `IsActive='N'`; `Doc.postIt`'s lock UPDATE (`Doc.java:591-605`) requires `IsActive='Y'` before `createFacts` ever runs → `DocManager.postDocument` returns `"CannotPostInactiveDocument"` for both; NOT worked around (flipping `IsActive` on an existing document is data mutation out of the seed-prep ruling's scope). **M_Inventory (321)** — drafts 200000/200001 have ZERO lines each → `MInventory.prepareIt` (`MInventory.java:401-406`) refuses BEFORE completion with `@NoLines@`; the 1 completable draft (doc 100, product 147) DOES reach `DocStatus=Completed` but the REAL engine then REFUSES to post it — `"No Costs for TShirt - Red Large"` (`Doc_Inventory.java:319-336`: product 147 has `currentcostprice=0` everywhere and zero `M_CostDetail` rows, so the zero-cost-blessing check fails). Both manifests were still written into `doc_poster.js` (`deriveCash`/`deriveInventory`, full source-cited translations, reusable for any FUTURE active/costed document) and PROVEN live via falsifier, not oracle-diff: `deriveCash` computes REAL non-empty legs from the real line data (proving the ∅ is the `IsActive` gate, not a dead verb) and `deriveInventory` reproduces the SAME "No Costs" refusal (0==0, not vacuous — a blessing-row flip in an in-memory copy opens the gate). `build/erp/generate_post_tail_oracle.log` · `build/erp/poc_post_tail.log`. **M_Production** stays untouched: 0 docs + no component costs → ⛔, costs not synthesized. | -**Remaining ⬜ declarative surfaces: NONE** (`ad_evaluator` fell to B-1 W-LOGIC-HARDEN, `ad_workflow` fell to B-2 W-WF-HARDEN, 2026-06-12; the 0-seed POSTING tail fell to B-3 W-POST-B3, 2026-07-17; the poster tail's seed-oracled half fell to W-POST-TAIL, 2026-07-18 — 17 of 20 factory posters now fold, Cash/Inventory next, Production ⛔). +**Remaining ⬜ declarative surfaces: NONE** (`ad_evaluator` fell to B-1 W-LOGIC-HARDEN, `ad_workflow` fell to B-2 W-WF-HARDEN, 2026-06-12; the 0-seed POSTING tail fell to B-3 W-POST-B3, 2026-07-17; the poster tail's seed-oracled half fell to W-POST-TAIL, 2026-07-18 — **17 of 20 factory posters fold, CLOSED (2026-07-18): 3 remain ⛔, each for a DIFFERENT, precisely-cited engine-level reason — C_Cash (both real docs IsActive=N), M_Inventory (2/3 no lines + the 1 completable doc has no cost data), M_Production (0 docs + no component costs). None worked around; none is a research gap left open — the ceiling is this seed's actual data state.**). **The honest read: coverage 37🟡 ≠ equivalence.** FIFTY-TWO surfaces/classes are now oracle-equivalent to the unit/cent (TB-read · per-document GL derivation H-1 · `completeIt` Order→Ship→Invoice chain · Doc_Payment · Doc_AllocationHdr diff --git a/prompts/HARDEN_MATRIX.md b/prompts/HARDEN_MATRIX.md index 440169ff4..6c22e36a5 100644 --- a/prompts/HARDEN_MATRIX.md +++ b/prompts/HARDEN_MATRIX.md @@ -137,6 +137,98 @@ conversion + 0.01 CurrencyBalancing residual) · `§TAIL-POST M_MatchPO docs=37 17/20 factory posters fold. Remaining: Cash (post 2 real CO docs on clone) · Inventory (complete 3 real drafts on clone) · Production ⛔ (no docs, no component costs). Log: `build/erp/poc_post_tail.log`. +### §W-POST-TAIL-2 manifest spec (2026-07-18, Sonnet session — written BEFORE coding, per Spec-First) +**C_Cash (407) — Doc_Cash.createFacts:150-249 (`Doc_Cash.java`), DocLine_Cash.java for the CashType +constants.** Facts (re-verified live): 2 real CO docs, posted='N' — `c_cash_id=100` 1 line (CashType=I +Invoice, amount=50.35, c_invoice_id=100) · `c_cash_id=101` 3 lines (E −10, T −50 bankaccount=100, R +10). +No Charge/Difference lines in this seed → those two branches are manifest-complete (source-parsed) but +UNEXERCISED by the real docs; do not claim them oracle-diffed. Per-line legs, header running `assetAmt`: +- **E (Expense, :174-181):** DR `{CashBook.CashExpense}`=amount.negate(); assetAmt −= amount.negate(). +- **R (Receipt, :182-189):** assetAmt += amount; CR `{CashBook.CashReceipt}`=amount (raw, not abs). +- **C (Charge, :190-197):** DR `line.getChargeAccount` (`c_charge_acct.ch_expense_acct`, C_Charge_ID/schema) + =amount.negate(); assetAmt −= amount.negate(). Charge_ID=0 on any real line ⇒ this branch never fires here. +- **D (Difference, :198-205):** DR `{CashBook.CashDifference}`=amount.negate(); assetAmt += amount. +- **I (Invoice, :206-219):** if line currency == cashbook currency (true, both 100/USD): assetAmt += amount, + no separate CashAsset line; ELSE DR `{CashBook.CashAsset}` at line currency. ALWAYS: CR `{CashBook.CashTransfer}` + =amount.negate(), at LINE currency (not doc currency). +- **T (Transfer, :220-236):** DR `{BankAccount.InTransit}` (`c_bankaccount_acct.b_intransit_acct`, keyed by + the LINE's own C_BankAccount_ID/schema, not the doc's) =amount.negate(), at line currency; if line + currency==cashbook currency: assetAmt += amount; else DR `{CashBook.CashAsset}` at line currency. +- **Header close (:239-243):** if assetAmt≠0, one more leg on `{CashBook.CashAsset}` (doc/cashbook + currency) = assetAmt (sign-dependent DR/CR, 4-arg `Fact.createLine`). +- **Accounts:** `c_cashbook_acct` columns CB_Asset_Acct/CB_CashTransfer_Acct/CB_Expense_Acct/ + CB_Receipt_Acct/CB_Differences_Acct keyed by (C_CashBook_ID=101, schema) — `Doc.java:1478-1502`. + Schema 200000 carries ONLY `cb_cashtransfer_acct` (200065) — asset/expense/receipt/differences are + NULL there; `Fact.createLine` silently DROPS a null-account line (`Fact.java:116-122`, not an error) — + so schema-200000 posts a partial (possibly source-imbalanced) leg set BY DESIGN, not a bug. `Doc.getAccount` + returns null on a 0-combination (`Doc.java:1602-1610`), same drop behaviour. +- **maxDiff=0c gate** over both real docs × both schemas × (account,side), same shape as BankStatement. + +**M_Inventory (321) — Doc_Inventory.createFacts:211-513 (`Doc_Inventory.java`), physical-inventory branch +only** (this seed's 3 docs are all DocSubTypeInv=PI, doctype 144). Facts (re-verified live): `m_inventory_id` +100 (DR client 11) has 1 real line (product 147 "TShirt - Red Large", locator 101→warehouse 103, qtycount=1/ +qtybook=0 → qtyDiff=+1, no charge, no ASI, not reversal) — the ONLY completable doc. **`m_inventory_id` +200000 and 200001 have ZERO lines each** (`m_inventoryline` count=0, verified live) → `MInventory.prepareIt` +(`MInventory.java:401-406`) fails BEFORE completion with `@NoLines@`, STATUS_Invalid — these 2 CANNOT +reach docstatus=CO, let alone post. This is the engine's own validation, not a workaround decision: +⛔ both BY NAME, do not attempt to seed lines onto them (that would be inventing source data on someone +else's document, banned regardless of the seed-prep ruling — the ruling covers NEW documents, not +patching existing ones). **1 of 3 is the honest ceiling for this seed.** +- Manifest for doc 100 (PI, :279-340): `costs = line.getProductCosts(as, orgId, true, "M_InventoryLine_ID=?")` + (`ProductCost.getProductCosts`, the schema-costingmethod → cost-element → `m_cost.currentcostprice` hop, + same as `deriveProjectIssue`'s `{Product}` cost lookup already in `doc_poster.js`). Product 147's `m_cost` + rows are ALL currentcostprice=0 (schema 101; NO rows at all for schema 200000) and `m_costdetail` has + ZERO rows for this product (verified live) — so the `costs==0` zero-cost-blessing check + (`Doc_Inventory.java:319-336`, requires a processed zero-cost purchase M_CostDetail row) FAILS → the + REAL engine's own `createFacts` returns `p_Error="No Costs for TShirt - Red Large"` UNLESS the scratch + run shows otherwise (to be confirmed empirically against the actual OSGi-driven posting — the source + reading above is the prediction, the scratch-clone run is the oracle; if posting truly fails this is + ALSO an honest ⛔-by-data-state, named with the engine's exact error, not invented around). +- IF posting succeeds (costs resolve non-zero via a path not visible from static reading — e.g. batch/lot + MA rows, or a costingLevel hop this trace missed): DR `{Product.Asset}` (service→`{Product.Expense}`, + `ProductCost.ACCTTYPE_P_Asset`, m_product_category_acct via product→category, same token already in + `post_resolver.js`) = costs; CR `line.getChargeAccount` if C_Charge_ID≠0 (0 here → null) ELSE + `M_Warehouse_Acct.W_Differences_Acct` keyed by (M_Warehouse_ID=103, schema) — `Doc.java:1505-1509`, + `Doc.ACCTTYPE_InvDifferences` — = costs.negate(). `M_Warehouse_ID` resolved from the line's locator + (`m_locator.m_warehouse_id`), not a doc column. +- Capture additive: `m_inventory`, `m_inventoryline`, `m_warehouse_acct`, `m_locator` (locator→warehouse), + `m_cost`, `m_costelement`, `m_costdetail` (for the zero-cost-blessing count), `m_product`, + `m_product_category_acct`, `c_validcombination`. + +Witness extension: new diff bands in `scripts/poc_post_tail.js` reading a NEW fixture +`build/erp/oracle/post_tail_fixture.json` (twin of `post_b3_fixture.json`, via a new +`scripts/logic_oracle/PostingTailTest.java` + `scripts/generate_post_tail_oracle.sh` + +`scripts/capture_post_tail_fixture.js` — same scratch-clone-drive-capture-drop machinery, reused not +reinvented), same maxDiff=0c per-doc/per-schema gate. `M_Production` ⛔ stays untouched (0 docs). + +**✅ §W-POST-TAIL-2 CLOSED 2026-07-18 (same session as the spec above) — empirically driven, NOT the +optimistic outcome the spec predicted.** Ran the REAL compiled posters on a fresh scratch clone +`idempiere_tail` over the 2 real CO cash journals + 3 real inventory drafts (zero seed authoring — every +document already existed). Ground truth (`§TAILORACLE`, `build/erp/generate_post_tail_oracle.log`): +**C_Cash — both docs (100/101) are `IsActive='N'`**, not merely "never posted" as first assumed; +`Doc.postIt`'s lock UPDATE (`Doc.java:591-605`, requires `IsActive='Y'`) never fires → +`DocManager.postDocument` returns `"CannotPostInactiveDocument"` for both. **M_Inventory — 200000/200001 +have ZERO lines each** → `MInventory.prepareIt` (`:401-406`) refuses with `@NoLines@` before completion; +**doc 100 (1 line, product 147) DOES complete** (DocStatus=Completed) but the REAL engine then refuses to +POST it — `"No Costs for TShirt - Red Large"` (`Doc_Inventory.java:319-336` — product 147 has +`currentcostprice=0` everywhere and ZERO `M_CostDetail` rows, so the zero-cost-blessing check fails). +**Neither blocker worked around** — flipping `IsActive` or seeding lines/costs onto an EXISTING document +is data mutation, the same out-of-scope boundary this very card already drew for Inventory's `@NoLines@` +docs; extending it to Cash's `IsActive` flag is the consistent, non-invented call, not a new judgment. +`doc_poster.js` still gained `deriveCash` (all 6 CashType legs, Doc_Cash.createFacts:150-249, fully +source-cited) and `deriveInventory` (physical-inventory branch, Doc_Inventory.createFacts:211-513) — +reusable for any FUTURE active/costed document — proven LIVE via falsifier since no oracle-diff is +possible here: `deriveCash` computes real non-empty legs from the real line data (§TAIL-FALSIFIER +cash-live, doc100 2 lines / doc101 4 lines — the ∅ is the `IsActive` gate, not a dead verb); +`deriveInventory` reproduces the SAME "No Costs" refusal (0==0, non-vacuous — §TAIL-FALSIFIER +inv-bless-flip: a synthetic zero-cost-blessing row in an in-memory copy opens the gate, 0→2 lines). +**Ledger STAYS 52 / 17 of 20 posters** — Cash and Inventory join Production as named ⛔, each for a +different, precisely-cited reason; none is a "next session" placeholder anymore. Whole bundle re-run +green (`poc_post_b3` · `poc_post_harden` · `poc_factacct_doc` · `poc_doc_poster` · `poc_morder_post` · +`poc_alloc_fx` · `poc_money_post` · `poc_matchinv_fx` · `poc_gljournal` · `poc_post_tail` · +`test_report_fin` all exit 0, TB 46574.97/300 intact). Logs: `build/erp/generate_post_tail_oracle.log` · +`build/erp/poc_post_tail.log`. + ### H-3 Spot-harden the declarative engines `ad_evaluator`/`ad_access`/`ad_valrule`/`ad_reference` are 🟡 on parse; oracle-diff a SAMPLE of each against `GridField`/`MRole`/`MValRule` outputs — confirm the verdict matches, not just that it parses. The master-data diff --git a/prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md b/prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md index 7e8e3d7de..ec3d6c86f 100644 --- a/prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md +++ b/prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md @@ -111,3 +111,54 @@ so the guide inherits credibility without turning into a test report: ## SESSION END - Every §A/§B item ✅ or ⛔-with-the-one-question. Update THIS file's DONE appendix (§-lined), PROGRESS.md, push everything (zero local-only commits), PR + merge per the git note at top. + +# DONE — 2026-07-18, Sonnet session (§A + §B both worked to zero). Every claim = a § line in +# `build/erp/generate_post_tail_oracle.log` / `build/erp/poc_post_tail.log` (exit 0, READ). + +- **§A-1/§A-2 CLOSED, empirically — NOT the outcome this card predicted.** Read `Doc_Cash.createFacts` + (Doc_Cash.java:150-249) and `Doc_Inventory.createFacts` (Doc_Inventory.java:211-513) first, wrote the + manifest spec into `HARDEN_MATRIX.md §W-POST-TAIL-2` before coding (per Spec-First), then built the + reusable machinery: `scripts/logic_oracle/PostingTailTest.java` (sibling of `PostingOracleTest.java`, + same vendor OSGi harness) + `scripts/generate_post_tail_oracle.sh` (twin of `generate_post_oracle.sh`, + scratch clone `idempiere_tail`) + `scripts/capture_post_tail_fixture.js` → `build/erp/oracle/ + post_tail_fixture.json`. Drove the REAL engine over the REAL 2 CO cash journals + 3 inventory drafts — + ZERO seed authoring, every document already existed. Result: **both classes are ∅-by-DATA-STATE**, + distinct from B-3's seed-generation and from MatchPO/Requisition's config-gates: `§TAILORACLE class= + C_Cash record_id=100/101 isactive=N` (BOTH real docs — `Doc.java:591-605`'s lock UPDATE requires + `IsActive='Y'`, `DocManager.postDocument` returns `"CannotPostInactiveDocument"`); `§TAILORACLE class= + M_Inventory record_id=200000/200001 processIt_ok=false processMsg=@NoLines@` (zero lines each, + `MInventory.java:401-406`); `§TAILORACLE class=M_Inventory record_id=100 ... postErr=Posting Error + (No Costs for TShirt - Red Large)` (the 1 completable draft — product 147 has `currentcostprice=0` + everywhere and 0 `M_CostDetail` rows, `Doc_Inventory.java:319-336`). **Neither blocker worked + around** — flipping `IsActive` or seeding lines/costs onto an EXISTING document is data mutation, the + same out-of-scope boundary this card itself drew for Inventory's `@NoLines@` docs (not a new judgment + call, the consistent application of it). `doc_poster.js` gained `deriveCash` + `deriveInventory` + anyway (full source-cited translations, reusable for any FUTURE active/costed document) and proved + them LIVE via falsifier since no oracle-diff is possible: `§TAIL-FALSIFIER cash-live doc100_lines=2 + doc101_lines=4` (the manifest computes real non-empty legs — the ∅ is the `IsActive` gate, not a dead + verb) and `§TAIL-FALSIFIER inv-bless-flip doc=100 before_lines=0 after_lines=2` (the manifest + reproduces the SAME "No Costs" refusal, 0==0 non-vacuously, and a synthetic blessing row opens it). + **Ledger STAYS 52 / 17-of-20 posters** (not 54 — the card's own optimistic headline was wrong; this is + the corrected, evidence-based number). Whole regression bundle re-run green: `poc_post_b3` · + `poc_post_harden` · `poc_factacct_doc` · `poc_doc_poster` · `poc_morder_post` · `poc_alloc_fx` · + `poc_money_post` · `poc_matchinv_fx` · `poc_gljournal` · `poc_post_tail` · `test_report_fin` all exit 0, + TB 46574.97/300 intact. Banked: `docs/internal/ERP_COVERAGE_MATRIX.md` (the W-POST-TAIL row extended + + the "17 of 20" summary line corrected), `prompts/HARDEN_MATRIX.md` (§W-POST-TAIL-2 CLOSED band), + `PROGRESS.md` (archive line + OPEN line updated). +- **§B-1/§B-2 DONE** — `docs/ERPUserGuide.md` gained **"The lay of the land"** (inserted after Quick + start: a mermaid map + prose walking front-door → Login → Pill Bar → Windows/Tabs/Fields → Process + Button → where documents live → where the books live, every claim pointing at an existing section) and + **"The standard flow — order to cash, procure to pay, books to reports"** (inserted after §6 Process + Button, before §7 POS, matching this guide's existing convention of unnumbered thematic sections + interleaved with the numbered core-manual sections — renumbering the 15 numbered sections was + considered and rejected as unnecessary blast radius for a doc-only change). All 8 steps told as one + continuous story over the GardenWorld demo data (Sales Order → Shipment → Invoice/AR → Receipt & + Allocation → Replenishment → PO/Receipt/Vendor-Invoice/Match → Final accounts → Financial reporting), + each citing its real witness (W-FOLD-COMPLETE/QTYONHAND/INOUTGL/PAYMENT/ALLOC/REPLENISH/MORDER-POST/ + AP-INVOICE/MATCHINV/GLJOURNAL/POST-TAIL/PA-REPORT) plus a one-paragraph "also in the books" note for + the B-3 fixed-asset/project classes. POS/Kitchen/WH/Tenancy/4D-5D/Ninja addon lenses untouched, per + scope. Verified: `mkdocs build --strict` clean (no new broken anchors/warnings vs. the pre-existing + HRBIMAssetGuide.md ones), mermaid fence syntax matches the one other file already using it + (`docs/internal/FoldEngineQuality.md`) — confirms the pattern is established, not novel. +- **Housekeeping:** this card's own DONE appendix is the record (per `feedback_prompt_file_organization` + rule 0 — a session working a `prompts/#.md` file updates only that file, not `MEMORY.md`). diff --git a/scripts/capture_post_tail_fixture.js b/scripts/capture_post_tail_fixture.js new file mode 100644 index 000000000..37a457aa3 --- /dev/null +++ b/scripts/capture_post_tail_fixture.js @@ -0,0 +1,160 @@ +// Copyright (c) 2025-2026 Redhuan D. Oon +// SPDX-License-Identifier: MIT +// ⚠ DO NOT REMOVE — Scope guard +// Scope: W-POST-TAIL §W-POST-TAIL-2 fixture capture (prompts/HARDEN_MATRIX.md). Twin of +// capture_post_b3_fixture.js: straight copy of the COMMITTED scratch DB (idempiere_tail) after +// scripts/logic_oracle/PostingTailTest.java drove the REAL compiled Doc_Cash/Doc_Inventory posters over +// REAL pre-existing documents (no seed authoring at all here — unlike B-3). NON-INVENT: every row below +// is read from the scratch PG; nothing is computed or authored here. +// Called by scripts/generate_post_tail_oracle.sh; standalone: node scripts/capture_post_tail_fixture.js +'use strict'; +var cp = require('child_process'); +var fs = require('fs'); +var path = require('path'); + +var DB = process.argv[2] || 'idempiere_tail'; +var OUT = process.argv[3] || path.join(__dirname, '..', 'build', 'erp', 'oracle', 'post_tail_fixture.json'); +var US = '\x1f'; + +function pgRows(sql) { + var out = cp.execFileSync('docker', ['exec', 'postgres', 'psql', '-U', 'adempiere', '-d', DB, + '-t', '-A', '-F', US, '-c', sql], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); + return out.split('\n').filter(function (s) { return s.length; }).map(function (l) { return l.split(US); }); +} + +// tail target ad_table_ids: C_Cash=407, M_Inventory=321 +var IDS = '407,321'; + +var CAPTURE = { + fact_acct: { + cols: ['fact_acct_id','ad_client_id','ad_org_id','c_acctschema_id','account_id','c_period_id', + 'ad_table_id','record_id','line_id','gl_category_id','c_tax_id','postingtype','c_currency_id', + 'amtsourcedr','amtsourcecr','amtacctdr','amtacctcr','qty','m_product_id','c_bpartner_id','description'], + sql: "SELECT fact_acct_id, ad_client_id, ad_org_id, c_acctschema_id, account_id, c_period_id," + + " ad_table_id, record_id, line_id, gl_category_id, c_tax_id, postingtype, c_currency_id," + + " round(amtsourcedr,2), round(amtsourcecr,2), round(amtacctdr,2), round(amtacctcr,2)," + + " round(qty,2), m_product_id, c_bpartner_id, replace(coalesce(description,''),chr(10),' ')" + + " FROM adempiere.fact_acct WHERE ad_table_id IN (" + IDS + ") ORDER BY fact_acct_id" + }, + c_cashbook: { + cols: ['c_cashbook_id','c_currency_id','ad_org_id'], + sql: "SELECT c_cashbook_id, c_currency_id, ad_org_id FROM adempiere.c_cashbook WHERE c_cashbook_id=101" + }, + c_cash: { + cols: ['c_cash_id','c_cashbook_id','statementdate','docstatus','posted','isactive','ad_org_id','ad_client_id'], + sql: "SELECT c_cash_id, c_cashbook_id, statementdate::date, docstatus, posted, isactive, ad_org_id, ad_client_id" + + " FROM adempiere.c_cash WHERE c_cash_id IN (100,101) ORDER BY c_cash_id" + }, + c_cashline: { + cols: ['c_cashline_id','c_cash_id','cashtype','c_currency_id','amount','c_bankaccount_id','c_invoice_id','c_charge_id'], + sql: "SELECT c_cashline_id, c_cash_id, cashtype, c_currency_id, round(amount,2)," + + " coalesce(c_bankaccount_id,0), coalesce(c_invoice_id,0), coalesce(c_charge_id,0)" + + " FROM adempiere.c_cashline WHERE c_cash_id IN (100,101) ORDER BY c_cashline_id" + }, + c_cashbook_acct: { + cols: ['c_cashbook_id','c_acctschema_id','cb_asset_acct','cb_cashtransfer_acct','cb_expense_acct','cb_receipt_acct','cb_differences_acct'], + sql: "SELECT c_cashbook_id, c_acctschema_id, coalesce(cb_asset_acct,0), coalesce(cb_cashtransfer_acct,0)," + + " coalesce(cb_expense_acct,0), coalesce(cb_receipt_acct,0), coalesce(cb_differences_acct,0)" + + " FROM adempiere.c_cashbook_acct WHERE c_cashbook_id=101 ORDER BY c_acctschema_id" + }, + c_bankaccount_acct: { + cols: ['c_bankaccount_id','c_acctschema_id','b_intransit_acct'], + sql: "SELECT c_bankaccount_id, c_acctschema_id, coalesce(b_intransit_acct,0)" + + " FROM adempiere.c_bankaccount_acct WHERE c_bankaccount_id=100 ORDER BY c_acctschema_id" + }, + c_charge_acct: { + cols: ['c_charge_id','c_acctschema_id','ch_expense_acct'], + sql: "SELECT c_charge_id, c_acctschema_id, coalesce(ch_expense_acct,0) FROM adempiere.c_charge_acct" + + " WHERE ad_client_id=11 ORDER BY c_charge_id, c_acctschema_id" + }, + m_inventory: { + cols: ['m_inventory_id','docstatus','posted','movementdate','m_warehouse_id','c_doctype_id','ad_org_id','ad_client_id'], + sql: "SELECT m_inventory_id, docstatus, posted, movementdate::date, m_warehouse_id, c_doctype_id, ad_org_id, ad_client_id" + + " FROM adempiere.m_inventory WHERE m_inventory_id IN (100,200000,200001) ORDER BY m_inventory_id" + }, + m_inventoryline: { + cols: ['m_inventoryline_id','m_inventory_id','m_product_id','m_locator_id','qtycount','qtybook', + 'qtyinternaluse','m_attributesetinstance_id','c_charge_id','isactive','reversalline_id'], + sql: "SELECT m_inventoryline_id, m_inventory_id, m_product_id, m_locator_id, round(qtycount,2), round(qtybook,2)," + + " round(coalesce(qtyinternaluse,0),2), coalesce(m_attributesetinstance_id,0), coalesce(c_charge_id,0)," + + " isactive, coalesce(reversalline_id,0)" + + " FROM adempiere.m_inventoryline WHERE m_inventory_id IN (100,200000,200001) ORDER BY m_inventoryline_id" + }, + c_doctype: { + cols: ['c_doctype_id','docsubtypeinv'], + sql: "SELECT c_doctype_id, coalesce(docsubtypeinv,'') FROM adempiere.c_doctype WHERE c_doctype_id=144" + }, + m_locator: { + cols: ['m_locator_id','m_warehouse_id'], + sql: "SELECT m_locator_id, m_warehouse_id FROM adempiere.m_locator WHERE m_locator_id=101" + }, + m_warehouse_acct: { + cols: ['m_warehouse_id','c_acctschema_id','w_differences_acct'], + sql: "SELECT m_warehouse_id, c_acctschema_id, coalesce(w_differences_acct,0) FROM adempiere.m_warehouse_acct" + + " WHERE m_warehouse_id=103 ORDER BY c_acctschema_id" + }, + m_product: { + cols: ['m_product_id','producttype','isstocked','m_product_category_id','name'], + sql: "SELECT m_product_id, producttype, isstocked, m_product_category_id, name FROM adempiere.m_product" + + " WHERE m_product_id=147" + }, + m_product_category_acct: { + cols: ['m_product_category_id','c_acctschema_id','p_asset_acct','p_expense_acct'], + sql: "SELECT m_product_category_id, c_acctschema_id, coalesce(p_asset_acct,0), coalesce(p_expense_acct,0)" + + " FROM adempiere.m_product_category_acct WHERE m_product_category_id=105 ORDER BY c_acctschema_id" + }, + m_cost: { + cols: ['m_product_id','c_acctschema_id','m_costtype_id','m_costelement_id','currentcostprice'], + sql: "SELECT m_product_id, c_acctschema_id, m_costtype_id, m_costelement_id, currentcostprice" + + " FROM adempiere.m_cost WHERE m_product_id=147 ORDER BY c_acctschema_id, m_costelement_id" + }, + m_costelement: { + cols: ['m_costelement_id','costingmethod','costelementtype'], + sql: "SELECT m_costelement_id, coalesce(costingmethod,''), costelementtype FROM adempiere.m_costelement" + + " WHERE ad_client_id IN (0,11) ORDER BY m_costelement_id" + }, + m_costdetail: { + cols: ['m_costdetail_id','m_product_id','processed','amt','qty','c_orderline_id','c_invoiceline_id'], + sql: "SELECT m_costdetail_id, m_product_id, processed, round(amt,2), round(qty,2)," + + " coalesce(c_orderline_id,0), coalesce(c_invoiceline_id,0)" + + " FROM adempiere.m_costdetail WHERE m_product_id=147 ORDER BY m_costdetail_id" + }, + c_acctschema: { + cols: ['c_acctschema_id','c_currency_id','costingmethod','m_costtype_id'], + sql: "SELECT c_acctschema_id, c_currency_id, costingmethod, m_costtype_id FROM adempiere.c_acctschema" + + " WHERE ad_client_id=11 ORDER BY c_acctschema_id" + }, + c_validcombination: { + cols: ['c_validcombination_id','account_id'], + sql: "SELECT c_validcombination_id, account_id FROM adempiere.c_validcombination WHERE ad_client_id IN (0,11)" + + " ORDER BY c_validcombination_id" + } +}; + +var fixture = { db: DB, tables: {} }; +Object.keys(CAPTURE).forEach(function (t) { + var spec = CAPTURE[t]; + var rows = pgRows(spec.sql); + rows.forEach(function (r) { + if (r.length !== spec.cols.length) { + throw new Error('capture ' + t + ': row width ' + r.length + ' != cols ' + spec.cols.length); + } + }); + fixture.tables[t] = { cols: spec.cols, rows: rows }; + console.log('§GEN-CAPTURE table=' + t + ' rows=' + rows.length); +}); + +var fa = fixture.tables.fact_acct.rows; +var dr = 0, cr = 0; +fa.forEach(function (r) { dr += Math.round(Number(r[15]) * 100); cr += Math.round(Number(r[16]) * 100); }); +console.log('§GEN-CAPTURE fact_acct tail-classes rows=' + fa.length + ' ΣDRc=' + dr + ' ΣCRc=' + cr); +// NOTE: unlike B-3, a 0-row fact_acct here is NOT automatically refused — the honest outcome could be +// "C_Cash posted, M_Inventory refused (No Costs)" i.e. partial. The witness (poc_post_tail.js) enforces +// the real per-class expectation; this capture just copies whatever the engine actually wrote. +if (fa.length === 0) { + console.log('§GEN-CAPTURE WARNING 0 fact_acct rows for tail classes — check §TAILORACLE log for postErr'); +} + +fs.mkdirSync(path.dirname(OUT), { recursive: true }); +fs.writeFileSync(OUT, JSON.stringify(fixture, null, 1)); +console.log('§GEN-CAPTURE fixture=' + OUT + ' bytes=' + fs.statSync(OUT).size); diff --git a/scripts/doc_poster.js b/scripts/doc_poster.js index a95b128b7..1714fcb15 100644 --- a/scripts/doc_poster.js +++ b/scripts/doc_poster.js @@ -411,6 +411,117 @@ function deriveRequisition(db, id, schema) { return d; } +// Doc_Cash.createFacts:150-249 (HARDEN_MATRIX.md §W-POST-TAIL-2) — per c_cashline CashType leg + the +// header running assetAmt close. NOT oracle-diffable in THIS seed: both real c_cash docs (100/101) carry +// IsActive='N' — Doc.postIt's lock UPDATE (Doc.java:591-605) requires IsActive='Y' before createFacts +// ever runs, so the REAL engine posts ZERO rows for these two (verified live: DocManager.postDocument → +// "CannotPostInactiveDocument"). This manifest is a faithful, source-cited translation (reusable for any +// FUTURE active C_Cash doc) — its role HERE is only the falsifier: it computes REAL non-empty legs from +// the real line data, proving the ∅ is an IsActive-gate fact (Doc.postIt, outside createFacts), not a +// dead/no-op verb or a manifest bug. NEVER invents: IsActive is read, never flipped, on the real rows. +function deriveCash(db, id, schema) { + var hdr = getRow(db, 'SELECT * FROM c_cash WHERE c_cash_id=?', num(id)); + if (!hdr) return null; + var cb = getRow(db, 'SELECT * FROM c_cashbook WHERE c_cashbook_id=?', num(hdr.c_cashbook_id)); + var cbAcct = getRow(db, 'SELECT * FROM c_cashbook_acct WHERE c_cashbook_id=? AND c_acctschema_id=?', [num(hdr.c_cashbook_id), num(schema)]); + var docCur = cb ? num(cb.c_currency_id) : null; + var d = b3New(); + if (!cbAcct) { d.absent.push('c_cashbook_acct#' + hdr.c_cashbook_id + '/' + schema); return d; } + var lines = allRows(db, 'SELECT * FROM c_cashline WHERE c_cash_id=? ORDER BY c_cashline_id', num(id)); + var assetAmt = 0; + function cbEl(col, token) { return elOf(db, vcAcct(db, cbAcct[col]), d.absent, token); } + lines.forEach(function (l) { + var amt = cents(l.amount); + var lineCur = num(l.c_currency_id); + if (l.cashtype === 'E') { // Expense :174-181 + var expEl = cbEl('cb_expense_acct', '{CashBook.CashExpense}'); + if (expEl) d.add('DR', expEl, -amt); + assetAmt -= -amt; + } else if (l.cashtype === 'R') { // Receipt :182-189 + assetAmt += amt; + var rcvEl = cbEl('cb_receipt_acct', '{CashBook.CashReceipt}'); + if (rcvEl) d.add('CR', rcvEl, amt); + } else if (l.cashtype === 'C') { // Charge :190-197 + var chg = getRow(db, 'SELECT ch_expense_acct AS acct FROM c_charge_acct WHERE c_charge_id=? AND c_acctschema_id=?', [num(l.c_charge_id), num(schema)]); + var chgEl = elOf(db, vcAcct(db, chg && chg.acct), d.absent, '{Charge.Expense}'); + if (chgEl) d.add('DR', chgEl, -amt); + assetAmt -= -amt; + } else if (l.cashtype === 'D') { // Difference :198-205 + var diffEl = cbEl('cb_differences_acct', '{CashBook.CashDifference}'); + if (diffEl) d.add('DR', diffEl, -amt); + assetAmt += amt; + } else if (l.cashtype === 'I') { // Invoice :206-219 + if (lineCur === docCur) assetAmt += amt; + else { var caEl = cbEl('cb_asset_acct', '{CashBook.CashAsset}'); if (caEl) d.add('DR', caEl, amt); } + var trEl = cbEl('cb_cashtransfer_acct', '{CashBook.CashTransfer}'); + if (trEl) d.add('CR', trEl, amt); // amount.negate() → CR |amt| + } else if (l.cashtype === 'T') { // Transfer :220-236 + var ba = getRow(db, 'SELECT * FROM c_bankaccount_acct WHERE c_bankaccount_id=? AND c_acctschema_id=?', [num(l.c_bankaccount_id), num(schema)]); + var itEl = elOf(db, vcAcct(db, ba && ba.b_intransit_acct), d.absent, '{BankAccount.InTransit}'); + if (itEl) d.add('DR', itEl, -amt); + if (lineCur === docCur) assetAmt += amt; + else { var caEl2 = cbEl('cb_asset_acct', '{CashBook.CashAsset}'); if (caEl2) d.add('DR', caEl2, amt); } + } + }); + if (assetAmt !== 0) { // header close :239-243 + var assetEl = cbEl('cb_asset_acct', '{CashBook.CashAsset}'); + if (assetEl) d.add(assetAmt > 0 ? 'DR' : 'CR', assetEl, Math.abs(assetAmt)); + } + return d; +} + +// Doc_Inventory.createFacts:211-513 (HARDEN_MATRIX.md §W-POST-TAIL-2), physical-inventory branch only +// (this seed's docs are all DocSubTypeInv=PI). costs = the schema-costingmethod → cost-element → +// m_cost.currentcostprice hop (same lookup as deriveProjectIssue); if costs resolves to 0 AND no +// qualifying zero-cost-blessing M_CostDetail row exists (Doc_Inventory.java:319-336: Processed='Y', +// Amt=0, Qty>0, from an order/invoice line), the REAL engine REFUSES the whole doc ("No Costs for +// ") — createFacts returns null, ZERO fact rows, NOT a partial post. Verified live: product 147 +// (doc 100's only line) has currentcostprice=0 everywhere and ZERO m_costdetail rows → the REAL engine +// refused doc 100 exactly this way (§TAILORACLE postErr="No Costs for TShirt - Red Large"). This manifest +// reproduces that SAME refusal (0==0, a genuine match, not a vacuous one) and the falsifier flips the +// blessing count to prove the gate — not the manifest — is what closes to ∅. +function deriveInventory(db, id, schema) { + var hdr = getRow(db, 'SELECT * FROM m_inventory WHERE m_inventory_id=?', num(id)); + if (!hdr) return null; + var lines = allRows(db, 'SELECT * FROM m_inventoryline WHERE m_inventory_id=? AND isactive=\'Y\'', num(id)); + var d = b3New(); + if (lines.length === 0) { d.absent.push('@NoLines@#' + id); return d; } // MInventory.prepareIt:401-406 + lines.forEach(function (l) { + var as = schemaRow(db, schema); + var cost = getRow(db, + "SELECT c.currentcostprice AS p FROM m_cost c JOIN m_costelement e ON e.m_costelement_id=c.m_costelement_id" + + " AND e.costelementtype='M' AND e.costingmethod=? WHERE c.m_product_id=? AND c.c_acctschema_id=? AND c.m_costtype_id=?", + [as ? String(as.costingmethod) : '', num(l.m_product_id), num(schema), as ? num(as.m_costtype_id) : 0]); + var qtyDiff = Number(l.qtycount) - Number(l.qtybook); // PI branch :164-165 + var costCents = cents(cost ? cost.p : 0); + if (costCents === 0) { + var bless = getRow(db, + "SELECT COUNT(*) AS n FROM m_costdetail WHERE m_product_id=? AND processed='Y' AND amt=0.00 AND qty>0" + + " AND (c_orderline_id>0 OR c_invoiceline_id>0)", num(l.m_product_id)); + if (!bless || Number(bless.n) === 0) { d.absent.push('{Product.NoCosts}#' + l.m_product_id); return; } // :332-335 refusal + } + var amt = Math.round(costCents * qtyDiff); + var prod = getRow(db, 'SELECT producttype, m_product_category_id FROM m_product WHERE m_product_id=?', num(l.m_product_id)); + var isService = prod && prod.producttype === 'S'; + var pcol = isService ? 'p_expense_acct' : 'p_asset_acct'; + var pacct = prod ? getRow(db, 'SELECT ' + pcol + ' AS acct FROM m_product_category_acct WHERE m_product_category_id=? AND c_acctschema_id=?', [num(prod.m_product_category_id), num(schema)]) : null; + var drEl = elOf(db, vcAcct(db, pacct && pacct.acct), d.absent, '{Product.' + (isService ? 'Expense' : 'Asset') + '}'); + if (drEl) d.add('DR', drEl, amt); + // CR: line.getChargeAccount if C_Charge_ID≠0, else M_Warehouse_Acct.W_Differences_Acct (:1505-1509) + if (num(l.c_charge_id) > 0) { + var chg = getRow(db, 'SELECT ch_expense_acct AS acct FROM c_charge_acct WHERE c_charge_id=? AND c_acctschema_id=?', [num(l.c_charge_id), num(schema)]); + var chgEl = elOf(db, vcAcct(db, chg && chg.acct), d.absent, '{Charge.Expense}'); + if (chgEl) d.add('CR', chgEl, amt); + } else { + var loc = getRow(db, 'SELECT m_warehouse_id FROM m_locator WHERE m_locator_id=?', num(l.m_locator_id)); + var wa = loc ? getRow(db, 'SELECT w_differences_acct AS acct FROM m_warehouse_acct WHERE m_warehouse_id=? AND c_acctschema_id=?', [num(loc.m_warehouse_id), num(schema)]) : null; + var crEl = elOf(db, vcAcct(db, wa && wa.acct), d.absent, '{Warehouse.Differences}'); + if (crEl) d.add('CR', crEl, amt); + } + }); + return d; +} + function finish(d, basis) { if (!d) return { lines: [], balanced: false, sumDr: 0, sumCr: 0, absent: [], basis: 'none' }; var lines = Object.keys(d.by).map(function (k) { @@ -450,6 +561,8 @@ function derivePostings(db, recordRef, schema, R) { if (table === 'C_BankStatement') return finish(deriveBankStatement(db, id, schema), 'bank-statement'); if (table === 'M_MatchPO') return finish(deriveMatchPO(db, id, schema), 'matchpo'); if (table === 'M_Requisition') return finish(deriveRequisition(db, id, schema), 'requisition'); + if (table === 'C_Cash') return finish(deriveCash(db, id, schema), 'cash'); + if (table === 'M_Inventory') return finish(deriveInventory(db, id, schema), 'inventory'); return { lines: [], balanced: false, sumDr: 0, sumCr: 0, absent: [], basis: 'none' }; } diff --git a/scripts/generate_post_tail_oracle.sh b/scripts/generate_post_tail_oracle.sh new file mode 100755 index 000000000..884727af9 --- /dev/null +++ b/scripts/generate_post_tail_oracle.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Copyright (c) 2025-2026 Redhuan D. Oon +# SPDX-License-Identifier: MIT +# ⚠ DO NOT REMOVE — Scope guard +# Scope: W-POST-TAIL §W-POST-TAIL-2 (prompts/HARDEN_MATRIX.md) — GENERATE the Cash/Inventory posting-tail +# oracle. Twin of scripts/generate_post_oracle.sh (W-POST-B3 §W-2): clone idempiere_test → SCRATCH +# idempiere_tail, drive the REAL compiled posters via the vendor's own OSGi test harness +# (scripts/logic_oracle/PostingTailTest.java), capture the committed fact_acct + source rows into the +# TEXT fixture build/erp/oracle/post_tail_fixture.json. UNLIKE B-3, this drives ZERO seed authoring — +# every document posted/completed here already existed in idempiere_test (2 CO c_cash journals never +# posted; 3 m_inventory drafts). The shared idempiere_test is NEVER written; scratch dropped at the end. +# READ THE LOG after every run: build/erp/generate_post_tail_oracle.log (exit ≠ evidence). +# Run: bash scripts/generate_post_tail_oracle.sh +set -euo pipefail +cd "$(dirname "$0")/.." + +CONTAINER=postgres +PGUSER=adempiere +SRC_DB=idempiere_test +SCRATCH_DB=idempiere_tail +IDMP="$HOME/idempiere-dev-setup/idempiere" +TAIL_HOME=/tmp/idempiere_tail_home +LOG=build/erp/generate_post_tail_oracle.log +FIXTURE=build/erp/oracle/post_tail_fixture.json +ORACLE_SRC=scripts/logic_oracle/PostingTailTest.java +ORACLE_DST="$IDMP/org.idempiere.test/src/org/idempiere/test/oracle/PostingTailTest.java" +# same workspace-bundle dep closure of org.idempiere.test as generate_post_oracle.sh (probed 2026-07-17): +MODULES="org.idempiere.p2.targetplatform,org.apache.ecs,org.adempiere.base,org.adempiere.base.callout,org.adempiere.base.process,org.adempiere.payment.processor,org.adempiere.ui,org.adempiere.ui.zk,org.adempiere.report.jasper,org.adempiere.report.jasper.library,org.idempiere.zk.billboard,org.idempiere.zk.extra,org.adempiere.server,org.idempiere.webservices,org.idempiere.webservices.resources,org.compiere.db.postgresql.provider,org.compiere.db.oracle.provider,org.adempiere.install,org.adempiere.replication,org.adempiere.pipo,org.adempiere.pipo.handlers,org.adempiere.plugin.utils,org.idempiere.hazelcast.service,org.idempiere.tablepartition,org.idempiere.test" + +mkdir -p build/erp/oracle +: > "$LOG" +say() { echo "$@" | tee -a "$LOG"; } + +say "== §W-POST-TAIL-2 generate: scratch clone $SCRATCH_DB from $SRC_DB ==" +docker exec "$CONTAINER" psql -U "$PGUSER" -d postgres -c \ + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IN ('$SRC_DB','$SCRATCH_DB') AND pid<>pg_backend_pid();" >>"$LOG" 2>&1 || true +docker exec "$CONTAINER" psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $SCRATCH_DB;" >>"$LOG" 2>&1 +docker exec "$CONTAINER" createdb -U "$PGUSER" -T "$SRC_DB" "$SCRATCH_DB" +say "§GEN scratch=$SCRATCH_DB cloned (template $SRC_DB)" + +say "== scratch IDEMPIERE_HOME at $TAIL_HOME (Connection → $SCRATCH_DB) ==" +mkdir -p "$TAIL_HOME/utils" "$TAIL_HOME/log" +sed "s/DBname\\\\=idempiere,/DBname\\\\=$SCRATCH_DB,/" "$IDMP/idempiere.properties" > "$TAIL_HOME/idempiere.properties" +grep -q "DBname\\\\=$SCRATCH_DB" "$TAIL_HOME/idempiere.properties" || { say "§GEN FATAL properties patch failed"; exit 1; } +cp "$IDMP/org.adempiere.server-feature/utils.unix/getVar.sh" "$TAIL_HOME/utils/" +chmod +x "$TAIL_HOME/utils/getVar.sh" +cp "$IDMP/.idpass" "$TAIL_HOME/.idpass" +sed "s/^ADEMPIERE_DB_NAME=.*/ADEMPIERE_DB_NAME=$SCRATCH_DB/" "$IDMP/idempiereEnv.properties" > "$TAIL_HOME/idempiereEnv.properties" + +say "== place PostingTailTest into the vendor harness (removed again at the end) ==" +mkdir -p "$(dirname "$ORACLE_DST")" +cp "$ORACLE_SRC" "$ORACLE_DST" + +say "== drive the REAL compiled posters (tycho-surefire OSGi, scratch DB) — long step ==" +set +e +( cd "$IDMP" && ./mvnw verify -pl "$MODULES" -DskipTests=false \ + -Didempiere.home="$TAIL_HOME" -Dtest=PostingTailTest ) >>"$LOG" 2>&1 +MVN_EXIT=$? +set -e +rm -f "$ORACLE_DST"; rmdir "$(dirname "$ORACLE_DST")" 2>/dev/null || true +grep '§TAILORACLE' "$LOG" | tee build/erp/oracle/post_tail_oracle_run.log || true +if [ $MVN_EXIT -ne 0 ]; then + say "§GEN FATAL mvn exit=$MVN_EXIT — READ $LOG (posting did not complete; scratch kept for autopsy)" + exit 1 +fi +say "§GEN posting run green (mvn exit 0) — capturing fixture" + +say "== capture fixture (scoped to C_Cash/M_Inventory) ==" +node scripts/capture_post_tail_fixture.js "$SCRATCH_DB" "$FIXTURE" | tee -a "$LOG" + +say "== drop scratch ==" +docker exec "$CONTAINER" psql -U "$PGUSER" -d postgres -c \ + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$SCRATCH_DB' AND pid<>pg_backend_pid();" >>"$LOG" 2>&1 || true +docker exec "$CONTAINER" psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $SCRATCH_DB;" >>"$LOG" 2>&1 +say "§GEN scratch dropped; fixture at $FIXTURE — now run: bash build/erp/run_witness.sh scripts/poc_post_tail.js" diff --git a/scripts/logic_oracle/PostingTailTest.java b/scripts/logic_oracle/PostingTailTest.java new file mode 100644 index 000000000..1fed2e18d --- /dev/null +++ b/scripts/logic_oracle/PostingTailTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025-2026 Redhuan D. Oon + * SPDX-License-Identifier: MIT + * + * PostingTailTest.java — ORACLE side of W-POST-TAIL §W-POST-TAIL-2 (prompts/HARDEN_MATRIX.md). + * Sibling of PostingOracleTest.java (W-POST-B3): same vendor OSGi test harness, same scratch-clone + * drive-capture-drop machinery (scripts/generate_post_tail_oracle.sh), but ZERO seed authoring — every + * document driven here is a REAL row that already existed in idempiere_test (2 CO c_cash journals never + * posted; 3 m_inventory drafts, of which 2 have ZERO lines and cannot even complete). This test posts/ + * completes the REAL rows, nothing more. + * + * TARGET DB: the SCRATCH clone idempiere_tail (createdb -T idempiere_test), never idempiere_test itself. + * COMMITS into the scratch clone only (AbstractTestCase default is rollback). + * + * C_CASH (407): c_cash_id=100 (1 line, CashType=Invoice) and c_cash_id=101 (3 lines: Expense/Transfer/ + * Receipt) are ALREADY DocStatus=CO, Posted=N — no processIt(Complete) call (that transition is invalid + * from an already-completed document); just drive DocManager.postDocument directly, the same real + * posting entrypoint postImmediate/AcctProcessor call. + * + * M_INVENTORY (321): m_inventory_id=100 has 1 real line (product 147, qtycount=1/qtybook=0) and is the + * only completable draft. m_inventory_id=200000/200001 have ZERO lines each (verified live 2026-07-18) — + * MInventory.prepareIt (MInventory.java:401-406) rejects 0-line docs with "@NoLines@" BEFORE completion. + * This test drives processIt(Complete) on all 3 and expects exactly that outcome: 100 completes (posting + * outcome logged honestly either way — cost data for product 147 is 0 everywhere in this seed, so the + * REAL engine may itself refuse to post with "No Costs for ..." per Doc_Inventory.java:319-336; that is + * an honest finding, not a test bug), 200000/200001 fail with the engine's own @NoLines@ error. NON-INVENT: + * nothing is seeded onto the 0-line drafts to make them completable — that would be authoring source data + * on someone else's existing document, out of scope for the seed-prep ruling (which covers NEW documents). + */ +package org.idempiere.test.oracle; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.Properties; + +import org.compiere.acct.DocManager; +import org.compiere.model.MAcctSchema; +import org.compiere.model.MCash; +import org.compiere.model.MInventory; +import org.compiere.process.DocAction; +import org.compiere.util.DB; +import org.compiere.util.Env; +import org.idempiere.test.AbstractTestCase; +import org.junit.jupiter.api.Test; + +public class PostingTailTest extends AbstractTestCase { + + private void say(String s) { System.out.println("§TAILORACLE " + s); } + + @Test + public void tailPostingOracle() { + Properties ctx = Env.getCtx(); + String trxName = getTrxName(); + + String dbName = DB.getSQLValueStringEx(trxName, "SELECT current_database()"); + say("db=" + dbName); + assertEquals("idempiere_tail", dbName, "PostingTailTest must run on the scratch clone idempiere_tail"); + + // ── C_Cash: post the 2 REAL already-CO journals directly (no re-complete) ────────────────── + postExistingCash(ctx, 100, trxName); + postExistingCash(ctx, 101, trxName); + + // ── M_Inventory: drive processIt(CO) on all 3 real docs; 100 is the only completable one ── + tryCompleteInventory(ctx, 100, trxName, false); + tryCompleteInventory(ctx, 200000, trxName, true); + tryCompleteInventory(ctx, 200001, trxName, true); + + // ── fact tallies from inside the trx (external capture re-reads post-commit) ─────────────── + int cashFacts = DB.getSQLValueEx(trxName, "SELECT COUNT(*) FROM Fact_Acct WHERE AD_Table_ID=407"); + int invFacts = DB.getSQLValueEx(trxName, "SELECT COUNT(*) FROM Fact_Acct WHERE AD_Table_ID=321"); + say("fact_count table=C_Cash ad_table_id=407 rows=" + cashFacts); + say("fact_count table=M_Inventory ad_table_id=321 rows=" + invFacts); + + commit(); + say("committed=Y db=" + dbName); + } + + /** Post an already-CO MCash doc via the real posting entrypoint (no re-complete). */ + private void postExistingCash(Properties ctx, int id, String trxName) { + MCash cash = new MCash(ctx, id, trxName); + assertEquals(id, cash.get_ID(), "MCash " + id + " did not load"); + String preStatus = cash.getDocStatus(); + assertEquals("CO", preStatus, "MCash " + id + " expected pre-existing DocStatus=CO"); + + // Doc.postIt's lock UPDATE (Doc.java:591-605) requires Processed='Y' AND IsActive='Y' — if the + // row itself is IsActive='N' (verified live 2026-07-18: BOTH c_cash 100/101 carry IsActive='N' + // in this seed), the lock never fires and DocManager returns "CannotPostInactiveDocument". This + // is an engine-enforced block on the row's own data state — the SAME category as M_Inventory's + // @NoLines@ below: NOT worked around (flipping IsActive would be mutating someone else's + // document, out of the seed-prep ruling's scope), named honestly and skipped. + if (!cash.isActive()) { + say("class=C_Cash record_id=" + id + " isactive=N — SKIPPED by design (CannotPostInactiveDocument gate, Doc.java:605)"); + return; + } + + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(ctx, cash.getAD_Client_ID()); + String postErr = DocManager.postDocument(ass, MCash.Table_ID, id, true, true, trxName); + cash.load(trxName); + Object rawPosted = cash.get_Value("Posted"); + boolean posted = "Y".equals(rawPosted) || Boolean.TRUE.equals(rawPosted); + say("class=C_Cash record_id=" + id + " docstatus=" + cash.getDocStatus() + + " posted=" + (posted ? "Y" : "N") + " rawPosted=" + rawPosted + + (postErr != null ? " postErr=" + postErr : "")); + assertTrue(posted, "MCash " + id + " not posted" + (postErr != null ? " — " + postErr : "")); + commit(); // scratch clone: null-trx readers must see this before the external capture + } + + /** + * Drive processIt(Complete) on an MInventory draft. expectNoLines=true asserts the engine's own + * @NoLines@ rejection (0-line doc, cannot complete); otherwise asserts completion succeeded and + * attempts the real posting entrypoint, logging the outcome honestly either way (a "No Costs" + * refusal from Doc_Inventory.createFacts is a valid, named, non-invented result here). + */ + private void tryCompleteInventory(Properties ctx, int id, String trxName, boolean expectNoLines) { + MInventory inv = new MInventory(ctx, id, trxName); + assertEquals(id, inv.get_ID(), "MInventory " + id + " did not load"); + DocAction doc = (DocAction) inv; + boolean ok; + String threw = null; + try { ok = doc.processIt(DocAction.ACTION_Complete); } + catch (Exception e) { ok = false; threw = e.getMessage(); } + inv.saveEx(); + inv.load(trxName); + String docStatus = inv.getDocStatus(); + String procMsg = doc.getProcessMsg(); + say("class=M_Inventory record_id=" + id + " processIt_ok=" + ok + " docstatus=" + docStatus + + " processMsg=" + procMsg + (threw != null ? " threw=" + threw : "")); + + if (expectNoLines) { + assertFalse(ok, "MInventory " + id + " expected to fail (@NoLines@) but processIt returned true"); + assertTrue(procMsg != null && procMsg.contains("NoLines"), + "MInventory " + id + " expected @NoLines@, got: " + procMsg); + return; // by design: no posting attempt on a doc that never completed + } + + assertTrue(ok, "MInventory " + id + " processIt(CO) failed: " + procMsg); + assertEquals(DocAction.STATUS_Completed, docStatus, "MInventory " + id + " not completed"); + commit(); + + Object rawPosted = inv.get_Value("Posted"); + boolean posted = "Y".equals(rawPosted) || Boolean.TRUE.equals(rawPosted); + String postErr = null; + if (!posted) { + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(ctx, inv.getAD_Client_ID()); + postErr = DocManager.postDocument(ass, MInventory.Table_ID, id, true, true, trxName); + inv.load(trxName); + rawPosted = inv.get_Value("Posted"); + posted = "Y".equals(rawPosted) || Boolean.TRUE.equals(rawPosted); + } + say("class=M_Inventory record_id=" + id + " posted=" + (posted ? "Y" : "N") + " rawPosted=" + rawPosted + + (postErr != null ? " postErr=" + postErr : "") + " (an honest post-refusal here is a named result, not a test failure)"); + commit(); + } +} diff --git a/scripts/poc_post_tail.js b/scripts/poc_post_tail.js index 7facf11f8..c2b26011a 100644 --- a/scripts/poc_post_tail.js +++ b/scripts/poc_post_tail.js @@ -99,7 +99,76 @@ console.log('§TAIL-POST class=M_Requisition docs=' + reqs.length + ' oracle_row verdict(reqs.length === 1 && reqFacts === 0 && reqNonEmpty === 0, 'M_Requisition: engine ∅ == the REAL engine\'s ∅ (reservation gate closed)'); -// ── 4. §FALSIFIERS (load-bearing) on a THROWAWAY copy ────────────────────────────────────────────── +// ── 4. C_Cash (407) / M_Inventory (321) — REAL scratch-clone run (generate_post_tail_oracle.sh): +// both classes' real documents hit ENGINE-ENFORCED data-state blockers, distinct from B-3's seed +// generation and from MatchPO/Requisition's CONFIG gates. NON-INVENT: neither blocker is worked +// around (flipping IsActive or seeding lines onto an existing doc would be mutating someone else's +// document, out of the seed-prep ruling's scope — same boundary the card itself draws for Inventory). +console.log('\n── C_Cash (407) / M_Inventory (321) — REAL engine run, both ∅-by-data-state ──'); +var fs = require('fs'); +var TAIL_FIXTURE = path.join(__dirname, '..', 'build', 'erp', 'oracle', 'post_tail_fixture.json'); +if (!fs.existsSync(TAIL_FIXTURE)) { + console.log('§TAIL-SKIP tail fixture absent (' + TAIL_FIXTURE + ') — run: bash scripts/generate_post_tail_oracle.sh'); + fails++; +} else { + var tfx = JSON.parse(fs.readFileSync(TAIL_FIXTURE, 'utf8')); + var tdb = new Database(':memory:'); + Object.keys(tfx.tables).forEach(function (t) { + var spec = tfx.tables[t]; + tdb.prepare('CREATE TABLE ' + t + ' (' + spec.cols.join(',') + ')').run(); + var ins = tdb.prepare('INSERT INTO ' + t + ' VALUES (' + spec.cols.map(function () { return '?'; }).join(',') + ')'); + spec.rows.forEach(function (r) { ins.run(r.map(function (v) { return /^-?\d+(\.\d+)?$/.test(v) ? Number(v) : v; })); }); + }); + + // §TAIL-POST2 re-assert: the REAL engine (scratch-clone, generate_post_tail_oracle.sh) wrote ZERO + // fact_acct rows for either class — read from the captured oracle, not assumed. + var cashOracleRows = tdb.prepare('SELECT count(*) c FROM fact_acct WHERE ad_table_id=407').get().c; + var invOracleRows = tdb.prepare('SELECT count(*) c FROM fact_acct WHERE ad_table_id=321').get().c; + + // C_Cash: both real docs are IsActive='N' (re-read from the captured oracle, not the Java log alone) + var cashRows = tdb.prepare('SELECT c_cash_id, isactive FROM c_cash ORDER BY c_cash_id').all(); + var cashInactive = cashRows.filter(function (r) { return r.isactive === 'N'; }); + console.log('§TAIL-POST2 C_Cash docs=' + cashRows.length + ' isactive_N=' + cashInactive.length + + ' oracle_rows=' + cashOracleRows + ' basis=∅-by-data-state (Doc.java:591-605 CannotPostInactiveDocument)'); + verdict(cashRows.length === 2 && cashInactive.length === 2 && cashOracleRows === 0, + 'C_Cash: both real CO docs are IsActive=N → the REAL engine\'s lock-UPDATE never fires, ∅ oracle confirmed', + 'docs=' + cashRows.map(function (r) { return r.c_cash_id; }).join(',')); + // falsifier: the manifest itself is LIVE — it computes REAL non-empty legs from the real line data, + // proving the ∅ is the IsActive gate (outside createFacts), not a dead/no-op verb or a manifest bug. + var cashLive0 = DP.derivePostings(tdb, { table: 'C_Cash', id: 100 }, 101); + var cashLive1 = DP.derivePostings(tdb, { table: 'C_Cash', id: 101 }, 101); + var cashLiveOk = cashLive0.lines.length > 0 && cashLive1.lines.length > 0; + verdict(cashLiveOk, '§FALSIFIER C_Cash manifest is LIVE (non-empty legs from the real line data) — the ∅ is IsActive-gate-derived, not dead', + 'doc100 lines=' + cashLive0.lines.length + ' doc101 lines=' + cashLive1.lines.length); + console.log('§TAIL-FALSIFIER cash-live doc100_lines=' + cashLive0.lines.length + ' doc101_lines=' + cashLive1.lines.length + ' (must be >0)'); + + // M_Inventory: 2/3 never even complete (@NoLines@); the 1 completable doc (100) hits the REAL + // engine's own "No Costs" refusal (Doc_Inventory.java:319-336) — product 147 has zero cost data. + var invNoLines = tdb.prepare('SELECT m_inventory_id FROM m_inventory WHERE m_inventory_id IN (200000,200001)').all(); + var inv100Lines = tdb.prepare('SELECT count(*) c FROM m_inventoryline WHERE m_inventory_id=100').get().c; + var costRow = tdb.prepare("SELECT currentcostprice FROM m_cost WHERE m_product_id=147 AND currentcostprice<>0").all(); + var blessRows = tdb.prepare("SELECT count(*) c FROM m_costdetail WHERE m_product_id=147 AND processed='Y' AND amt=0.00 AND qty>0 AND (c_orderline_id>0 OR c_invoiceline_id>0)").get().c; + console.log('§TAIL-POST2 M_Inventory noLines_docs=' + invNoLines.length + ' doc100_lines=' + inv100Lines + + ' nonzero_cost_rows=' + costRow.length + ' zero_cost_blessing_rows=' + blessRows + + ' oracle_rows=' + invOracleRows + ' basis=∅-by-data-state (MInventory.java:401-406 @NoLines@ + Doc_Inventory.java:319-336 No-Costs)'); + verdict(invNoLines.length === 2 && inv100Lines === 1 && costRow.length === 0 && blessRows === 0 && invOracleRows === 0, + 'M_Inventory: 2 docs cannot even complete (@NoLines@), the 1 completable doc has zero cost data anywhere → ∅ oracle confirmed', + 'noLines=' + invNoLines.map(function (r) { return r.m_inventory_id; }).join(',')); + // falsifier: the manifest matches the SAME "No Costs" refusal (0==0, not vacuous — flip the blessing + // count in an in-memory copy and the gate opens, a real DR/CR pair appears). + var invReal = DP.derivePostings(tdb, { table: 'M_Inventory', id: 100 }, 101); + var invRealOk = invReal.lines.length === 0 && invReal.absent.some(function (a) { return String(a).indexOf('NoCosts') >= 0; }); + verdict(invRealOk, 'M_Inventory doc 100: manifest ALSO refuses (0 lines, {Product.NoCosts} named absent) — matches the real engine\'s refusal exactly', + 'lines=' + invReal.lines.length + ' absent=[' + invReal.absent.join(',') + ']'); + tdb.prepare("INSERT INTO m_costdetail (m_costdetail_id,m_product_id,processed,amt,qty,c_orderline_id,c_invoiceline_id) VALUES (999,147,'Y',0.00,1,500,0)").run(); + var invFlip = DP.derivePostings(tdb, { table: 'M_Inventory', id: 100 }, 101); + var invFlipOk = invFlip.lines.length > 0 || invFlip.absent.some(function (a) { return String(a).indexOf('NoCosts') < 0; }); + verdict(invFlipOk, '§FALSIFIER M_Inventory blessing-flip: adding a qualifying zero-cost M_CostDetail row opens the gate (0 → a real DR/CR pair at cost=0)', + 'lines=' + invFlip.lines.length + ' absent=[' + invFlip.absent.join(',') + ']'); + console.log('§TAIL-FALSIFIER inv-bless-flip doc=100 before_lines=' + invReal.lines.length + ' after_lines=' + invFlip.lines.length + ' (must open)'); +} + +// ── 5. §FALSIFIERS (load-bearing) on a THROWAWAY copy ────────────────────────────────────────────── console.log('\n── §FALSIFIERS (in-memory copy; the oracle db stays read-only) ──'); var mem = new Database(':memory:'); ['c_acctschema', 'c_acctschema_gl', 'c_bankstatement', 'c_bankstatementline', 'c_bankaccount_acct', @@ -150,14 +219,25 @@ verdict(gotDr !== wantDr, '§FALSIFIER scale-one-line: doubling stmtamt breaks t console.log('§TAIL-FALSIFIER bs-scale line=100 oracleΣDR=' + wantDr + 'c scaledΣDR=' + gotDr + 'c (must differ)'); mem.close(); -// ── 5. named skips (the honest remainder) ────────────────────────────────────────────────────────── -console.log('\n§TAIL-SKIPS C_Cash (407): 2 real CO docs NEVER posted (posted=N, 0 facts) → post them on a scratch'); -console.log('§TAIL-SKIPS clone via the B-3 generator (zero seed prep) — NEXT session, not claimed here.'); -console.log('§TAIL-SKIPS M_Inventory (321): 3 real DRAFTS → complete+post on the clone — NEXT session.'); +// ── 6. named ⛔ (the honest remainder, all empirically driven on a scratch clone — nothing assumed) ── +console.log('\n§TAIL-SKIPS C_Cash (407): drove the REAL engine on scratch idempiere_tail (generate_post_tail_oracle.sh)'); +console.log('§TAIL-SKIPS over the 2 real CO docs — BOTH are IsActive=N, DocManager.postDocument refuses'); +console.log('§TAIL-SKIPS ("CannotPostInactiveDocument", Doc.java:591-605) → ⛔ BY THE ENGINE\'S OWN ERROR.'); +console.log('§TAIL-SKIPS NOT worked around: flipping IsActive on an existing document is out of the'); +console.log('§TAIL-SKIPS seed-prep ruling\'s scope (same boundary as the Inventory @NoLines@ docs below).'); +console.log('§TAIL-SKIPS M_Inventory (321): drove processIt(CO) on all 3 real drafts on the same scratch —'); +console.log('§TAIL-SKIPS 200000/200001 have ZERO lines each, MInventory.prepareIt refuses BEFORE completion'); +console.log('§TAIL-SKIPS (@NoLines@, MInventory.java:401-406); doc 100 (1 line, product 147) DOES complete'); +console.log('§TAIL-SKIPS but the REAL engine then REFUSES to post it ("No Costs for TShirt - Red Large",'); +console.log('§TAIL-SKIPS Doc_Inventory.java:319-336 — product 147 has currentcostprice=0 everywhere and'); +console.log('§TAIL-SKIPS zero M_CostDetail rows, so the zero-cost-blessing check fails) → ⛔, 0/3 posted.'); console.log('§TAIL-SKIPS M_Production (325): 0 docs + component m_cost absent (W-FOLD-PRODUCTION deferral) → ⛔'); console.log('§TAIL-SKIPS until a costed BOM seed exists; costs are NOT synthesized.'); +console.log('§TAIL-SKIPS Matrix ledger stays 52 oracle-equivalent / 17-of-20 posters — Cash+Inventory join'); +console.log('§TAIL-SKIPS Production as named ⛔, each for a DIFFERENT, precisely-cited engine-level reason.'); db.close(); console.log('\n' + (fails === 0 ? '🟢 W-POST-TAIL PASS' : '🔴 W-POST-TAIL FAIL (' + fails + ')') + - ' — BankStatement folds the REAL journal to the cent; MatchPO(37)+Requisition ∅==∅ config-derived; 3 named skips.'); + ' — BankStatement folds the REAL journal to the cent; MatchPO(37)+Requisition ∅==∅ config-derived;' + + ' Cash+Inventory ∅==∅ data-state-derived (REAL scratch-clone run); 3 named ⛔ (Cash/Inventory/Production).'); process.exit(fails === 0 ? 0 : 1);