NG Bailey / CVR / Issue 15

[BI].[Commitment_Order]

Fundamental change, 10 Sep 2026: posted only is now enforced. The transaction table holds matched-but-unposted batch rows (DET_BATCH_FLAG = 1; ~£2.2m in flight when found). Counting them released commitment at entry while actuals recognise at posting, leaving the value in neither bucket, the vanishing pound the definition forbids. Both views now filter DET_BATCH_FLAG = 0: commitment releases only at posting, the same moment the cost becomes actual.

How we proved the flag means unposted: the 693 flag 1 rows cluster in the last two months (a rolling posting pipeline shape) with ~29 stragglers destined for accounting periods closed since 2007–2016 (abandoned batches that will never post); nominal periods are assigned at entry so could not distinguish posted from unposted; Focalpoint's own TS_NL_TRANSACTION_DETAIL_SIMPLE_VIEW confirms the batch flag semantics, and its only special case (recurring journal masters) was ruled out: every flag 1 PL invoice has DET_RECUR_FLAG = 0. Passive confirmation pending: the EA Technology invoice on order 920582 is flag 1 today and should flip to 0 when its batch posts. Side effect of the fix: the abandoned batch rows the old report has counted as real invoices for up to nineteen years are now permanently excluded.

Purpose

Grain: one row per Purchase Order. This view is the single source of truth for commitment, implementing the agreed definition: Commitment = Total Purchase Order Value − Total Posted Invoices against that Purchase Order (net of credits). It replaces the calculation inside the ProjectCommitments SSRS report and will feed the CVR (issue 18) and any summary reporting. The line level companion [BI].[Commitment_OrderLine] is presentation over the same logic; each order's rows there sum exactly to this view's figure.

Views cannot hold an ORDER BY or a parameter, so consumers filter WHERE [ProjectCode] = @CH_CODE and sort themselves. Resolves tracker issues 15, 24 and 33.

What Was Wrong Before

The old ProjectCommitments report (SSRS, http://sql-reports/Reports/report/Freedom/FreedomFinance/SSRS Reports/ProjectCommitments) calculated per Purchase Order line:

CASE WHEN POD_QTYORD = POD_QTYINVD THEN 0
     ELSE ISNULL(POD_NETT, 0) - ISNULL(SumOfDET_NETT, 0) END

Four changes were agreed to make it match the definition:

  1. Remove the quantity test. The report set commitment to zero when ordered and invoiced quantities matched, even if the values did not, which hid over-invoices.
  2. Calculate per whole order, not per line. Line level matching inherits Focalpoint's auto-split errors; order level totals are immune to them.
  3. Correct credit note handling in the category columns. Credits were netted correctly in the total but added as positives in the Labour, Plant, Materials and Subcontract columns.
  4. Every published copy starts at the Purchase Order. Older copies started from Purchase Requests, whose links to Purchase Orders are unreliable (confirmed live: the first comparison run used a PR based copy). Check every published copy and retire any still doing so.

Design Decisions

DecisionRationale
Values only, never quantitiesCommitment is money, not units. Focalpoint's quantities are unreliable (lump sum lines, auto-splits). There is no quantity reference anywhere in the view.
Whole orderOrder totals survive Focalpoint's auto-split corruption; line values do not. Lines are summed to one order value, everything posted against the order is summed, one subtraction.
Posted only, credits netted at sourceOnly posted ledger transactions (DET_LEDGER = 'PL', types INV/CRN, DET_BATCH_FLAG = 0) discharge commitment. Matching and posting are separate moments in Focalpoint: matched rows can sit in unposted batches, and those stay in commitment until they post (the 10 Sep 2026 fundamental change). The register is invisible by design. The CRN sign flip happens once in the posted aggregation, so every downstream column inherits correct credit handling.
Floor at zero, keep the truthCommitment never goes negative; the true value stays in Commitment_Raw and OverInvoiced = Y where posted exceeds ordered. Over-invoiced orders are flagged, never allowed to quietly reduce the project total.
Categories by apportionmentCommitment_Labour/Plant/Materials/Subcontract are apportioned from the order's own cost centre mix, so they always sum to the total. The PostedValue_* columns are categorised by the invoice's cost centre and can legitimately disagree with the order's mix; they report what was invoiced as Labour, not what Labour commitment was released. NULLIF guards the division on zero value orders.
ROUND(…, 2) on all moneyPOD_NETT is stored as float; unrounded output produced values like 7170.19999999999.
Status filter pendingPlaceholder awaiting definition question 5: which Purchase Order statuses release commitment. Becomes a WHERE clause when Finance answer.

The View

As documented before deployment (the deployed view in the BI schema is authoritative; project filter and ORDER BY removed, ROUND to be applied throughout as per the rounding decision):

;WITH [Orders] AS
(
    -- Grain: one row per Purchase Order. Sum the lines, so auto-split errors cancel out
    SELECT
         POD.[POD_COSTHEADER]        AS [ProjectCode]
        ,POD.[POD_ACCOUNT]           AS [SupplierCode]
        ,POD.[POD_ORDER_NO]          AS [OrderNumber]
        ,SUM(ISNULL(POD.[POD_NETT], 0))  AS [OrderValue]
        ,SUM(CASE WHEN POD.[POD_COSTCENTRE] LIKE 'LAB%' THEN ISNULL(POD.[POD_NETT], 0) ELSE 0 END) AS [OrderValue_Labour]
        ,SUM(CASE WHEN POD.[POD_COSTCENTRE] LIKE 'PLA%' THEN ISNULL(POD.[POD_NETT], 0) ELSE 0 END) AS [OrderValue_Plant]
        ,SUM(CASE WHEN POD.[POD_COSTCENTRE] LIKE 'MAT%' THEN ISNULL(POD.[POD_NETT], 0) ELSE 0 END) AS [OrderValue_Materials]
        ,SUM(CASE WHEN POD.[POD_COSTCENTRE] LIKE 'SUB%' THEN ISNULL(POD.[POD_NETT], 0) ELSE 0 END) AS [OrderValue_Subcontract]

    FROM [POP_DETAIL] POD
    -- Pending definition question 5: exclude/flag statuses that release commitment

    GROUP BY POD.[POD_COSTHEADER], POD.[POD_ACCOUNT], POD.[POD_ORDER_NO]
),
[Posted] AS
(
    -- Grain: one row per Purchase Order with postings. Invoices positive, credits negative.
    -- The line link (DET_ORDER_LINK = POD_PRIMARY) is used only to find which order a
    -- transaction belongs to, then immediately grouped to order level.
    SELECT
         POD.[POD_COSTHEADER]        AS [ProjectCode]
        ,POD.[POD_ORDER_NO]          AS [OrderNumber]
        ,SUM(CASE WHEN DET.[DET_TYPE] = 'CRN' THEN -1 * DET.[DET_NETT] ELSE DET.[DET_NETT] END) AS [PostedValue]
        ,SUM(CASE WHEN DET.[DET_COSTCENTRE] LIKE 'LAB%' THEN CASE WHEN DET.[DET_TYPE] = 'CRN' THEN -1 * DET.[DET_NETT] ELSE DET.[DET_NETT] END ELSE 0 END) AS [PostedValue_Labour]
        ,SUM(CASE WHEN DET.[DET_COSTCENTRE] LIKE 'PLA%' THEN CASE WHEN DET.[DET_TYPE] = 'CRN' THEN -1 * DET.[DET_NETT] ELSE DET.[DET_NETT] END ELSE 0 END) AS [PostedValue_Plant]
        ,SUM(CASE WHEN DET.[DET_COSTCENTRE] LIKE 'MAT%' THEN CASE WHEN DET.[DET_TYPE] = 'CRN' THEN -1 * DET.[DET_NETT] ELSE DET.[DET_NETT] END ELSE 0 END) AS [PostedValue_Materials]
        ,SUM(CASE WHEN DET.[DET_COSTCENTRE] LIKE 'SUB%' THEN CASE WHEN DET.[DET_TYPE] = 'CRN' THEN -1 * DET.[DET_NETT] ELSE DET.[DET_NETT] END ELSE 0 END) AS [PostedValue_Subcontract]

    FROM [SL_PL_NL_DETAIL] DET

    JOIN [POP_DETAIL] POD
        ON POD.[POD_PRIMARY] = DET.[DET_ORDER_LINK]

    WHERE DET.[DET_LEDGER] = 'PL'
      AND DET.[DET_TYPE] IN ('INV', 'CRN')
      AND DET.[DET_BATCH_FLAG] = 0   -- posted only: exclude matched-but-unposted batch rows

    GROUP BY POD.[POD_COSTHEADER], POD.[POD_ORDER_NO]
)
SELECT
     CH.[CH_CODE]                    AS [ProjectCode]
    ,CH.[CH_NAME]                    AS [ProjectName]
    ,CH.[CH_USER1]                   AS [ProjectManager]
    ,PL.[SUNAME]                     AS [Supplier]
    ,O.[SupplierCode]
    ,O.[OrderNumber]
    ,ROUND(O.[OrderValue], 2)        AS [OrderValue]
    ,ROUND(ISNULL(P.[PostedValue], 0), 2)      AS [PostedValue]
    ,ROUND(ISNULL(P.[PostedValue_Labour], 0), 2)      AS [PostedValue_Labour]
    ,ROUND(ISNULL(P.[PostedValue_Plant], 0), 2)       AS [PostedValue_Plant]
    ,ROUND(ISNULL(P.[PostedValue_Materials], 0), 2)   AS [PostedValue_Materials]
    ,ROUND(ISNULL(P.[PostedValue_Subcontract], 0), 2) AS [PostedValue_Subcontract]
    -- The true value: negative when the order is over-invoiced
    ,ROUND(RAW.[Commitment_Raw], 2)  AS [Commitment_Raw]
    -- The headline figure: floored at zero
    ,ROUND(FLOORED.[Commitment], 2)  AS [Commitment]
    ,CASE WHEN RAW.[Commitment_Raw] < 0 THEN 'Y' ELSE 'N' END AS [OverInvoiced]

    -- Category split: order level commitment apportioned by the order's cost centre mix,
    -- so the categories always sum to the total by construction
    ,ROUND(FLOORED.[Commitment] * O.[OrderValue_Labour]      / NULLIF(O.[OrderValue], 0), 2) AS [Commitment_Labour]
    ,ROUND(FLOORED.[Commitment] * O.[OrderValue_Plant]       / NULLIF(O.[OrderValue], 0), 2) AS [Commitment_Plant]
    ,ROUND(FLOORED.[Commitment] * O.[OrderValue_Materials]   / NULLIF(O.[OrderValue], 0), 2) AS [Commitment_Materials]
    ,ROUND(FLOORED.[Commitment] * O.[OrderValue_Subcontract] / NULLIF(O.[OrderValue], 0), 2) AS [Commitment_Subcontract]

FROM [Orders] O

LEFT JOIN [Posted] P
    ON  P.[OrderNumber] = O.[OrderNumber]
    AND P.[ProjectCode] = O.[ProjectCode]

JOIN [CST_COSTHEADER] CH
    ON CH.[CH_CODE] = O.[ProjectCode]

JOIN [PL_ACCOUNTS] PL
    ON PL.[SUCODE] = O.[SupplierCode]

-- Named calculations, written once and reused (aliases cannot be referenced in the same SELECT)
CROSS APPLY (SELECT O.[OrderValue] - ISNULL(P.[PostedValue], 0) AS [Commitment_Raw]) RAW
CROSS APPLY (SELECT CASE WHEN RAW.[Commitment_Raw] < 0 THEN 0 ELSE RAW.[Commitment_Raw] END AS [Commitment]) FLOORED

Column Mapping (old report → view)

The old column names are bound into the SSRS layout and the CVR template pivots. Apply this mapping either by renaming in the report update or with a facade view over [BI].[Commitment_Order].

Old columnView columnNotes
ProjectCode / ProjectName / ProjectManagerProjectCode / ProjectName / ProjectManagerUnchanged.
Supplier / Supplier_CodeSupplier / SupplierCodeRename only.
Order_NumberOrderNumberRename only.
Order_Value (+ _Labour/_Plant/_Materials/_Subcontract)OrderValue (+ category columns)Rename only; now order level.
Transaction_TotalPostedValueRename only; CRN netting unchanged.
Transaction_Labour/_Plant/_Materials/_SubcontractPostedValue_Labour/_Plant/_Materials/_SubcontractRenamed, with correct CRN netting (fixing the old credit note bug in these columns).
Commitments_Total (+ categories)Commitment (+ category columns)Rename; categories now apportioned so they always sum to the total.
Item_Description / POD_COSTCENTRE / POD_PRIMARY / POD_LINE_NUMBER—Line level; served by [BI].[Commitment_OrderLine] instead.
—Commitment_Raw / OverInvoicedNew: true value and flag for over-invoiced orders.

Verification: Reconciliation Against the Old Report

Both the old RDL query and this calculation were run against the live Focalpoint database on 10 Sep 2026 for project 446459 (Maidenhead Grid). Every difference reconciles exactly to a known defect in the old calculation.

📊 commitment-reconciliation-446459.xlsx
Three sheets: Reconciliation (per order differences with explanations), Old Report Line Level (the RDL query output), New Query Order Level (this view's output).
Old report commitment
£2,826,879.10
View commitment
£2,784,701.58
Old report overstates by
£42,177.52
OrderSupplierOldNewDiffExplanation
912364PT Contractors Ltd 2,771,288.562,732,727.93+38,560.63 Line 1 ordered £389.50 but invoiced £38,950.13 (roughly 100×, a possible decimal keying error or a cert matched to the wrong line). The quantity gate zeroed the line, hiding a £38.5k over-invoice. Order level netting includes it.
920438Microll Engineering Ltd 3,698.490.00+3,698.49 The auto-split order (issue 24, the original worked example). Line 1 over-invoiced by £3,698.49 and gated to zero; line 2 under-invoiced by the same. The order is now fully invoiced; order level nets to exactly zero.
923899Reconomy UK Ltd 92.5043.89+48.61 Second line over-invoiced (£50 ordered, £98.61 posted), gated to zero by the old report.
914382Cable Services (Central) Ltd 30.000.00+30.00 Invoices swapped across two lines (£7,000/£7,030 and £1,988.45/£1,958.45). Old report showed £30 commitment; order level correctly nets to zero.
913556Circle Waste Limited 5,167.575,146.77+20.80 £23.40 posted against a £2.60 line, hidden by the quantity gate.
918053Apex Site Solutions Ltd −124.000.00−124.00 Old report showed a negative commitment, quietly reducing the project total. The view floors at zero and flags OverInvoiced.
916394Sunbelt Rentals Ltd −57.000.00−57.00 As above: negative on the old report, floored and flagged on the view.
918055Sunbelt Rentals Ltd 0.000.01−0.01 Quantity matched but 1p under-invoiced. The quantity gate hid it; the value rule shows it.
TOTAL 2,826,879.102,784,701.58+42,177.52 Reconciled exactly. All other orders agree between old and new.

Additional findings from the run

Change Detection: the ReviewStatus Gate (Issues 11 / 28)

Delivered 11 Sep 2026 as stored procedures (a view cannot take a date parameter): [BI].[usp_Commitment_Order] and [BI].[usp_Commitment_OrderLine], each taking @SinceDate and @CH_CODE. They return the matching view's output plus one column, ReviewStatus: 'Needs Review' if anything changed on the Purchase Order since @SinceDate, otherwise 'Previously Reviewed'. The report slicer defaults to 'Needs Review'. The flag is decided at order level and stamped on every row (including Unallocated rows in the line level proc), so a changed order pulls through whole.

An order is 'Needs Review' if either:

The period fence

Focalpoint does not record when a transaction posted: no posting timestamp exists anywhere (confirmed: ledger batches leave no dated record; DET_DATE_PUTIN is entry, not posting, so a row entered in one refresh cycle and posted in a later one would slip past any date filter). Two facts, proven against live data on 10 Sep 2026, make the accounting period a reliable fence instead:

Therefore every posting made after @SinceDate must sit in a period still open on @SinceDate. [BI].[PERIOD_CHANGE] (Freedom's dated log of every roll) gives the period that was open at any date; SYS_YEAR converts the letter year codes (C current, L last, O older, N next) to real years; both sides become one comparable number (year × 100 + period, e.g. 202607) and the fence is a single >= comparison. PL/PL pairing throughout: the row's purchase year/period against the purchase roll log, never the nominal ledger, which rolls on different dates. The then-open period also returns some pre-@SinceDate postings, a small accepted over-catch.

Open Questions

Related Tracker Issues

IssueRelevance
15The core fix: commitment report as the single source of truth. This view is that source.
18Once verified, the CVR loads commitment from this view automatically.
19Credits matched to the supplier account rather than the Purchase Order never release commitment. Process issue; the view tolerates it but cannot fix it.
20Invoices can be approved above Purchase Order value. The view floors at zero and flags rather than distorting.
24Auto-split of Purchase Order lines. Beaten by order level aggregation; see the reconciliation.
27Timing differences between batched and transacted items. Likely the register versus posted gap; to confirm with Elena.
29Point in time commitment. POP_DETAIL is modified in place, so this would need snapshots of the view's output.
33Focalpoint's own commitment figure not matching the true value. Users switch to this view's output instead.